1
// Copyright 2016 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
10
"github.com/juju/errors"
11
"github.com/juju/loggo"
12
"github.com/juju/names"
13
"github.com/juju/utils/set"
14
"gopkg.in/mgo.v2/bson"
16
"github.com/juju/juju/core/description"
19
// Export the current model for the State.
20
func (st *State) Export() (description.Model, error) {
21
dbModel, err := st.Model()
23
return nil, errors.Trace(err)
29
logger: loggo.GetLogger("juju.state.export-model"),
31
if err := export.readAllStatuses(); err != nil {
32
return nil, errors.Annotate(err, "reading statuses")
34
if err := export.readAllStatusHistory(); err != nil {
35
return nil, errors.Trace(err)
37
if err := export.readAllSettings(); err != nil {
38
return nil, errors.Trace(err)
40
if err := export.readAllAnnotations(); err != nil {
41
return nil, errors.Trace(err)
43
if err := export.readAllConstraints(); err != nil {
44
return nil, errors.Trace(err)
47
envConfig, found := export.settings[modelGlobalKey]
49
return nil, errors.New("missing environ config")
52
blocks, err := export.readBlocks()
54
return nil, errors.Trace(err)
57
args := description.ModelArgs{
58
Owner: dbModel.Owner(),
59
Config: envConfig.Settings,
60
LatestToolsVersion: dbModel.LatestToolsVersion(),
63
export.model = description.NewModel(args)
64
modelKey := dbModel.globalKey()
65
export.model.SetAnnotations(export.getAnnotations(modelKey))
66
if err := export.sequences(); err != nil {
67
return nil, errors.Trace(err)
69
constraintsArgs, err := export.constraintsArgs(modelKey)
71
return nil, errors.Trace(err)
73
export.model.SetConstraints(constraintsArgs)
75
if err := export.modelUsers(); err != nil {
76
return nil, errors.Trace(err)
78
if err := export.machines(); err != nil {
79
return nil, errors.Trace(err)
81
if err := export.services(); err != nil {
82
return nil, errors.Trace(err)
84
if err := export.relations(); err != nil {
85
return nil, errors.Trace(err)
88
if err := export.model.Validate(); err != nil {
89
return nil, errors.Trace(err)
94
return export.model, nil
97
type exporter struct {
100
model description.Model
103
annotations map[string]annotatorDoc
104
constraints map[string]bson.M
105
settings map[string]settingsDoc
106
status map[string]bson.M
107
statusHistory map[string][]historicalStatusDoc
108
// Map of service name to units. Populated as part
109
// of the services export.
110
units map[string][]*Unit
113
func (e *exporter) sequences() error {
114
sequences, closer := e.st.getCollection(sequenceC)
117
var docs []sequenceDoc
118
if err := sequences.Find(nil).All(&docs); err != nil {
119
return errors.Trace(err)
122
for _, doc := range docs {
123
e.model.SetSequence(doc.Name, doc.Counter)
128
func (e *exporter) readBlocks() (map[string]string, error) {
129
blocks, closer := e.st.getCollection(blocksC)
133
if err := blocks.Find(nil).All(&docs); err != nil {
134
return nil, errors.Trace(err)
137
result := make(map[string]string)
138
for _, doc := range docs {
139
// We don't care about the id, uuid, or tag.
140
// The uuid and tag both refer to the model uuid, and the
141
// id is opaque - even though it is sequence generated.
142
result[doc.Type.MigrationValue()] = doc.Message
147
func (e *exporter) modelUsers() error {
148
users, err := e.dbModel.Users()
150
return errors.Trace(err)
152
lastConnections, err := e.readLastConnectionTimes()
154
return errors.Trace(err)
157
for _, user := range users {
158
lastConn := lastConnections[strings.ToLower(user.UserName())]
159
arg := description.UserArgs{
160
Name: user.UserTag(),
161
DisplayName: user.DisplayName(),
162
CreatedBy: names.NewUserTag(user.CreatedBy()),
163
DateCreated: user.DateCreated(),
164
LastConnection: lastConn,
165
ReadOnly: user.ReadOnly(),
172
func (e *exporter) machines() error {
173
machines, err := e.st.AllMachines()
175
return errors.Trace(err)
177
e.logger.Debugf("found %d machines", len(machines))
179
instanceDataCollection, closer := e.st.getCollection(instanceDataC)
182
var instData []instanceData
183
instances := make(map[string]instanceData)
184
if err := instanceDataCollection.Find(nil).All(&instData); err != nil {
185
return errors.Annotate(err, "instance data")
187
e.logger.Debugf("found %d instanceData", len(instData))
188
for _, data := range instData {
189
instances[data.MachineId] = data
192
// Read all the open ports documents.
193
openedPorts, closer := e.st.getCollection(openedPortsC)
195
var portsData []portsDoc
196
if err := openedPorts.Find(nil).All(&portsData); err != nil {
197
return errors.Annotate(err, "opened ports")
199
e.logger.Debugf("found %d openedPorts docs", len(portsData))
201
// We are iterating through a flat list of machines, but the migration
202
// model stores the nesting. The AllMachines method assures us that the
203
// machines are returned in an order so the parent will always before
205
machineMap := make(map[string]description.Machine)
207
for _, machine := range machines {
208
e.logger.Debugf("export machine %s", machine.Id())
210
var exParent description.Machine
211
if parentId := ParentId(machine.Id()); parentId != "" {
213
exParent, found = machineMap[parentId]
215
return errors.Errorf("machine %s missing parent", machine.Id())
219
exMachine, err := e.newMachine(exParent, machine, instances, portsData)
221
return errors.Trace(err)
223
machineMap[machine.Id()] = exMachine
229
func (e *exporter) newMachine(exParent description.Machine, machine *Machine, instances map[string]instanceData, portsData []portsDoc) (description.Machine, error) {
230
args := description.MachineArgs{
231
Id: machine.MachineTag(),
232
Nonce: machine.doc.Nonce,
233
PasswordHash: machine.doc.PasswordHash,
234
Placement: machine.doc.Placement,
235
Series: machine.doc.Series,
236
ContainerType: machine.doc.ContainerType,
239
if supported, ok := machine.SupportedContainers(); ok {
240
containers := make([]string, len(supported))
241
for i, containerType := range supported {
242
containers[i] = string(containerType)
244
args.SupportedContainers = &containers
247
for _, job := range machine.Jobs() {
248
args.Jobs = append(args.Jobs, job.MigrationValue())
251
// A null value means that we don't yet know which containers
252
// are supported. An empty slice means 'no containers are supported'.
253
var exMachine description.Machine
255
exMachine = e.model.AddMachine(args)
257
exMachine = exParent.AddContainer(args)
259
exMachine.SetAddresses(
260
e.newAddressArgsSlice(machine.doc.MachineAddresses),
261
e.newAddressArgsSlice(machine.doc.Addresses))
262
exMachine.SetPreferredAddresses(
263
e.newAddressArgs(machine.doc.PreferredPublicAddress),
264
e.newAddressArgs(machine.doc.PreferredPrivateAddress))
266
// We fully expect the machine to have tools set, and that there is
267
// some instance data.
268
instData, found := instances[machine.doc.Id]
270
return nil, errors.NotValidf("missing instance data for machine %s", machine.Id())
272
exMachine.SetInstance(e.newCloudInstanceArgs(instData))
274
// Find the current machine status.
275
globalKey := machine.globalKey()
276
statusArgs, err := e.statusArgs(globalKey)
278
return nil, errors.Annotatef(err, "status for machine %s", machine.Id())
280
exMachine.SetStatus(statusArgs)
281
exMachine.SetStatusHistory(e.statusHistoryArgs(globalKey))
283
tools, err := machine.AgentTools()
285
// This means the tools aren't set, but they should be.
286
return nil, errors.Trace(err)
289
exMachine.SetTools(description.AgentToolsArgs{
290
Version: tools.Version,
292
SHA256: tools.SHA256,
296
for _, args := range e.networkPortsArgsForMachine(machine.Id(), portsData) {
297
exMachine.AddNetworkPorts(args)
300
exMachine.SetAnnotations(e.getAnnotations(globalKey))
302
constraintsArgs, err := e.constraintsArgs(globalKey)
304
return nil, errors.Trace(err)
306
exMachine.SetConstraints(constraintsArgs)
308
return exMachine, nil
311
func (e *exporter) networkPortsArgsForMachine(machineId string, portsData []portsDoc) []description.NetworkPortsArgs {
312
var result []description.NetworkPortsArgs
313
for _, doc := range portsData {
314
// Don't bother including a network if there are no ports open on it.
315
if doc.MachineID == machineId && len(doc.Ports) > 0 {
316
args := description.NetworkPortsArgs{NetworkName: doc.NetworkName}
317
for _, p := range doc.Ports {
318
args.OpenPorts = append(args.OpenPorts, description.PortRangeArgs{
319
UnitName: p.UnitName,
320
FromPort: p.FromPort,
322
Protocol: p.Protocol,
325
result = append(result, args)
331
func (e *exporter) newAddressArgsSlice(a []address) []description.AddressArgs {
332
result := []description.AddressArgs{}
333
for _, addr := range a {
334
result = append(result, e.newAddressArgs(addr))
339
func (e *exporter) newAddressArgs(a address) description.AddressArgs {
340
return description.AddressArgs{
343
NetworkName: a.NetworkName,
349
func (e *exporter) newCloudInstanceArgs(data instanceData) description.CloudInstanceArgs {
350
inst := description.CloudInstanceArgs{
351
InstanceId: string(data.InstanceId),
354
if data.Arch != nil {
355
inst.Architecture = *data.Arch
358
inst.Memory = *data.Mem
360
if data.RootDisk != nil {
361
inst.RootDisk = *data.RootDisk
363
if data.CpuCores != nil {
364
inst.CpuCores = *data.CpuCores
366
if data.CpuPower != nil {
367
inst.CpuPower = *data.CpuPower
369
if data.Tags != nil {
370
inst.Tags = *data.Tags
372
if data.AvailZone != nil {
373
inst.AvailabilityZone = *data.AvailZone
378
func (e *exporter) services() error {
379
services, err := e.st.AllServices()
381
return errors.Trace(err)
383
e.logger.Debugf("found %d services", len(services))
385
refcounts, err := e.readAllSettingsRefCounts()
387
return errors.Trace(err)
390
e.units, err = e.readAllUnits()
392
return errors.Trace(err)
395
meterStatus, err := e.readAllMeterStatus()
397
return errors.Trace(err)
400
leaders := e.readServiceLeaders()
402
for _, service := range services {
403
serviceUnits := e.units[service.Name()]
404
leader := leaders[service.Name()]
405
if err := e.addService(service, refcounts, serviceUnits, meterStatus, leader); err != nil {
406
return errors.Trace(err)
412
func (e *exporter) readServiceLeaders() map[string]string {
413
result := make(map[string]string)
414
for key, value := range e.st.leadershipClient.Leases() {
415
result[key] = value.Holder
420
func (e *exporter) addService(service *Service, refcounts map[string]int, units []*Unit, meterStatus map[string]*meterStatusDoc, leader string) error {
421
settingsKey := service.settingsKey()
422
leadershipKey := leadershipSettingsKey(service.Name())
424
serviceSettingsDoc, found := e.settings[settingsKey]
426
return errors.Errorf("missing settings for service %q", service.Name())
428
refCount, found := refcounts[settingsKey]
430
return errors.Errorf("missing settings refcount for service %q", service.Name())
432
leadershipSettingsDoc, found := e.settings[leadershipKey]
434
return errors.Errorf("missing leadership settings for service %q", service.Name())
437
args := description.ServiceArgs{
438
Tag: service.ServiceTag(),
439
Series: service.doc.Series,
440
Subordinate: service.doc.Subordinate,
441
CharmURL: service.doc.CharmURL.String(),
442
CharmModifiedVersion: service.doc.CharmModifiedVersion,
443
ForceCharm: service.doc.ForceCharm,
444
Exposed: service.doc.Exposed,
445
MinUnits: service.doc.MinUnits,
446
Settings: serviceSettingsDoc.Settings,
447
SettingsRefCount: refCount,
449
LeadershipSettings: leadershipSettingsDoc.Settings,
450
MetricsCredentials: service.doc.MetricCredentials,
452
exService := e.model.AddService(args)
453
// Find the current service status.
454
globalKey := service.globalKey()
455
statusArgs, err := e.statusArgs(globalKey)
457
return errors.Annotatef(err, "status for service %s", service.Name())
459
exService.SetStatus(statusArgs)
460
exService.SetStatusHistory(e.statusHistoryArgs(globalKey))
461
exService.SetAnnotations(e.getAnnotations(globalKey))
463
constraintsArgs, err := e.constraintsArgs(globalKey)
465
return errors.Trace(err)
467
exService.SetConstraints(constraintsArgs)
469
for _, unit := range units {
470
agentKey := unit.globalAgentKey()
471
unitMeterStatus, found := meterStatus[agentKey]
473
return errors.Errorf("missing meter status for unit %s", unit.Name())
476
args := description.UnitArgs{
478
Machine: names.NewMachineTag(unit.doc.MachineId),
479
PasswordHash: unit.doc.PasswordHash,
480
MeterStatusCode: unitMeterStatus.Code,
481
MeterStatusInfo: unitMeterStatus.Info,
483
if principalName, isSubordinate := unit.PrincipalName(); isSubordinate {
484
args.Principal = names.NewUnitTag(principalName)
486
if subs := unit.SubordinateNames(); len(subs) > 0 {
487
for _, subName := range subs {
488
args.Subordinates = append(args.Subordinates, names.NewUnitTag(subName))
491
exUnit := exService.AddUnit(args)
492
// workload uses globalKey, agent uses globalAgentKey.
493
globalKey := unit.globalKey()
494
statusArgs, err := e.statusArgs(globalKey)
496
return errors.Annotatef(err, "workload status for unit %s", unit.Name())
498
exUnit.SetWorkloadStatus(statusArgs)
499
exUnit.SetWorkloadStatusHistory(e.statusHistoryArgs(globalKey))
500
statusArgs, err = e.statusArgs(agentKey)
502
return errors.Annotatef(err, "agent status for unit %s", unit.Name())
504
exUnit.SetAgentStatus(statusArgs)
505
exUnit.SetAgentStatusHistory(e.statusHistoryArgs(agentKey))
507
tools, err := unit.AgentTools()
509
// This means the tools aren't set, but they should be.
510
return errors.Trace(err)
512
exUnit.SetTools(description.AgentToolsArgs{
513
Version: tools.Version,
515
SHA256: tools.SHA256,
518
exUnit.SetAnnotations(e.getAnnotations(globalKey))
520
constraintsArgs, err := e.constraintsArgs(agentKey)
522
return errors.Trace(err)
524
exUnit.SetConstraints(constraintsArgs)
530
func (e *exporter) relations() error {
531
rels, err := e.st.AllRelations()
533
return errors.Trace(err)
535
e.logger.Debugf("read %d relations", len(rels))
537
relationScopes, err := e.readAllRelationScopes()
539
return errors.Trace(err)
542
for _, relation := range rels {
543
exRelation := e.model.AddRelation(description.RelationArgs{
545
Key: relation.String(),
547
for _, ep := range relation.Endpoints() {
548
exEndPoint := exRelation.AddEndpoint(description.EndpointArgs{
549
ServiceName: ep.ServiceName,
551
Role: string(ep.Role),
552
Interface: ep.Interface,
553
Optional: ep.Optional,
555
Scope: string(ep.Scope),
557
// We expect a relationScope and settings for each of the
558
// units of the specified service.
559
units := e.units[ep.ServiceName]
560
for _, unit := range units {
561
ru, err := relation.Unit(unit)
563
return errors.Trace(err)
566
if !relationScopes.Contains(key) {
567
return errors.Errorf("missing relation scope for %s and %s", relation, unit.Name())
569
settingsDoc, found := e.settings[key]
571
return errors.Errorf("missing relation settings for %s and %s", relation, unit.Name())
573
exEndPoint.SetUnitSettings(unit.Name(), settingsDoc.Settings)
580
func (e *exporter) readAllRelationScopes() (set.Strings, error) {
581
relationScopes, closer := e.st.getCollection(relationScopesC)
584
docs := []relationScopeDoc{}
585
err := relationScopes.Find(nil).All(&docs)
587
return nil, errors.Annotate(err, "cannot get all relation scopes")
589
e.logger.Debugf("found %d relationScope docs", len(docs))
591
result := set.NewStrings()
592
for _, doc := range docs {
598
func (e *exporter) readAllUnits() (map[string][]*Unit, error) {
599
unitsCollection, closer := e.st.getCollection(unitsC)
603
err := unitsCollection.Find(nil).All(&docs)
605
return nil, errors.Annotate(err, "cannot get all units")
607
e.logger.Debugf("found %d unit docs", len(docs))
608
result := make(map[string][]*Unit)
609
for _, doc := range docs {
610
units := result[doc.Service]
611
result[doc.Service] = append(units, newUnit(e.st, &doc))
616
func (e *exporter) readAllMeterStatus() (map[string]*meterStatusDoc, error) {
617
meterStatuses, closer := e.st.getCollection(meterStatusC)
620
docs := []meterStatusDoc{}
621
err := meterStatuses.Find(nil).All(&docs)
623
return nil, errors.Annotate(err, "cannot get all meter status docs")
625
e.logger.Debugf("found %d meter status docs", len(docs))
626
result := make(map[string]*meterStatusDoc)
627
for _, doc := range docs {
628
result[e.st.localID(doc.DocID)] = &doc
633
func (e *exporter) readLastConnectionTimes() (map[string]time.Time, error) {
634
lastConnections, closer := e.st.getCollection(modelUserLastConnectionC)
637
var docs []modelUserLastConnectionDoc
638
if err := lastConnections.Find(nil).All(&docs); err != nil {
639
return nil, errors.Trace(err)
642
result := make(map[string]time.Time)
643
for _, doc := range docs {
644
result[doc.UserName] = doc.LastConnection.UTC()
649
func (e *exporter) readAllAnnotations() error {
650
annotations, closer := e.st.getCollection(annotationsC)
653
var docs []annotatorDoc
654
if err := annotations.Find(nil).All(&docs); err != nil {
655
return errors.Trace(err)
657
e.logger.Debugf("read %d annotations docs", len(docs))
659
e.annotations = make(map[string]annotatorDoc)
660
for _, doc := range docs {
661
e.annotations[doc.GlobalKey] = doc
666
func (e *exporter) readAllConstraints() error {
667
constraintsCollection, closer := e.st.getCollection(constraintsC)
670
// Since the constraintsDoc doesn't include any global key or _id
671
// fields, we can't just deserialize the entire collection into a slice
672
// of docs, so we get them all out with bson maps.
674
err := constraintsCollection.Find(nil).All(&docs)
676
return errors.Annotate(err, "failed to read constraints collection")
679
e.logger.Debugf("read %d constraints docs", len(docs))
680
e.constraints = make(map[string]bson.M)
681
for _, doc := range docs {
682
docId, ok := doc["_id"].(string)
684
return errors.Errorf("expected string, got %s (%T)", doc["_id"], doc["_id"])
686
id := e.st.localID(docId)
687
e.constraints[id] = doc
688
e.logger.Debugf("doc[%q] = %#v", id, doc)
693
// getAnnotations doesn't really care if there are any there or not
694
// for the key, but if they were there, they are removed so we can
695
// check at the end of the export for anything we have forgotten.
696
func (e *exporter) getAnnotations(key string) map[string]string {
697
result, found := e.annotations[key]
699
delete(e.annotations, key)
701
return result.Annotations
704
func (e *exporter) readAllSettings() error {
705
settings, closer := e.st.getCollection(settingsC)
708
var docs []settingsDoc
709
if err := settings.Find(nil).All(&docs); err != nil {
710
return errors.Trace(err)
713
e.settings = make(map[string]settingsDoc)
714
for _, doc := range docs {
715
key := e.st.localID(doc.DocID)
716
e.settings[key] = doc
721
func (e *exporter) readAllStatuses() error {
722
statuses, closer := e.st.getCollection(statusesC)
726
err := statuses.Find(nil).All(&docs)
728
return errors.Annotate(err, "failed to read status collection")
731
e.logger.Debugf("read %d status documents", len(docs))
732
e.status = make(map[string]bson.M)
733
for _, doc := range docs {
734
docId, ok := doc["_id"].(string)
736
return errors.Errorf("expected string, got %s (%T)", doc["_id"], doc["_id"])
738
id := e.st.localID(docId)
745
func (e *exporter) readAllStatusHistory() error {
746
statuses, closer := e.st.getCollection(statusesHistoryC)
750
e.statusHistory = make(map[string][]historicalStatusDoc)
751
var doc historicalStatusDoc
752
iter := statuses.Find(nil).Sort("-updated").Iter()
754
for iter.Next(&doc) {
755
history := e.statusHistory[doc.GlobalKey]
756
e.statusHistory[doc.GlobalKey] = append(history, doc)
760
if err := iter.Err(); err != nil {
761
return errors.Annotate(err, "failed to read status history collection")
764
e.logger.Debugf("read %d status history documents", count)
769
func (e *exporter) statusArgs(globalKey string) (description.StatusArgs, error) {
770
result := description.StatusArgs{}
771
statusDoc, found := e.status[globalKey]
773
return result, errors.NotFoundf("status data for %s", globalKey)
776
status, ok := statusDoc["status"].(string)
778
return result, errors.Errorf("expected string for status, got %T", statusDoc["status"])
780
info, ok := statusDoc["statusinfo"].(string)
782
return result, errors.Errorf("expected string for statusinfo, got %T", statusDoc["statusinfo"])
784
// data is an embedded map and comes out as a bson.M
785
// A bson.M is map[string]interface{}, so we can type cast it.
786
data, ok := statusDoc["statusdata"].(bson.M)
788
return result, errors.Errorf("expected map for data, got %T", statusDoc["statusdata"])
790
dataMap := map[string]interface{}(data)
791
updated, ok := statusDoc["updated"].(int64)
793
return result, errors.Errorf("expected int64 for updated, got %T", statusDoc["updated"])
796
result.Value = status
797
result.Message = info
798
result.Data = dataMap
799
result.Updated = time.Unix(0, updated)
803
func (e *exporter) statusHistoryArgs(globalKey string) []description.StatusArgs {
804
history := e.statusHistory[globalKey]
805
result := make([]description.StatusArgs, len(history))
806
e.logger.Debugf("found %d status history docs for %s", len(history), globalKey)
807
for i, doc := range history {
808
result[i] = description.StatusArgs{
809
Value: string(doc.Status),
810
Message: doc.StatusInfo,
811
Data: doc.StatusData,
812
Updated: time.Unix(0, doc.Updated),
819
func (e *exporter) constraintsArgs(globalKey string) (description.ConstraintsArgs, error) {
820
doc, found := e.constraints[globalKey]
822
// No constraints for this key.
823
e.logger.Debugf("no constraints found for key %q", globalKey)
824
return description.ConstraintsArgs{}, nil
826
// We capture any type error using a closure to avoid having to return
827
// multiple values from the optional functions. This does mean that we will
828
// only report on the last one, but that is fine as there shouldn't be any.
829
var optionalErr error
830
optionalString := func(name string) string {
831
switch value := doc[name].(type) {
836
optionalErr = errors.Errorf("expected uint64 for %s, got %T", name, value)
840
optionalInt := func(name string) uint64 {
841
switch value := doc[name].(type) {
848
optionalErr = errors.Errorf("expected uint64 for %s, got %T", name, value)
852
optionalStringSlice := func(name string) []string {
853
switch value := doc[name].(type) {
858
optionalErr = errors.Errorf("expected []string] for %s, got %T", name, value)
862
result := description.ConstraintsArgs{
863
Architecture: optionalString("arch"),
864
Container: optionalString("container"),
865
CpuCores: optionalInt("cpucores"),
866
CpuPower: optionalInt("cpupower"),
867
InstanceType: optionalString("instancetype"),
868
Memory: optionalInt("mem"),
869
RootDisk: optionalInt("rootdisk"),
870
Spaces: optionalStringSlice("spaces"),
871
Tags: optionalStringSlice("tags"),
873
if optionalErr != nil {
874
return description.ConstraintsArgs{}, errors.Trace(optionalErr)
879
func (e *exporter) readAllSettingsRefCounts() (map[string]int, error) {
880
refCounts, closer := e.st.getCollection(settingsrefsC)
884
err := refCounts.Find(nil).All(&docs)
886
return nil, errors.Annotate(err, "failed to read settings refcount collection")
889
e.logger.Debugf("read %d settings refcount documents", len(docs))
890
result := make(map[string]int)
891
for _, doc := range docs {
892
docId, ok := doc["_id"].(string)
894
return nil, errors.Errorf("expected string, got %s (%T)", doc["_id"], doc["_id"])
896
id := e.st.localID(docId)
897
count, ok := doc["refcount"].(int)
899
return nil, errors.Errorf("expected int, got %s (%T)", doc["refcount"], doc["refcount"])
907
func (e *exporter) logExtras() {
908
// As annotations are saved into the model, they are removed from the
909
// exporter's map. If there are any left at the end, we are missing
910
// things. Not an error just now, just a warning that we have missed
911
// something. Could potentially be an error at a later date when
912
// migrations are complete (but probably not).
913
for key, doc := range e.annotations {
914
e.logger.Warningf("unexported annotation for %s, %s", doc.Tag, key)