{"action":"created","comment":{"url":"https://api.github.com/repos/hashicorp/vault/pulls/comments/80099551","id":80099551,"diff_hunk":"@@ -0,0 +1,1121 @@\n+package physical\n+\n+import (\n+\t\"crypto/tls\"\n+\t\"crypto/x509\"\n+\t\"errors\"\n+\t\"fmt\"\n+\t\"io/ioutil\"\n+\t\"net\"\n+\t\"net/url\"\n+\t\"sort\"\n+\t\"strconv\"\n+\t\"strings\"\n+\t\"sync\"\n+\t\"time\"\n+\n+\tlog \"github.com/mgutz/logxi/v1\"\n+\n+\t\"gopkg.in/mgo.v2\"\n+\t\"gopkg.in/mgo.v2/bson\"\n+)\n+\n+// this part was derived from and adjusted for our needs from:\n+//   builtin/logical/mongodb/util.go\n+//\n+// Unfortunately, mgo doesn't support the ssl parameter in its MongoDB URI parsing logic, so we have to handle that\n+// ourselves. See https://github.com/go-mgo/mgo/issues/84\n+func (b *MongoBackend) parseMongoURI(rawUri string) (*mgo.DialInfo, error) {\n+\turi, err := url.Parse(rawUri)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\tinfo := mgo.DialInfo{\n+\t\tAddrs:    strings.Split(uri.Host, \",\"),\n+\t\tDatabase: strings.TrimPrefix(uri.Path, \"/\"),\n+\t\tTimeout:  10 * time.Second,\n+\t}\n+\n+\tif uri.User != nil {\n+\t\tinfo.Username = uri.User.Username()\n+\t\tinfo.Password, _ = uri.User.Password()\n+\t}\n+\n+\turiSsl := false\n+\n+\tquery := uri.Query()\n+\tfor key, values := range query {\n+\t\tvar value string\n+\t\tif len(values) > 0 {\n+\t\t\tvalue = values[0]\n+\t\t}\n+\n+\t\tswitch key {\n+\t\tcase \"authSource\":\n+\t\t\tinfo.Source = value\n+\t\tcase \"authMechanism\":\n+\t\t\tinfo.Mechanism = value\n+\t\tcase \"gssapiServiceName\":\n+\t\t\tinfo.Service = value\n+\t\tcase \"replicaSet\":\n+\t\t\tinfo.ReplicaSetName = value\n+\t\tcase \"maxPoolSize\":\n+\t\t\tpoolLimit, err := strconv.Atoi(value)\n+\t\t\tif err != nil {\n+\t\t\t\treturn nil, errors.New(\"bad value for maxPoolSize: \" + value)\n+\t\t\t}\n+\t\t\tinfo.PoolLimit = poolLimit\n+\t\tcase \"ssl\":\n+\t\t\tssl, err := strconv.ParseBool(value)\n+\t\t\tif err != nil {\n+\t\t\t\treturn nil, errors.New(\"bad value for ssl: \" + value)\n+\t\t\t}\n+\t\t\tif ssl {\n+\t\t\t\turiSsl = true\n+\t\t\t}\n+\t\tcase \"connect\":\n+\t\t\tif value == \"direct\" {\n+\t\t\t\tinfo.Direct = true\n+\t\t\t\tbreak\n+\t\t\t}\n+\t\t\tif value == \"replicaSet\" {\n+\t\t\t\tbreak\n+\t\t\t}\n+\t\t\tfallthrough\n+\t\tdefault:\n+\t\t\treturn nil, errors.New(\"unsupported connection URL option: \" + key + \"=\" + value)\n+\t\t}\n+\t}\n+\n+\t// deal with TLS\n+\tif uriSsl || b.tls {\n+\t\ttlsConfig := tls.Config{}\n+\n+\t\tif b.tlsSkipVerify {\n+\t\t\ttlsConfig.InsecureSkipVerify = true\n+\t\t}\n+\n+\t\tif b.tlsCAFile != \"\" {\n+\t\t\tcaBytes, err := ioutil.ReadFile(b.tlsCAFile)\n+\t\t\tif err != nil {\n+\t\t\t\treturn nil, errors.New(\"could not read CA data from '\" + b.tlsCAFile + \"'\")\n+\t\t\t}\n+\t\t\tcaPool := x509.NewCertPool()\n+\t\t\tok := caPool.AppendCertsFromPEM(caBytes)\n+\t\t\tif !ok {\n+\t\t\t\tb.logger.Warn(fmt.Sprintf(\"physical/mongo: could not parse CAs from '%v'. Are you sure they are PEM encoded?\", b.tlsCAFile))\n+\t\t\t}\n+\t\t\ttlsConfig.RootCAs = caPool\n+\t\t}\n+\n+\t\tif b.tlsKeyFile != \"\" && b.tlsCertFile != \"\" {\n+\t\t\tcert, err := tls.LoadX509KeyPair(b.tlsCertFile, b.tlsKeyFile)\n+\t\t\tif err != nil {\n+\t\t\t\treturn nil, errors.New(\"could not load cert and/or key from '\" + b.tlsCertFile + \" / '\" + b.tlsKeyFile + \"'\")\n+\t\t\t}\n+\n+\t\t\ttlsConfig.Certificates = []tls.Certificate{cert}\n+\t\t}\n+\n+\t\tinfo.DialServer = func(addr *mgo.ServerAddr) (net.Conn, error) {\n+\t\t\treturn tls.Dial(\"tcp\", addr.String(), &tlsConfig)\n+\t\t}\n+\t}\n+\n+\treturn &info, nil\n+}\n+\n+var keyIndex mgo.Index = mgo.Index{\n+\tKey:    []string{\"key\"},\n+\tUnique: true,\n+}\n+\n+var lastCheckedInIndex mgo.Index = mgo.Index{\n+\tKey:         []string{\"lastCheckedIn\"},\n+\tExpireAfter: 5 * time.Second,\n+}\n+\n+// this part was derived from and adjusted for our needs from:\n+//   builtin/logical/mongodb/backend.go\n+//\n+func (b *MongoBackend) activeSession() (*mgo.Session, error) {\n+\tb.sessionLock.Lock()\n+\tdefer b.sessionLock.Unlock()\n+\n+\tif b.session != nil {\n+\t\tif err := b.session.Ping(); err == nil {\n+\t\t\treturn b.session, nil\n+\t\t}\n+\t\tb.session.Close()\n+\t\tb.ensuredDataIndices = false\n+\t}\n+\n+\t// we need to establish a new session\n+\tb.logger.Info(\"physical/mongo: establishing new MongoDB session\")\n+\tdialInfo, err := b.parseMongoURI(b.Url)\n+\tif err != nil {\n+\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: could not parse MongoDB URI: %v\", err))\n+\t\treturn nil, err\n+\t}\n+\n+\tb.session, err = mgo.DialWithInfo(dialInfo)\n+\tif err != nil {\n+\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: could not establish connection to MongoDB: %v\", err))\n+\t\treturn nil, err\n+\t}\n+\t// TODO: adjust these values\n+\tb.session.SetSyncTimeout(1 * time.Minute)\n+\tb.session.SetSocketTimeout(1 * time.Minute)\n+\n+\t// ensure indices\n+\tif !b.ensuredDataIndices {\n+\t\tb.logger.Info(fmt.Sprintf(\"physical/mongo: ensuring MongoDB indices for collection '%v' exist\", b.Collection))\n+\t\tc := b.session.DB(b.Database).C(b.Collection)\n+\t\terr = c.EnsureIndex(keyIndex)\n+\t\tif err != nil {\n+\t\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: could not EnsureIndex() for 'key': %v\", err))\n+\t\t\treturn nil, err\n+\t\t}\n+\n+\t\terr = c.EnsureIndex(lastCheckedInIndex)\n+\t\tif err != nil {\n+\t\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: could not EnsureIndex() for 'lastCheckedIn': %v\", err))\n+\t\t\treturn nil, err\n+\t\t}\n+\t\tb.ensuredDataIndices = true\n+\t}\n+\n+\treturn b.session, nil\n+}\n+\n+// this part was derived from and adjusted for our needs from:\n+//   builtin/logical/mongodb/backend.go\n+//\n+func (b *MongoBackend) activeSessionMsgReceiver() (*mgo.Session, error) {\n+\tb.sessionMsgReceiverLock.Lock()\n+\tdefer b.sessionMsgReceiverLock.Unlock()\n+\n+\tif b.sessionMsgReceiver != nil {\n+\t\tif err := b.sessionMsgReceiver.Ping(); err == nil {\n+\t\t\treturn b.sessionMsgReceiver, nil\n+\t\t}\n+\t\tb.sessionMsgReceiver.Close()\n+\t\tb.ensuredMsgCapped = false\n+\t}\n+\n+\t// we need to establish a new session\n+\tb.logger.Info(\"physical/mongo: establishing new MongoDB session for Broadcast Message Receiver\")\n+\tdialInfo, err := b.parseMongoURI(b.Url)\n+\tif err != nil {\n+\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: could not parse MongoDB URI: %v\", err))\n+\t\treturn nil, err\n+\t}\n+\n+\tb.sessionMsgReceiver, err = mgo.DialWithInfo(dialInfo)\n+\tif err != nil {\n+\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: could not establish connection to MongoDB: %v\", err))\n+\t\treturn nil, err\n+\t}\n+\t// TODO: adjust these values\n+\tb.sessionMsgReceiver.SetSyncTimeout(1 * time.Minute)\n+\tb.sessionMsgReceiver.SetSocketTimeout(1 * time.Minute)\n+\n+\tif b.haEnabled && !b.ensuredMsgCapped {\n+\t\tb.logger.Info(fmt.Sprintf(\"physical/mongo: ensuring MongoDB collection '%v' exists and is capped\", b.CollectionMsg))\n+\t\t// check if collection exists, create if not\n+\t\t// if it exitst, check if it is a capped collection, if not use convertToCapped (?)\n+\t\tdb := b.sessionMsgReceiver.DB(b.Database)\n+\t\tcolls, err := db.CollectionNames()\n+\t\tif err != nil {\n+\t\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: could not retrieve list of collection names: %v\", err))\n+\t\t\treturn nil, err\n+\t\t}\n+\n+\t\texists := false\n+\t\tfor _, coll := range colls {\n+\t\t\tif coll == b.CollectionMsg {\n+\t\t\t\texists = true\n+\t\t\t\tbreak\n+\t\t\t}\n+\t\t}\n+\n+\t\tif exists {\n+\t\t\tb.logger.Info(fmt.Sprintf(\"physical/mongo: MongoDB collection '%v' exists, ensuring it is capped\", b.CollectionMsg))\n+\t\t\tvar result CappedResult\n+\t\t\terr = db.Run(bson.D{bson.DocElem{Name: \"collStats\", Value: b.CollectionMsg}}, &result)\n+\t\t\tif err != nil {\n+\t\t\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: could not retrieve collStats: %v\", err))\n+\t\t\t\treturn nil, err\n+\t\t\t}\n+\n+\t\t\tif !result.Capped {\n+\t\t\t\tb.logger.Info(fmt.Sprintf(\"physical/mongo: MongoDB collection '%v' is not capped, running convertToCapped\", b.CollectionMsg))\n+\t\t\t\tvar result2 bson.M\n+\t\t\t\terr = db.Run(bson.D{bson.DocElem{Name: \"convertToCapped\", Value: b.CollectionMsg}, bson.DocElem{Name: \"size\", Value: 4096}}, &result2)\n+\t\t\t\tif err != nil {\n+\t\t\t\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: could not run convertToCapped: %v\", err))\n+\t\t\t\t\treturn nil, err\n+\t\t\t\t}\n+\t\t\t}\n+\t\t} else {\n+\t\t\tb.logger.Info(fmt.Sprintf(\"physical/mongo: MongoDB collection '%v' does not exist yet, creating it now\", b.CollectionMsg))\n+\t\t\terr = db.C(b.CollectionMsg).Create(&mgo.CollectionInfo{\n+\t\t\t\tCapped:   true,\n+\t\t\t\tMaxBytes: 4096,\n+\t\t\t\tMaxDocs:  1,\n+\t\t\t})\n+\t\t\tif err != nil {\n+\t\t\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: could not create collection '%v': %v\", b.CollectionMsg, err))\n+\t\t\t\treturn nil, err\n+\t\t\t}\n+\t\t}\n+\n+\t\t// check if we need to insert an initialization row\n+\t\t// otherwise vault will eat up CPU because the Tail() on the collection\n+\t\t// returns immediately which burns the for loop (see receiveBroadcast() for\n+\t\t// details)\n+\t\tn, err := db.C(b.CollectionMsg).Count()\n+\t\tif err != nil {\n+\t\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: could not count messages in collection '%v': %v\", b.CollectionMsg, err))\n+\t\t\treturn nil, err\n+\t\t}\n+\n+\t\tif n < 1 {\n+\t\t\tb.logger.Info(fmt.Sprintf(\"physical/mongo: inserting initialization row into '%v' collection\", b.CollectionMsg))\n+\t\t\terr = db.C(b.CollectionMsg).Insert(BroadcastMsg{Msg: MsgCollectionInitialization})\n+\t\t\tif err != nil {\n+\t\t\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: inserting initialization row into '%v' collection failed: %v\", b.CollectionMsg, err))\n+\t\t\t\treturn nil, err\n+\t\t\t}\n+\t\t}\n+\n+\t\t// only if we reach this part, we truly ensured the collection is capped\n+\t\tb.logger.Info(fmt.Sprintf(\"physical/mongo: MongoDB collection '%v' exists and is capped\", b.CollectionMsg))\n+\t\tb.ensuredMsgCapped = true\n+\t}\n+\n+\treturn b.sessionMsgReceiver, nil\n+}\n+\n+type CappedResult struct {\n+\tCapped bool\n+}\n+\n+type BroadcastMsg struct {\n+\tMsg string\n+}\n+\n+const MsgForcedRemoval = \"ForcedLeaderRemoval\"\n+const MsgStepDownPrimaryChange = \"SteppedDownPrimaryChanged\"\n+const MsgStepDownActiveUpdateFailure = \"SteppedDownActiveUpdateFailure\"\n+const MsgStepDownShutdown = \"SteppedDownShutdown\"\n+\n+// these 2 don't need to be checked in the isKnownBroadcastReason function\n+const MsgUnknownReason = \"UnknownReason\"\n+const MsgCollectionInitialization = \"CollectionInitialized\"\n+\n+func isKnownBroadcastReason(reason string) bool {\n+\tswitch reason {\n+\tcase MsgForcedRemoval:\n+\t\treturn true\n+\tcase MsgStepDownPrimaryChange:\n+\t\treturn true\n+\tcase MsgStepDownActiveUpdateFailure:\n+\t\treturn true\n+\tcase MsgStepDownShutdown:\n+\t\treturn true\n+\tdefault:\n+\t\treturn false\n+\t}\n+}\n+\n+func (b *MongoBackend) getDataCollection() (*mgo.Collection, error) {\n+\tsession, err := b.activeSession()\n+\tif err != nil {\n+\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: could not establish mongo session: %v\", err))\n+\t\treturn nil, err\n+\t}\n+\tc := session.DB(b.Database).C(b.Collection)\n+\treturn c, nil\n+}\n+\n+// MongoBackend is a physical backend that stores data on disk\n+type MongoBackend struct {\n+\tUrl                    string\n+\tDatabase               string\n+\tCollection             string\n+\tCollectionMsg          string\n+\tsessionLock            sync.Mutex\n+\tsessionMsgReceiverLock sync.Mutex\n+\tsession                *mgo.Session\n+\tsessionMsgReceiver     *mgo.Session\n+\tensuredDataIndices     bool\n+\tensuredMsgCapped       bool\n+\tlogger                 log.Logger\n+\thaEnabled              bool\n+\tlockOnPrimary          string\n+\ttls                    bool\n+\ttlsSkipVerify          bool\n+\ttlsCertFile            string\n+\ttlsKeyFile             string\n+\ttlsCAFile              string\n+\tlockValue              string\n+\tleaderCh               chan struct{}\n+\tisLeader               bool\n+\tbroadcastCh            chan BroadcastMsg\n+\tbroadcastChListening   bool\n+\tshutdownReason         string\n+}\n+\n+// newMongoBackend constructs a MongoBackend using the given directory\n+func newMongoBackend(conf map[string]string, logger log.Logger) (Backend, error) {\n+\tlogger.Debug(\"physical/mongo: newMongoBackend()\")\n+\turl, ok := conf[\"url\"]\n+\tif !ok {\n+\t\turl = \"mongodb://127.0.0.1:27017/vault\"\n+\t}\n+\n+\tdatabase, ok := conf[\"database\"]\n+\tif !ok {\n+\t\tdatabase = \"\"\n+\t}\n+\n+\tcollection, ok := conf[\"collection\"]\n+\tif !ok {\n+\t\tcollection = \"vault\"\n+\t}\n+\n+\tcollectionMsg, ok := conf[\"collection_ha_msg\"]\n+\tif !ok {\n+\t\tcollectionMsg = \"msg\"\n+\t}\n+\n+\ttls := false\n+\t_, ok = conf[\"tls\"]\n+\tif ok {\n+\t\ttls = true\n+\t}\n+\n+\thaEnabled := false\n+\t_, ok = conf[\"ha_enabled\"]\n+\tif ok {\n+\t\thaEnabled = true\n+\t}\n+\n+\tlockOnPrimary, ok := conf[\"lock_on_primary\"]\n+\tif !ok {\n+\t\tlockOnPrimary = \"\"\n+\t}\n+\n+\ttlsSkipVerify := false\n+\t_, ok = conf[\"tls_skip_verify\"]\n+\tif ok {\n+\t\ttlsSkipVerify = true\n+\t}\n+\n+\ttlsCAFile, ok := conf[\"tls_ca_file\"]\n+\tif !ok {\n+\t\ttlsCAFile = \"\"\n+\t}\n+\n+\ttlsCertFile, ok := conf[\"tls_cert_file\"]\n+\tif !ok {\n+\t\ttlsCertFile = \"\"\n+\t}\n+\n+\ttlsKeyFile, ok := conf[\"tls_key_file\"]\n+\tif !ok {\n+\t\ttlsKeyFile = \"\"\n+\t}\n+\n+\tvar broadcastCh chan BroadcastMsg\n+\tif haEnabled {\n+\t\tbroadcastCh = make(chan BroadcastMsg)\n+\t} else {\n+\t\tbroadcastCh = nil\n+\t}\n+\n+\tlogger.Debug(fmt.Sprintf(\"physical/mongo: newMongoBackend: (%v, %v, %v)\", url, database, collection))\n+\tb := MongoBackend{\n+\t\tUrl:           url,\n+\t\tDatabase:      database,\n+\t\tCollection:    collection,\n+\t\tCollectionMsg: collectionMsg,\n+\t\tlogger:        logger,\n+\t\thaEnabled:     haEnabled,\n+\t\tlockOnPrimary: lockOnPrimary,\n+\t\ttls:           tls,\n+\t\ttlsSkipVerify: tlsSkipVerify,\n+\t\ttlsCAFile:     tlsCAFile,\n+\t\ttlsCertFile:   tlsCertFile,\n+\t\ttlsKeyFile:    tlsKeyFile,\n+\t\tbroadcastCh:   broadcastCh,\n+\t}\n+\n+\tif haEnabled {\n+\t\t// monitor if we can remove the lock if in standby\n+\t\t// this is a last resort if the leader died (or went away for other reasons)\n+\t\tgo func() {\n+\t\t\tfor {\n+\t\t\t\tselect {\n+\t\t\t\tcase <-time.After(time.Second * 3):\n+\t\t\t\t\tif !b.isLeader {\n+\t\t\t\t\t\tb.logger.Debug(\"physical/mongo: not active, monitoring necessity to remove Lock\")\n+\t\t\t\t\t\terr := b.inactivePoller()\n+\n+\t\t\t\t\t\t// a different error from 'Not Found' should be the only interesting case\n+\t\t\t\t\t\tif err != nil && err != mgo.ErrNotFound {\n+\t\t\t\t\t\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: inactivePoller error: %v\", err))\n+\t\t\t\t\t\t}\n+\n+\t\t\t\t\t\t// removal was necessary, log and send broadcast\n+\t\t\t\t\t\tif err == nil {\n+\t\t\t\t\t\t\tb.logger.Info(\"physical/mongo: Lock was older than 5s and was therefore removed\")\n+\n+\t\t\t\t\t\t\t// sending broadcast\n+\t\t\t\t\t\t\terr := b.sendBroadcast(MsgForcedRemoval)\n+\t\t\t\t\t\t\tif err != nil {\n+\t\t\t\t\t\t\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: could not send broadcast message for forced removal: %v\", err))\n+\t\t\t\t\t\t\t}\n+\t\t\t\t\t\t}\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}()\n+\n+\t\t// starts and keeps the broadcast receiver running\n+\t\tgo func() {\n+\t\t\tfor {\n+\t\t\t\tb.logger.Debug(\"physical/mongo: starting broadcast receiver\")\n+\t\t\t\t// this is blocking\n+\t\t\t\tb.receiveBroadcast()\n+\t\t\t}\n+\t\t}()\n+\t}\n+\n+\treturn &b, nil\n+}\n+\n+func (b *MongoBackend) Delete(k string) error {\n+\tb.logger.Debug(fmt.Sprintf(\"physical/mongo: Delete(%v)\", k))\n+\tc, err := b.getDataCollection()\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\terr = c.Remove(bson.M{\"key\": k})\n+\tif err != nil {\n+\t\t// the docs/tests say that we are not supposed to fail on a delete if the\n+\t\t// entry does not exist\n+\t\tif err == mgo.ErrNotFound {\n+\t\t\treturn nil\n+\t\t}\n+\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: Delete(%v): error in Remove() : %v\", k, err))\n+\t\treturn err\n+\t}\n+\treturn nil\n+}\n+\n+func (b *MongoBackend) Get(k string) (*Entry, error) {\n+\tb.logger.Debug(fmt.Sprintf(\"physical/mongo: Get(%v)\", k))\n+\tc, err := b.getDataCollection()\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\tq := c.Find(bson.M{\"key\": k})\n+\tn, err := q.Count()\n+\tif err != nil {\n+\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: Get(%v): error in Count() : %v\", k, err))\n+\t\treturn nil, err\n+\t}\n+\n+\t// not found requires us to return nil and not throw an error\n+\t// make an exception\n+\tif n <= 0 {\n+\t\tb.logger.Debug(fmt.Sprintf(\"physical/mongo: Get(%v): not found\", k))\n+\t\treturn nil, nil\n+\t}\n+\tvar entry Entry\n+\terr = q.One(&entry)\n+\tif err != nil {\n+\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: Get(%v): error in One(): %v\", k, err))\n+\t\treturn nil, err\n+\t}\n+\treturn &entry, nil\n+}\n+\n+func (b *MongoBackend) Put(entry *Entry) error {\n+\tb.logger.Debug(fmt.Sprintf(\"physical/mongo: Put(%v)\", entry.Key))\n+\tc, err := b.getDataCollection()\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\t_, err = c.Upsert(bson.M{\"key\": entry.Key}, entry)\n+\tif err != nil {\n+\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: Put(%v): error in Upsert(): %v\", entry.Key, err))\n+\t\treturn err\n+\t}\n+\treturn nil\n+}\n+\n+func (b *MongoBackend) List(prefix string) ([]string, error) {\n+\tb.logger.Debug(fmt.Sprintf(\"physical/mongo: List(%v)\", prefix))\n+\tc, err := b.getDataCollection()\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\tvar results []string\n+\n+\t// The prefix needs to get its slashes replaced with '\\/' so that it can form\n+\t// a proper regex\n+\tp := strings.Replace(prefix, \"/\", \"\\\\/\", -1)\n+\tregex := `^` + p\n+\n+\titer := c.Find(bson.M{\"key\": bson.M{\"$regex\": bson.RegEx{Pattern: regex, Options: \"\"}}}).\n+\t\tSelect(bson.M{\"key\": 1}).\n+\t\tIter()\n+\n+\tvar result Entry\n+\tfor iter.Next(&result) {\n+\t\tb.logger.Trace(fmt.Sprintf(\"physical/mongo: List(%v): Next(%v)\", prefix, result))\n+\t\t// we remove the prefix from the result and add it to the return list\n+\t\tkey := strings.TrimPrefix(result.Key, prefix)\n+\t\tif strings.ContainsAny(key, \"/\") {\n+\t\t\tdirKey := strings.SplitAfter(key, \"/\")[0]\n+\t\t\tinResults := false\n+\t\t\tfor _, a := range results {\n+\t\t\t\tif a == dirKey {\n+\t\t\t\t\tinResults = true\n+\t\t\t\t\tbreak\n+\t\t\t\t}\n+\t\t\t}\n+\t\t\tif !inResults {\n+\t\t\t\t// \"on prepending\" here\n+\t\t\t\t//\n+\t\t\t\t// the general attitude should be:\n+\t\t\t\t// - append to arrays\n+\t\t\t\t// - prepend to lists\n+\t\t\t\t//\n+\t\t\t\t// however, the for loop above would need to loop potentially over\n+\t\t\t\t// a lot of results, this gives us an early break there\n+\t\t\t\t//\n+\t\t\t\t//results = append(results, dirKey)\n+\t\t\t\t//append([]string{\"Prepend Item\"}, data...)\n+\t\t\t\tresults = append([]string{dirKey}, results...)\n+\t\t\t}\n+\t\t} else {\n+\t\t\tresults = append(results, key)\n+\t\t}\n+\t}\n+\n+\terr = iter.Close()\n+\tif err != nil {\n+\t\tb.logger.Error(fmt.Sprintf(\"physical/mongo: List(%v): error in iter.Next(): %v\", prefix, err))\n+\t\treturn nil, err\n+\t}\n+\n+\t// sort\n+\tsort.Strings(results)\n+\n+\tb.logger.Debug(fmt.Sprintf(\"physical/mongo: List(%v): %v\", prefix, results))\n+\n+\treturn results, nil\n+}\n+\n+// This is configurable right now, as HA involves more routines that we don't\n+// necessarily want to run, if not really necessary\n+// The default is off\n+func (b *MongoBackend) HAEnabled() bool {\n+\tb.logger.Debug(fmt.Sprintf(\"physical/mongo: HAEnabled(%v)\", b.haEnabled))\n+\treturn b.haEnabled\n+}\n+\n+// LockWith is used for mutual exclusion based on the given key.\n+func (b *MongoBackend) LockWith(key, value string) (Lock, error) {\n+\tb.logger.Debug(fmt.Sprintf(\"physical/mongo: LockWith(%v, %v)\", key, value))\n+\tl := &MongoLock{\n+\t\tb:     b,\n+\t\tkey:   key,\n+\t\tvalue: value,\n+\t}\n+\treturn l, nil\n+}\n+\n+type MongoLock struct {\n+\tb        *MongoBackend\n+\tkey      string\n+\tvalue    string\n+\tllCh     chan struct{}\n+\tllChOpen bool\n+}\n+\n+type LockEntry struct {\n+\tKey           string\n+\tValue         string\n+\tLastCheckedIn bson.MongoTimestamp\n+}\n+\n+// Lock is used to acquire the given lock\n+// The stopCh is optional and if closed should interrupt the lock\n+// acquisition attempt. The return struct should be closed when\n+// leadership is lost.\n+func (l *MongoLock) Lock(stopCh <-chan struct{}) (<-chan struct{}, error) {\n+\tl.b.logger.Debug(fmt.Sprintf(\"physical/mongo: Lock(%v)\", l.value))\n+\n+\t// the \"read\" lock needs special treatment\n+\tif l.value == \"read\" {\n+\t\treturn l.b.leaderCh, nil\n+\t}\n+\n+\t// Attempt an async acquisition\n+\tdidLock := make(chan bool)\n+\treleaseCh := make(chan bool, 1)\n+\tgo func() {\n+\t\t// the acquire function definition\n+\t\tacquire := func() {\n+\t\t\t// check primary first\n+\t\t\tif l.b.lockOnPrimary != \"\" {\n+\t\t\t\tcurrentPrimary, err := l.getPrimary()\n+\t\t\t\tif err != nil {\n+\t\t\t\t\tl.b.logger.Error(fmt.Sprintf(\"physical/mongo: could not get primary: %v\", err))\n+\t\t\t\t\treturn\n+\t\t\t\t}\n+\n+\t\t\t\tif l.b.lockOnPrimary != currentPrimary {\n+\t\t\t\t\tl.b.logger.Warn(fmt.Sprintf(\"current MongoDB primary is not '%v', but '%v'. Not attempting to become leader.\", l.b.lockOnPrimary, currentPrimary))\n+\t\t\t\t\treturn\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\t// get the collection and try to insert\n+\t\t\tc, err := l.b.getDataCollection()\n+\t\t\tif err != nil {\n+\t\t\t\treturn\n+\t\t\t}\n+\n+\t\t\terr = c.Insert(bson.M{\"key\": l.key, \"value\": l.value, \"lastCheckedIn\": bson.Now()})\n+\t\t\tif err != nil {\n+\t\t\t\tl.b.logger.Debug(fmt.Sprintf(\"physical/mongo: acquireLock failed: %v\", err))\n+\t\t\t\treturn\n+\t\t\t}\n+\n+\t\t\t// Signal that lock is held\n+\t\t\tdidLock <- true\n+\t\t\treturn\n+\t\t}\n+\n+\t\t// run the acquire() function first once, then go into the loop\n+\t\tacquire()\n+\t\tl.b.broadcastChListening = true\n+\tacquireLoop:\n+\t\tfor {\n+\t\t\tselect {\n+\t\t\tcase msg := <-l.b.broadcastCh:\n+\t\t\t\tif isKnownBroadcastReason(msg.Msg) {\n+\t\t\t\t\tl.b.logger.Info(fmt.Sprintf(\"physical/mongo: received broadcast '%v'. Trying to acquire lock.\", msg.Msg))\n+\t\t\t\t\tacquire()\n+\t\t\t\t} else if msg.Msg == MsgCollectionInitialization {\n+\t\t\t\t\tl.b.logger.Debug(fmt.Sprintf(\"physical/mongo: received broadcast '%v'. Broadcast collection '%v' has just been initialized. Not trying to acquire lock.\", l.b.CollectionMsg, msg.Msg))\n+\t\t\t\t} else {\n+\t\t\t\t\tl.b.logger.Warn(fmt.Sprintf(\"physical/mongo: received unknown broadcast: '%v'. Not trying to acquire lock.\", msg.Msg))\n+\t\t\t\t}\n+\n+\t\t\tcase <-time.After(time.Second * 30):\n+\t\t\t\tl.b.logger.Debug(\"physical/mongo: reached 30s impatient timeout. Trying to acquire lock.\")\n+\t\t\t\tacquire()\n+\n+\t\t\t\t// Handle an early abort\n+\t\t\tcase release := <-releaseCh:\n+\t\t\t\tif release {\n+\t\t\t\t\t// remove lock again\n+\t\t\t\t\tl.b.logger.Debug(\"physical/mongo: early release on Lock requested\")\n+\t\t\t\t\tc, err := l.b.getDataCollection()\n+\t\t\t\t\tif err == nil {\n+\t\t\t\t\t\terr = c.Remove(bson.M{\"key\": l.key, \"value\": l.value})\n+\t\t\t\t\t\tif err != nil {\n+\t\t\t\t\t\t\tl.b.logger.Error(fmt.Sprintf(\"physical/mongo: earlyRelease: %v\", err))\n+\t\t\t\t\t\t}\n+\t\t\t\t\t}\n+\n+\t\t\t\t\t// and send broadcast that the remove happened\n+\t\t\t\t\tl.b.shutdownReason = MsgStepDownShutdown\n+\t\t\t\t\terr = l.b.sendBroadcast(MsgStepDownShutdown)\n+\t\t\t\t\tif err != nil {\n+\t\t\t\t\t\tl.b.logger.Error(fmt.Sprintf(\"physical/mongo: l.Unlock() could not send broadcast message '%v': %v\", MsgStepDownShutdown, err))\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t\tbreak acquireLoop\n+\t\t\t}\n+\t\t}\n+\t\tl.b.broadcastChListening = false\n+\t\tl.b.logger.Debug(\"physical/mongo: acquireLoop broken\")\n+\t}()\n+\n+\t// Wait for lock acquisition or shutdown\n+\tselect {\n+\tcase <-stopCh:\n+\t\treleaseCh <- true\n+\t\tl.b.logger.Info(\"physical/mongo: Early Lock release requested\")\n+\t\treturn nil, nil\n+\tcase <-didLock:\n+\t\treleaseCh <- false\n+\t}\n+\tclose(didLock)\n+\tclose(releaseCh)\n+\n+\t// acquired :)\n+\tl.b.lockValue = l.value\n+\tl.b.isLeader = true\n+\tl.b.leaderCh = make(chan struct{})\n+\n+\t// poll and update the lock as long as we are the leader\n+\tabortCh1 := make(chan struct{})\n+\t// we initialize this one only later if we really use it\n+\tvar abortCh2 chan struct{}\n+\tl.llCh = make(chan struct{})\n+\tl.llChOpen = true\n+\tgo func() {\n+\tupdateLoop:\n+\t\tfor {\n+\t\t\tselect {\n+\t\t\tcase <-abortCh1:\n+\t\t\t\tl.b.logger.Trace(\"physical/mongo: abortCh1\")\n+\t\t\t\tclose(abortCh1)\n+\t\t\t\tbreak updateLoop\n+\t\t\tcase <-time.After(time.Second * 3):\n+\t\t\t\tif l.b.isLeader {\n+\t\t\t\t\terr := l.activePoller()\n+\t\t\t\t\tif err != nil {\n+\t\t\t\t\t\tl.b.logger.Error(fmt.Sprintf(\"physical/mongo: l.Lock(): activePoller() error, going to lose leadership: %v\", err))\n+\t\t\t\t\t\tif l.llChOpen {\n+\t\t\t\t\t\t\tl.b.shutdownReason = MsgStepDownActiveUpdateFailure\n+\t\t\t\t\t\t\tl.llCh <- struct{}{}\n+\t\t\t\t\t\t}\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}\n+\t}()\n+\n+\t// poll and break if the primary changes\n+\tif l.b.lockOnPrimary != \"\" {\n+\t\tabortCh2 = make(chan struct{})\n+\t\tgo func() {\n+\t\tprimaryLoop:\n+\t\t\tfor {\n+\t\t\t\tl.b.logger.Trace(\"physical/mongo: primary monitor\")\n+\t\t\t\tselect {\n+\t\t\t\tcase <-abortCh2:\n+\t\t\t\t\tl.b.logger.Trace(\"physical/mongo: abortCh2\")\n+\t\t\t\t\tclose(abortCh2)\n+\t\t\t\t\tbreak primaryLoop\n+\t\t\t\tcase <-time.After(time.Second * 1):\n+\t\t\t\t\tcurrentPrimary, err := l.getPrimary()\n+\t\t\t\t\tif err != nil {\n+\t\t\t\t\t\tl.b.logger.Warn(fmt.Sprintf(\"physical/mongo: could not get primary: %v\", err))\n+\t\t\t\t\t\tbreak\n+\t\t\t\t\t}\n+\n+\t\t\t\t\tif l.b.lockOnPrimary != currentPrimary {\n+\t\t\t\t\t\tl.b.logger.Info(fmt.Sprintf(\"physical/mongo: MongoDB PRIMARY changed to '%v'. Dropping leadership.\", currentPrimary))\n+\t\t\t\t\t\tif l.llChOpen {\n+\t\t\t\t\t\t\tl.b.shutdownReason = MsgStepDownPrimaryChange\n+\t\t\t\t\t\t\tl.llCh <- struct{}{}\n+\t\t\t\t\t\t}\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n+\t\t}()\n+\t}\n+\n+\t// abort and call lose leadership if there is something in the llCh channel\n+\tgo func() {\n+\t\tl.b.logger.Trace(\"physical/mongo: llCh before\")\n+\t\tif l.llChOpen {\n+\t\t\t<-l.llCh\n+\t\t\tl.llChOpen = false\n+\t\t\tclose(l.llCh)\n+\t\t\tl.llCh = nil\n+\t\t\tl.b.logger.Trace(\"physical/mongo: llCh after\")\n+\n+\t\t\t// lose leadership\n+\t\t\tl.b.logger.Trace(\"physical/mongo: loseLeadership() begin\")\n+\t\t\tl.b.lockValue = \"\"\n+\t\t\tl.b.isLeader = false\n+\t\t\tabortCh1 <- struct{}{}\n+\t\t\tif l.b.lockOnPrimary != \"\" {\n+\t\t\t\tabortCh2 <- struct{}{}\n+\t\t\t}\n+\t\t\tclose(l.b.leaderCh)\n+\t\t\tl.b.leaderCh = nil\n+\t\t\tl.b.logger.Trace(\"physical/mongo: loseLeadership() end\")\n+\t\t}\n+\t}()\n+\n+\t// check a last time if stop was requested, and stop everything before\n+\t// returning nil\n+\tselect {\n+\tcase <-stopCh:\n+\t\tif l.llChOpen {\n+\t\t\t// remove lock again\n+\t\t\tl.b.logger.Debug(\"physical/mongo: early release on Lock requested\")\n+\t\t\tc, err := l.b.getDataCollection()\n+\t\t\tif err == nil {\n+\t\t\t\terr = c.Remove(bson.M{\"key\": l.key, \"value\": l.value})\n+\t\t\t\tif err != nil {\n+\t\t\t\t\tl.b.logger.Error(fmt.Sprintf(\"physical/mongo: earlyRelease: %v\", err))\n+\t\t\t\t}\n+\t\t\t}\n+\n+\t\t\t// and send broadcast that the remove happened\n+\t\t\tl.b.shutdownReason = MsgStepDownShutdown\n+\t\t\terr = l.b.sendBroadcast(MsgStepDownShutdown)\n+\t\t\tif err != nil {\n+\t\t\t\tl.b.logger.Error(fmt.Sprintf(\"physical/mongo: l.Unlock() could not send broadcast message '%v': %v\", MsgStepDownShutdown, err))\n+\t\t\t}\n+\t\t\tl.llCh <- struct{}{}\n+\t\t}\n+\t\treturn nil, nil\n+\tdefault:\n+\t}\n+\n+\treturn l.b.leaderCh, nil\n+}\n+\n+// Unlock is used to release the lock\n+func (l *MongoLock) Unlock() error {\n+\tl.b.logger.Debug(\"physical/mongo: Unlock()\")\n+\tif l.value == \"read\" {\n+\t\tl.b.logger.Trace(\"physical/mongo: Unlock() for 'read' lock!\")\n+\t\treturn nil\n+\t}\n+\n+\t// lose leadership, stop go routines and close channels\n+\tif l.b.isLeader && l.llChOpen {\n+\t\tl.b.shutdownReason = MsgStepDownShutdown\n+\t\tl.llCh <- struct{}{}\n+\t}\n+\n+\tc, err := l.b.getDataCollection()\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\terr = c.Remove(bson.M{\"key\": l.key, \"value\": l.value})\n+\tif err != nil {\n+\t\tl.b.logger.Error(fmt.Sprintf(\"physical/mongo: l.Unlock() could not remove lock: %v\", err))\n+\t\treturn err\n+\t}\n+\n+\t// send broadcast message that we are Unlocking\n+\tvar msg string\n+\tif len(l.b.shutdownReason) > 0 {\n+\t\tmsg = l.b.shutdownReason\n+\t} else {\n+\t\tmsg = MsgUnknownReason\n+\t}\n+\terr = l.b.sendBroadcast(msg)\n+\tif err != nil {\n+\t\tl.b.logger.Error(fmt.Sprintf(\"physical/mongo: l.Unlock() could not send broadcast message '%v': %v\", msg, err))\n+\t}\n+\n+\treturn nil\n+}\n+\n+func (l *MongoLock) activePoller() error {\n+\tl.b.logger.Trace(\"physical/mongo: activePoller()\")\n+\tc, err := l.b.getDataCollection()\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\terr = c.Update(bson.M{\"key\": l.key, \"value\": l.value}, bson.M{\"$currentDate\": bson.M{\"lastCheckedIn\": bson.M{\"$type\": \"date\"}}})\n+\tif err != nil {\n+\t\tl.b.logger.Error(fmt.Sprintf(\"physical/mongo: activePoller(): Update() failed: %v\", err))\n+\t\treturn err\n+\t}\n+\n+\treturn nil\n+}\n+\n+func (b *MongoBackend) inactivePoller() error {\n+\tb.logger.Trace(\"physical/mongo: inactivePoller()\")\n+\tc, err := b.getDataCollection()\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\t// remove leader if timestamp too old\n+\t//\n+\t// PROBLEM 1: we're working in the second range and likely with a lot of vault servers together\n+\t//            of course time \"should\" be the same amongst servers, but we shouldn't assume it.\n+\t//            Therefore we need a serverside solution for a time check and removal\n+\t//\n+\t// PROBLEM 2: even though we have an index that expires the lock document, the trouble is that\n+\t//            this runs as a background task in mongod (and only on the primary), and is only\n+\t//            running once every 60s. This resolution is not enough. Furthermore, I think that\n+\t//            index won't work anyway, as we're also using a Unique Index on 'key'.\n+\t//\n+\t// SOLUTION:  not pretty, but a mongo \"$where\" clause executes JavaScript on the mongo server,\n+\t//            and therefore will not have trouble with the time. Better solutions are always\n+\t//            welcomed :)\n+\t//\n+\t//  db.vault.find({\"key\":\"core/lock\", $where:\"this.lastCheckedIn <= new Date(ISODate().getTime()-1000*5)\"})\n+\treturn c.Remove(bson.M{\"key\": \"core/lock\", \"$where\": \"this.lastCheckedIn <= new Date(ISODate().getTime()-1000*5)\"})","path":"physical/mongo.go","position":968,"original_position":968,"commit_id":"fd452d07a14ffa9cc61c23a5435ddb0671131512","original_commit_id":"fd452d07a14ffa9cc61c23a5435ddb0671131512","user":{"login":"mheese","id":1955729,"avatar_url":"https://avatars.githubusercontent.com/u/1955729?v=3","gravatar_id":"","url":"https://api.github.com/users/mheese","html_url":"https://github.com/mheese","followers_url":"https://api.github.com/users/mheese/followers","following_url":"https://api.github.com/users/mheese/following{/other_user}","gists_url":"https://api.github.com/users/mheese/gists{/gist_id}","starred_url":"https://api.github.com/users/mheese/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mheese/subscriptions","organizations_url":"https://api.github.com/users/mheese/orgs","repos_url":"https://api.github.com/users/mheese/repos","events_url":"https://api.github.com/users/mheese/events{/privacy}","received_events_url":"https://api.github.com/users/mheese/received_events","type":"User","site_admin":false},"body":"agreed ... I will change that","created_at":"2016-09-22T17:58:08Z","updated_at":"2016-09-22T17:58:08Z","html_url":"https://github.com/hashicorp/vault/pull/1847#discussion_r80099551","pull_request_url":"https://api.github.com/repos/hashicorp/vault/pulls/1847","_links":{"self":{"href":"https://api.github.com/repos/hashicorp/vault/pulls/comments/80099551"},"html":{"href":"https://github.com/hashicorp/vault/pull/1847#discussion_r80099551"},"pull_request":{"href":"https://api.github.com/repos/hashicorp/vault/pulls/1847"}}},"pull_request":{"url":"https://api.github.com/repos/hashicorp/vault/pulls/1847","id":83839061,"html_url":"https://github.com/hashicorp/vault/pull/1847","diff_url":"https://github.com/hashicorp/vault/pull/1847.diff","patch_url":"https://github.com/hashicorp/vault/pull/1847.patch","issue_url":"https://api.github.com/repos/hashicorp/vault/issues/1847","number":1847,"state":"open","locked":false,"title":"Support for MongoDB as physical backend","user":{"login":"mheese","id":1955729,"avatar_url":"https://avatars.githubusercontent.com/u/1955729?v=3","gravatar_id":"","url":"https://api.github.com/users/mheese","html_url":"https://github.com/mheese","followers_url":"https://api.github.com/users/mheese/followers","following_url":"https://api.github.com/users/mheese/following{/other_user}","gists_url":"https://api.github.com/users/mheese/gists{/gist_id}","starred_url":"https://api.github.com/users/mheese/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mheese/subscriptions","organizations_url":"https://api.github.com/users/mheese/orgs","repos_url":"https://api.github.com/users/mheese/repos","events_url":"https://api.github.com/users/mheese/events{/privacy}","received_events_url":"https://api.github.com/users/mheese/received_events","type":"User","site_admin":false},"body":"This is a new / follow up PR on #1776 (as I couln't change the source branch there).\r\n\r\nI wasn't able to test the Travis CI implementation on my own repo as this all is apparently configured and adjusted for working with the *hashicorp/vault* repo. I will use this PR in order to completely figure out a passing Travis build.","created_at":"2016-09-02T17:42:35Z","updated_at":"2016-09-22T17:58:08Z","closed_at":null,"merged_at":null,"merge_commit_sha":"76e1c2c666a8d9162b0c857bb721e7fe8ca016b3","assignee":{"login":"jefferai","id":28627,"avatar_url":"https://avatars.githubusercontent.com/u/28627?v=3","gravatar_id":"","url":"https://api.github.com/users/jefferai","html_url":"https://github.com/jefferai","followers_url":"https://api.github.com/users/jefferai/followers","following_url":"https://api.github.com/users/jefferai/following{/other_user}","gists_url":"https://api.github.com/users/jefferai/gists{/gist_id}","starred_url":"https://api.github.com/users/jefferai/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefferai/subscriptions","organizations_url":"https://api.github.com/users/jefferai/orgs","repos_url":"https://api.github.com/users/jefferai/repos","events_url":"https://api.github.com/users/jefferai/events{/privacy}","received_events_url":"https://api.github.com/users/jefferai/received_events","type":"User","site_admin":false},"assignees":[{"login":"jefferai","id":28627,"avatar_url":"https://avatars.githubusercontent.com/u/28627?v=3","gravatar_id":"","url":"https://api.github.com/users/jefferai","html_url":"https://github.com/jefferai","followers_url":"https://api.github.com/users/jefferai/followers","following_url":"https://api.github.com/users/jefferai/following{/other_user}","gists_url":"https://api.github.com/users/jefferai/gists{/gist_id}","starred_url":"https://api.github.com/users/jefferai/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefferai/subscriptions","organizations_url":"https://api.github.com/users/jefferai/orgs","repos_url":"https://api.github.com/users/jefferai/repos","events_url":"https://api.github.com/users/jefferai/events{/privacy}","received_events_url":"https://api.github.com/users/jefferai/received_events","type":"User","site_admin":false}],"milestone":{"url":"https://api.github.com/repos/hashicorp/vault/milestones/11","html_url":"https://github.com/hashicorp/vault/milestones/0.6.2","labels_url":"https://api.github.com/repos/hashicorp/vault/milestones/11/labels","id":1834013,"number":11,"title":"0.6.2","description":null,"creator":{"login":"jefferai","id":28627,"avatar_url":"https://avatars.githubusercontent.com/u/28627?v=3","gravatar_id":"","url":"https://api.github.com/users/jefferai","html_url":"https://github.com/jefferai","followers_url":"https://api.github.com/users/jefferai/followers","following_url":"https://api.github.com/users/jefferai/following{/other_user}","gists_url":"https://api.github.com/users/jefferai/gists{/gist_id}","starred_url":"https://api.github.com/users/jefferai/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/jefferai/subscriptions","organizations_url":"https://api.github.com/users/jefferai/orgs","repos_url":"https://api.github.com/users/jefferai/repos","events_url":"https://api.github.com/users/jefferai/events{/privacy}","received_events_url":"https://api.github.com/users/jefferai/received_events","type":"User","site_admin":false},"open_issues":8,"closed_issues":62,"state":"open","created_at":"2016-06-17T08:54:26Z","updated_at":"2016-09-22T12:35:24Z","due_on":null,"closed_at":null},"commits_url":"https://api.github.com/repos/hashicorp/vault/pulls/1847/commits","review_comments_url":"https://api.github.com/repos/hashicorp/vault/pulls/1847/comments","review_comment_url":"https://api.github.com/repos/hashicorp/vault/pulls/comments{/number}","comments_url":"https://api.github.com/repos/hashicorp/vault/issues/1847/comments","statuses_url":"https://api.github.com/repos/hashicorp/vault/statuses/fd452d07a14ffa9cc61c23a5435ddb0671131512","head":{"label":"fintechdev:mongo-pr","ref":"mongo-pr","sha":"fd452d07a14ffa9cc61c23a5435ddb0671131512","user":{"login":"fintechdev","id":19216330,"avatar_url":"https://avatars.githubusercontent.com/u/19216330?v=3","gravatar_id":"","url":"https://api.github.com/users/fintechdev","html_url":"https://github.com/fintechdev","followers_url":"https://api.github.com/users/fintechdev/followers","following_url":"https://api.github.com/users/fintechdev/following{/other_user}","gists_url":"https://api.github.com/users/fintechdev/gists{/gist_id}","starred_url":"https://api.github.com/users/fintechdev/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fintechdev/subscriptions","organizations_url":"https://api.github.com/users/fintechdev/orgs","repos_url":"https://api.github.com/users/fintechdev/repos","events_url":"https://api.github.com/users/fintechdev/events{/privacy}","received_events_url":"https://api.github.com/users/fintechdev/received_events","type":"Organization","site_admin":false},"repo":{"id":65977974,"name":"vault","full_name":"fintechdev/vault","owner":{"login":"fintechdev","id":19216330,"avatar_url":"https://avatars.githubusercontent.com/u/19216330?v=3","gravatar_id":"","url":"https://api.github.com/users/fintechdev","html_url":"https://github.com/fintechdev","followers_url":"https://api.github.com/users/fintechdev/followers","following_url":"https://api.github.com/users/fintechdev/following{/other_user}","gists_url":"https://api.github.com/users/fintechdev/gists{/gist_id}","starred_url":"https://api.github.com/users/fintechdev/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/fintechdev/subscriptions","organizations_url":"https://api.github.com/users/fintechdev/orgs","repos_url":"https://api.github.com/users/fintechdev/repos","events_url":"https://api.github.com/users/fintechdev/events{/privacy}","received_events_url":"https://api.github.com/users/fintechdev/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/fintechdev/vault","description":"A tool for managing secrets.","fork":true,"url":"https://api.github.com/repos/fintechdev/vault","forks_url":"https://api.github.com/repos/fintechdev/vault/forks","keys_url":"https://api.github.com/repos/fintechdev/vault/keys{/key_id}","collaborators_url":"https://api.github.com/repos/fintechdev/vault/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/fintechdev/vault/teams","hooks_url":"https://api.github.com/repos/fintechdev/vault/hooks","issue_events_url":"https://api.github.com/repos/fintechdev/vault/issues/events{/number}","events_url":"https://api.github.com/repos/fintechdev/vault/events","assignees_url":"https://api.github.com/repos/fintechdev/vault/assignees{/user}","branches_url":"https://api.github.com/repos/fintechdev/vault/branches{/branch}","tags_url":"https://api.github.com/repos/fintechdev/vault/tags","blobs_url":"https://api.github.com/repos/fintechdev/vault/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/fintechdev/vault/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/fintechdev/vault/git/refs{/sha}","trees_url":"https://api.github.com/repos/fintechdev/vault/git/trees{/sha}","statuses_url":"https://api.github.com/repos/fintechdev/vault/statuses/{sha}","languages_url":"https://api.github.com/repos/fintechdev/vault/languages","stargazers_url":"https://api.github.com/repos/fintechdev/vault/stargazers","contributors_url":"https://api.github.com/repos/fintechdev/vault/contributors","subscribers_url":"https://api.github.com/repos/fintechdev/vault/subscribers","subscription_url":"https://api.github.com/repos/fintechdev/vault/subscription","commits_url":"https://api.github.com/repos/fintechdev/vault/commits{/sha}","git_commits_url":"https://api.github.com/repos/fintechdev/vault/git/commits{/sha}","comments_url":"https://api.github.com/repos/fintechdev/vault/comments{/number}","issue_comment_url":"https://api.github.com/repos/fintechdev/vault/issues/comments{/number}","contents_url":"https://api.github.com/repos/fintechdev/vault/contents/{+path}","compare_url":"https://api.github.com/repos/fintechdev/vault/compare/{base}...{head}","merges_url":"https://api.github.com/repos/fintechdev/vault/merges","archive_url":"https://api.github.com/repos/fintechdev/vault/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/fintechdev/vault/downloads","issues_url":"https://api.github.com/repos/fintechdev/vault/issues{/number}","pulls_url":"https://api.github.com/repos/fintechdev/vault/pulls{/number}","milestones_url":"https://api.github.com/repos/fintechdev/vault/milestones{/number}","notifications_url":"https://api.github.com/repos/fintechdev/vault/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/fintechdev/vault/labels{/name}","releases_url":"https://api.github.com/repos/fintechdev/vault/releases{/id}","deployments_url":"https://api.github.com/repos/fintechdev/vault/deployments","created_at":"2016-08-18T08:06:57Z","updated_at":"2016-08-18T08:06:59Z","pushed_at":"2016-09-02T17:26:19Z","git_url":"git://github.com/fintechdev/vault.git","ssh_url":"git@github.com:fintechdev/vault.git","clone_url":"https://github.com/fintechdev/vault.git","svn_url":"https://github.com/fintechdev/vault","homepage":"https://www.vaultproject.io/","size":17646,"stargazers_count":0,"watchers_count":0,"language":"Go","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"mongo"}},"base":{"label":"hashicorp:master","ref":"master","sha":"425a07ce8750170351f5f01504a62623c1934516","user":{"login":"hashicorp","id":761456,"avatar_url":"https://avatars.githubusercontent.com/u/761456?v=3","gravatar_id":"","url":"https://api.github.com/users/hashicorp","html_url":"https://github.com/hashicorp","followers_url":"https://api.github.com/users/hashicorp/followers","following_url":"https://api.github.com/users/hashicorp/following{/other_user}","gists_url":"https://api.github.com/users/hashicorp/gists{/gist_id}","starred_url":"https://api.github.com/users/hashicorp/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hashicorp/subscriptions","organizations_url":"https://api.github.com/users/hashicorp/orgs","repos_url":"https://api.github.com/users/hashicorp/repos","events_url":"https://api.github.com/users/hashicorp/events{/privacy}","received_events_url":"https://api.github.com/users/hashicorp/received_events","type":"Organization","site_admin":false},"repo":{"id":31288958,"name":"vault","full_name":"hashicorp/vault","owner":{"login":"hashicorp","id":761456,"avatar_url":"https://avatars.githubusercontent.com/u/761456?v=3","gravatar_id":"","url":"https://api.github.com/users/hashicorp","html_url":"https://github.com/hashicorp","followers_url":"https://api.github.com/users/hashicorp/followers","following_url":"https://api.github.com/users/hashicorp/following{/other_user}","gists_url":"https://api.github.com/users/hashicorp/gists{/gist_id}","starred_url":"https://api.github.com/users/hashicorp/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hashicorp/subscriptions","organizations_url":"https://api.github.com/users/hashicorp/orgs","repos_url":"https://api.github.com/users/hashicorp/repos","events_url":"https://api.github.com/users/hashicorp/events{/privacy}","received_events_url":"https://api.github.com/users/hashicorp/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/hashicorp/vault","description":"A tool for managing secrets.","fork":false,"url":"https://api.github.com/repos/hashicorp/vault","forks_url":"https://api.github.com/repos/hashicorp/vault/forks","keys_url":"https://api.github.com/repos/hashicorp/vault/keys{/key_id}","collaborators_url":"https://api.github.com/repos/hashicorp/vault/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/hashicorp/vault/teams","hooks_url":"https://api.github.com/repos/hashicorp/vault/hooks","issue_events_url":"https://api.github.com/repos/hashicorp/vault/issues/events{/number}","events_url":"https://api.github.com/repos/hashicorp/vault/events","assignees_url":"https://api.github.com/repos/hashicorp/vault/assignees{/user}","branches_url":"https://api.github.com/repos/hashicorp/vault/branches{/branch}","tags_url":"https://api.github.com/repos/hashicorp/vault/tags","blobs_url":"https://api.github.com/repos/hashicorp/vault/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/hashicorp/vault/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/hashicorp/vault/git/refs{/sha}","trees_url":"https://api.github.com/repos/hashicorp/vault/git/trees{/sha}","statuses_url":"https://api.github.com/repos/hashicorp/vault/statuses/{sha}","languages_url":"https://api.github.com/repos/hashicorp/vault/languages","stargazers_url":"https://api.github.com/repos/hashicorp/vault/stargazers","contributors_url":"https://api.github.com/repos/hashicorp/vault/contributors","subscribers_url":"https://api.github.com/repos/hashicorp/vault/subscribers","subscription_url":"https://api.github.com/repos/hashicorp/vault/subscription","commits_url":"https://api.github.com/repos/hashicorp/vault/commits{/sha}","git_commits_url":"https://api.github.com/repos/hashicorp/vault/git/commits{/sha}","comments_url":"https://api.github.com/repos/hashicorp/vault/comments{/number}","issue_comment_url":"https://api.github.com/repos/hashicorp/vault/issues/comments{/number}","contents_url":"https://api.github.com/repos/hashicorp/vault/contents/{+path}","compare_url":"https://api.github.com/repos/hashicorp/vault/compare/{base}...{head}","merges_url":"https://api.github.com/repos/hashicorp/vault/merges","archive_url":"https://api.github.com/repos/hashicorp/vault/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/hashicorp/vault/downloads","issues_url":"https://api.github.com/repos/hashicorp/vault/issues{/number}","pulls_url":"https://api.github.com/repos/hashicorp/vault/pulls{/number}","milestones_url":"https://api.github.com/repos/hashicorp/vault/milestones{/number}","notifications_url":"https://api.github.com/repos/hashicorp/vault/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/hashicorp/vault/labels{/name}","releases_url":"https://api.github.com/repos/hashicorp/vault/releases{/id}","deployments_url":"https://api.github.com/repos/hashicorp/vault/deployments","created_at":"2015-02-25T00:15:59Z","updated_at":"2016-09-22T14:58:14Z","pushed_at":"2016-09-22T15:46:28Z","git_url":"git://github.com/hashicorp/vault.git","ssh_url":"git@github.com:hashicorp/vault.git","clone_url":"https://github.com/hashicorp/vault.git","svn_url":"https://github.com/hashicorp/vault","homepage":"https://www.vaultproject.io/","size":20073,"stargazers_count":5312,"watchers_count":5312,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":566,"mirror_url":null,"open_issues_count":173,"forks":566,"open_issues":173,"watchers":5312,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/hashicorp/vault/pulls/1847"},"html":{"href":"https://github.com/hashicorp/vault/pull/1847"},"issue":{"href":"https://api.github.com/repos/hashicorp/vault/issues/1847"},"comments":{"href":"https://api.github.com/repos/hashicorp/vault/issues/1847/comments"},"review_comments":{"href":"https://api.github.com/repos/hashicorp/vault/pulls/1847/comments"},"review_comment":{"href":"https://api.github.com/repos/hashicorp/vault/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/hashicorp/vault/pulls/1847/commits"},"statuses":{"href":"https://api.github.com/repos/hashicorp/vault/statuses/fd452d07a14ffa9cc61c23a5435ddb0671131512"}}}}
{"actor":{"display_login":"mheese"}}