github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/apiserver/facades/agent/provisioner/provisioninginfo.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package provisioner
     5  
     6  import (
     7  	"fmt"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/juju/collections/set"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/os/series"
    14  	"gopkg.in/juju/names.v2"
    15  
    16  	"github.com/juju/juju/apiserver/common"
    17  	"github.com/juju/juju/apiserver/common/storagecommon"
    18  	"github.com/juju/juju/apiserver/params"
    19  	"github.com/juju/juju/cloudconfig/instancecfg"
    20  	"github.com/juju/juju/core/lxdprofile"
    21  	"github.com/juju/juju/environs"
    22  	"github.com/juju/juju/environs/imagemetadata"
    23  	"github.com/juju/juju/environs/simplestreams"
    24  	"github.com/juju/juju/environs/tags"
    25  	"github.com/juju/juju/state"
    26  	"github.com/juju/juju/state/cloudimagemetadata"
    27  	"github.com/juju/juju/state/multiwatcher"
    28  	"github.com/juju/juju/storage"
    29  )
    30  
    31  // ProvisioningInfo returns the provisioning information for each given machine entity.
    32  func (p *ProvisionerAPI) ProvisioningInfo(args params.Entities) (params.ProvisioningInfoResults, error) {
    33  	result := params.ProvisioningInfoResults{
    34  		Results: make([]params.ProvisioningInfoResult, len(args.Entities)),
    35  	}
    36  	canAccess, err := p.getAuthFunc()
    37  	if err != nil {
    38  		return result, errors.Trace(err)
    39  	}
    40  	env, err := environs.GetEnviron(p.configGetter, environs.New)
    41  	if err != nil {
    42  		return result, errors.Annotate(err, "could not get environ")
    43  	}
    44  	for i, entity := range args.Entities {
    45  		tag, err := names.ParseMachineTag(entity.Tag)
    46  		if err != nil {
    47  			result.Results[i].Error = common.ServerError(common.ErrPerm)
    48  			continue
    49  		}
    50  		machine, err := p.getMachine(canAccess, tag)
    51  		if err == nil {
    52  			result.Results[i].Result, err = p.getProvisioningInfo(machine, env)
    53  		}
    54  		result.Results[i].Error = common.ServerError(err)
    55  	}
    56  	return result, nil
    57  }
    58  
    59  func (p *ProvisionerAPI) getProvisioningInfo(m *state.Machine, env environs.Environ) (*params.ProvisioningInfo, error) {
    60  	cons, err := m.Constraints()
    61  	if err != nil {
    62  		return nil, errors.Trace(err)
    63  	}
    64  
    65  	volumes, volumeAttachments, err := p.machineVolumeParams(m, env)
    66  	if err != nil {
    67  		return nil, errors.Trace(err)
    68  	}
    69  
    70  	var jobs []multiwatcher.MachineJob
    71  	for _, job := range m.Jobs() {
    72  		jobs = append(jobs, job.ToParams())
    73  	}
    74  
    75  	tags, err := p.machineTags(m, jobs)
    76  	if err != nil {
    77  		return nil, errors.Trace(err)
    78  	}
    79  
    80  	subnetsToZones, err := p.machineSubnetsAndZones(m)
    81  	if err != nil {
    82  		return nil, errors.Annotate(err, "cannot match subnets to zones")
    83  	}
    84  
    85  	pNames, err := p.machineLXDProfileNames(m, env)
    86  	if err != nil {
    87  		return nil, errors.Annotate(err, "cannot write lxd profiles")
    88  	}
    89  
    90  	endpointBindings, err := p.machineEndpointBindings(m)
    91  	if err != nil {
    92  		return nil, errors.Annotate(err, "cannot determine machine endpoint bindings")
    93  	}
    94  
    95  	imageMetadata, err := p.availableImageMetadata(m, env)
    96  	if err != nil {
    97  		return nil, errors.Annotate(err, "cannot get available image metadata")
    98  	}
    99  
   100  	controllerCfg, err := p.st.ControllerConfig()
   101  	if err != nil {
   102  		return nil, errors.Annotate(err, "cannot get controller configuration")
   103  	}
   104  
   105  	return &params.ProvisioningInfo{
   106  		Constraints:       cons,
   107  		Series:            m.Series(),
   108  		Placement:         m.Placement(),
   109  		Jobs:              jobs,
   110  		Volumes:           volumes,
   111  		VolumeAttachments: volumeAttachments,
   112  		Tags:              tags,
   113  		SubnetsToZones:    subnetsToZones,
   114  		EndpointBindings:  endpointBindings,
   115  		ImageMetadata:     imageMetadata,
   116  		ControllerConfig:  controllerCfg,
   117  		CloudInitUserData: env.Config().CloudInitUserData(),
   118  		CharmLXDProfiles:  pNames,
   119  	}, nil
   120  }
   121  
   122  // machineVolumeParams retrieves VolumeParams for the volumes that should be
   123  // provisioned with, and attached to, the machine. The client should ignore
   124  // parameters that it does not know how to handle.
   125  func (p *ProvisionerAPI) machineVolumeParams(
   126  	m *state.Machine,
   127  	env environs.Environ,
   128  ) ([]params.VolumeParams, []params.VolumeAttachmentParams, error) {
   129  	sb, err := state.NewStorageBackend(p.st)
   130  	if err != nil {
   131  		return nil, nil, errors.Trace(err)
   132  	}
   133  	volumeAttachments, err := m.VolumeAttachments()
   134  	if err != nil {
   135  		return nil, nil, errors.Trace(err)
   136  	}
   137  	if len(volumeAttachments) == 0 {
   138  		return nil, nil, nil
   139  	}
   140  	modelConfig, err := p.m.ModelConfig()
   141  	if err != nil {
   142  		return nil, nil, errors.Trace(err)
   143  	}
   144  	controllerCfg, err := p.st.ControllerConfig()
   145  	if err != nil {
   146  		return nil, nil, errors.Trace(err)
   147  	}
   148  	allVolumeParams := make([]params.VolumeParams, 0, len(volumeAttachments))
   149  	var allVolumeAttachmentParams []params.VolumeAttachmentParams
   150  	for _, volumeAttachment := range volumeAttachments {
   151  		volumeTag := volumeAttachment.Volume()
   152  		volume, err := sb.Volume(volumeTag)
   153  		if err != nil {
   154  			return nil, nil, errors.Annotatef(err, "getting volume %q", volumeTag.Id())
   155  		}
   156  		storageInstance, err := storagecommon.MaybeAssignedStorageInstance(
   157  			volume.StorageInstance, sb.StorageInstance,
   158  		)
   159  		if err != nil {
   160  			return nil, nil, errors.Annotatef(err, "getting volume %q storage instance", volumeTag.Id())
   161  		}
   162  		volumeParams, err := storagecommon.VolumeParams(
   163  			volume, storageInstance, modelConfig.UUID(), controllerCfg.ControllerUUID(),
   164  			modelConfig, p.storagePoolManager, p.storageProviderRegistry,
   165  		)
   166  		if err != nil {
   167  			return nil, nil, errors.Annotatef(err, "getting volume %q parameters", volumeTag.Id())
   168  		}
   169  		if _, err := env.StorageProvider(storage.ProviderType(volumeParams.Provider)); errors.IsNotFound(err) {
   170  			// This storage type is not managed by the environ
   171  			// provider, so ignore it. It'll be managed by one
   172  			// of the storage provisioners.
   173  			continue
   174  		} else if err != nil {
   175  			return nil, nil, errors.Annotate(err, "getting storage provider")
   176  		}
   177  
   178  		var volumeProvisioned bool
   179  		volumeInfo, err := volume.Info()
   180  		if err == nil {
   181  			volumeProvisioned = true
   182  		} else if !errors.IsNotProvisioned(err) {
   183  			return nil, nil, errors.Annotate(err, "getting volume info")
   184  		}
   185  		stateVolumeAttachmentParams, volumeDetached := volumeAttachment.Params()
   186  		if !volumeDetached {
   187  			// Volume is already attached to the machine, so
   188  			// there's nothing more to do for it.
   189  			continue
   190  		}
   191  		volumeAttachmentParams := params.VolumeAttachmentParams{
   192  			volumeTag.String(),
   193  			m.Tag().String(),
   194  			volumeInfo.VolumeId,
   195  			"", // we're creating the machine, so it has no instance ID.
   196  			volumeParams.Provider,
   197  			stateVolumeAttachmentParams.ReadOnly,
   198  		}
   199  		if volumeProvisioned {
   200  			// Volume is already provisioned, so we just need to attach it.
   201  			allVolumeAttachmentParams = append(
   202  				allVolumeAttachmentParams, volumeAttachmentParams,
   203  			)
   204  		} else {
   205  			// Not provisioned yet, so ask the cloud provisioner do it.
   206  			volumeParams.Attachment = &volumeAttachmentParams
   207  			allVolumeParams = append(allVolumeParams, volumeParams)
   208  		}
   209  	}
   210  	return allVolumeParams, allVolumeAttachmentParams, nil
   211  }
   212  
   213  // machineTags returns machine-specific tags to set on the instance.
   214  func (p *ProvisionerAPI) machineTags(m *state.Machine, jobs []multiwatcher.MachineJob) (map[string]string, error) {
   215  	// Names of all units deployed to the machine.
   216  	//
   217  	// TODO(axw) 2015-06-02 #1461358
   218  	// We need a worker that periodically updates
   219  	// instance tags with current deployment info.
   220  	units, err := m.Units()
   221  	if err != nil {
   222  		return nil, errors.Trace(err)
   223  	}
   224  	unitNames := make([]string, 0, len(units))
   225  	for _, unit := range units {
   226  		if !unit.IsPrincipal() {
   227  			continue
   228  		}
   229  		unitNames = append(unitNames, unit.Name())
   230  	}
   231  	sort.Strings(unitNames)
   232  
   233  	cfg, err := p.m.ModelConfig()
   234  	if err != nil {
   235  		return nil, errors.Trace(err)
   236  	}
   237  	controllerCfg, err := p.st.ControllerConfig()
   238  	if err != nil {
   239  		return nil, errors.Trace(err)
   240  	}
   241  	machineTags := instancecfg.InstanceTags(cfg.UUID(), controllerCfg.ControllerUUID(), cfg, jobs)
   242  	if len(unitNames) > 0 {
   243  		machineTags[tags.JujuUnitsDeployed] = strings.Join(unitNames, " ")
   244  	}
   245  	machineId := fmt.Sprintf("%s-%s", cfg.Name(), m.Tag().String())
   246  	machineTags[tags.JujuMachine] = machineId
   247  	return machineTags, nil
   248  }
   249  
   250  // machineSubnetsAndZones returns a map of subnet provider-specific id
   251  // to list of availability zone names for that subnet. The result can
   252  // be empty if there are no spaces constraints specified for the
   253  // machine, or there's an error fetching them.
   254  func (p *ProvisionerAPI) machineSubnetsAndZones(m *state.Machine) (map[string][]string, error) {
   255  	mcons, err := m.Constraints()
   256  	if err != nil {
   257  		return nil, errors.Annotate(err, "cannot get machine constraints")
   258  	}
   259  	includeSpaces := mcons.IncludeSpaces()
   260  	if len(includeSpaces) < 1 {
   261  		// Nothing to do.
   262  		return nil, nil
   263  	}
   264  	// TODO(dimitern): For the network model MVP we only use the first
   265  	// included space and ignore the rest.
   266  	//
   267  	// LKK Card: https://canonical.leankit.com/Boards/View/101652562/117352306
   268  	// LP Bug: http://pad.lv/1498232
   269  	spaceName := includeSpaces[0]
   270  	if len(includeSpaces) > 1 {
   271  		logger.Debugf(
   272  			"using space %q from constraints for machine %q (ignoring remaining: %v)",
   273  			spaceName, m.Id(), includeSpaces[1:],
   274  		)
   275  	}
   276  	space, err := p.st.Space(spaceName)
   277  	if err != nil {
   278  		return nil, errors.Trace(err)
   279  	}
   280  	subnets, err := space.Subnets()
   281  	if err != nil {
   282  		return nil, errors.Trace(err)
   283  	}
   284  	if len(subnets) == 0 {
   285  		return nil, errors.Errorf("cannot use space %q as deployment target: no subnets", spaceName)
   286  	}
   287  	subnetsToZones := make(map[string][]string, len(subnets))
   288  	for _, subnet := range subnets {
   289  		warningPrefix := fmt.Sprintf(
   290  			"not using subnet %q in space %q for machine %q provisioning: ",
   291  			subnet.CIDR(), spaceName, m.Id(),
   292  		)
   293  		providerId := subnet.ProviderId()
   294  		if providerId == "" {
   295  			logger.Warningf(warningPrefix + "no ProviderId set")
   296  			continue
   297  		}
   298  		// TODO(dimitern): Once state.Subnet supports multiple zones,
   299  		// use all of them below.
   300  		//
   301  		// LKK Card: https://canonical.leankit.com/Boards/View/101652562/119979611
   302  		zone := subnet.AvailabilityZone()
   303  		if zone == "" {
   304  			logger.Warningf(warningPrefix + "no availability zone(s) set")
   305  			continue
   306  		}
   307  		subnetsToZones[string(providerId)] = []string{zone}
   308  	}
   309  	return subnetsToZones, nil
   310  }
   311  
   312  // machineLXDProfileNames give the environ info to write lxd profiles needed for
   313  // the given machine and returns the names of profiles. Unlike
   314  // containerLXDProfilesInfo which returns the info necessary to write lxd profiles
   315  // via the lxd broker.
   316  func (p *ProvisionerAPI) machineLXDProfileNames(m *state.Machine, env environs.Environ) ([]string, error) {
   317  	profileEnv, ok := env.(environs.LXDProfiler)
   318  	if !ok {
   319  		logger.Tracef("LXDProfiler not implemented by environ")
   320  		return nil, nil
   321  	}
   322  	units, err := m.Units()
   323  	if err != nil {
   324  		return nil, errors.Trace(err)
   325  	}
   326  	var names []string
   327  	for _, unit := range units {
   328  		app, err := unit.Application()
   329  		if err != nil {
   330  			return nil, errors.Trace(err)
   331  		}
   332  		ch, _, err := app.Charm()
   333  		if err != nil {
   334  			return nil, errors.Trace(err)
   335  		}
   336  		profile := ch.LXDProfile()
   337  		if profile == nil || (profile != nil && profile.Empty()) {
   338  			continue
   339  		}
   340  		pName := lxdprofile.Name(p.m.Name(), app.Name(), ch.Revision())
   341  		if err := profileEnv.MaybeWriteLXDProfile(pName, profile); err != nil {
   342  			return nil, errors.Trace(err)
   343  		}
   344  		names = append(names, pName)
   345  	}
   346  	return names, nil
   347  }
   348  
   349  func (p *ProvisionerAPI) machineEndpointBindings(m *state.Machine) (map[string]string, error) {
   350  	units, err := m.Units()
   351  	if err != nil {
   352  		return nil, errors.Trace(err)
   353  	}
   354  
   355  	spacesNamesToProviderIds, err := p.allSpaceNamesToProviderIds()
   356  	if err != nil {
   357  		return nil, errors.Trace(err)
   358  	}
   359  
   360  	var combinedBindings map[string]string
   361  	processedServicesSet := set.NewStrings()
   362  	for _, unit := range units {
   363  		if !unit.IsPrincipal() {
   364  			continue
   365  		}
   366  		service, err := unit.Application()
   367  		if err != nil {
   368  			return nil, errors.Trace(err)
   369  		}
   370  		if processedServicesSet.Contains(service.Name()) {
   371  			// Already processed, skip it.
   372  			continue
   373  		}
   374  		bindings, err := service.EndpointBindings()
   375  		if err != nil {
   376  			return nil, errors.Trace(err)
   377  		}
   378  		processedServicesSet.Add(service.Name())
   379  
   380  		if len(bindings) == 0 {
   381  			continue
   382  		}
   383  		if combinedBindings == nil {
   384  			combinedBindings = make(map[string]string)
   385  		}
   386  
   387  		for endpoint, spaceName := range bindings {
   388  			if spaceName == "" {
   389  				// Skip unspecified bindings, as they won't affect the instance
   390  				// selected for provisioning.
   391  				continue
   392  			}
   393  
   394  			spaceProviderId, nameKnown := spacesNamesToProviderIds[spaceName]
   395  			if nameKnown {
   396  				combinedBindings[endpoint] = spaceProviderId
   397  			} else {
   398  				// Technically, this can't happen in practice, as we're
   399  				// validating the bindings during service deployment.
   400  				return nil, errors.Errorf("unknown space %q with no provider ID specified for endpoint %q", spaceName, endpoint)
   401  			}
   402  		}
   403  	}
   404  	return combinedBindings, nil
   405  }
   406  
   407  func (p *ProvisionerAPI) allSpaceNamesToProviderIds() (map[string]string, error) {
   408  	allSpaces, err := p.st.AllSpaces()
   409  	if err != nil {
   410  		return nil, errors.Annotate(err, "getting all spaces")
   411  	}
   412  
   413  	namesToProviderIds := make(map[string]string, len(allSpaces))
   414  	for _, space := range allSpaces {
   415  		name := space.Name()
   416  
   417  		// For providers without native support for spaces, use the name instead
   418  		// as provider ID.
   419  		providerId := string(space.ProviderId())
   420  		if len(providerId) == 0 {
   421  			providerId = name
   422  		}
   423  
   424  		namesToProviderIds[name] = providerId
   425  	}
   426  
   427  	return namesToProviderIds, nil
   428  }
   429  
   430  // availableImageMetadata returns all image metadata available to this machine
   431  // or an error fetching them.
   432  func (p *ProvisionerAPI) availableImageMetadata(m *state.Machine, env environs.Environ) ([]params.CloudImageMetadata, error) {
   433  	imageConstraint, err := p.constructImageConstraint(m, env)
   434  	if err != nil {
   435  		return nil, errors.Annotate(err, "could not construct image constraint")
   436  	}
   437  
   438  	// Look for image metadata in state.
   439  	data, err := p.findImageMetadata(imageConstraint, env)
   440  	if err != nil {
   441  		return nil, errors.Trace(err)
   442  	}
   443  	sort.Sort(metadataList(data))
   444  	logger.Debugf("available image metadata for provisioning: %v", data)
   445  	return data, nil
   446  }
   447  
   448  // constructImageConstraint returns model-specific criteria used to look for image metadata.
   449  func (p *ProvisionerAPI) constructImageConstraint(m *state.Machine, env environs.Environ) (*imagemetadata.ImageConstraint, error) {
   450  	lookup := simplestreams.LookupParams{
   451  		Series: []string{m.Series()},
   452  		Stream: env.Config().ImageStream(),
   453  	}
   454  
   455  	mcons, err := m.Constraints()
   456  	if err != nil {
   457  		return nil, errors.Annotatef(err, "cannot get machine constraints for machine %v", m.MachineTag().Id())
   458  	}
   459  
   460  	if mcons.Arch != nil {
   461  		lookup.Arches = []string{*mcons.Arch}
   462  	}
   463  
   464  	if hasRegion, ok := env.(simplestreams.HasRegion); ok {
   465  		// We can determine current region; we want only
   466  		// metadata specific to this region.
   467  		spec, err := hasRegion.Region()
   468  		if err != nil {
   469  			// can't really find images if we cannot determine cloud region
   470  			// TODO (anastasiamac 2015-12-03) or can we?
   471  			return nil, errors.Annotate(err, "getting provider region information (cloud spec)")
   472  		}
   473  		lookup.CloudSpec = spec
   474  	}
   475  
   476  	return imagemetadata.NewImageConstraint(lookup), nil
   477  }
   478  
   479  // findImageMetadata returns all image metadata or an error fetching them.
   480  // It looks for image metadata in state.
   481  // If none are found, we fall back on original image search in simple streams.
   482  func (p *ProvisionerAPI) findImageMetadata(imageConstraint *imagemetadata.ImageConstraint, env environs.Environ) ([]params.CloudImageMetadata, error) {
   483  	// Look for image metadata in state.
   484  	stateMetadata, err := p.imageMetadataFromState(imageConstraint)
   485  	if err != nil && !errors.IsNotFound(err) {
   486  		// look into simple stream if for some reason can't get from controller,
   487  		// so do not exit on error.
   488  		logger.Infof("could not get image metadata from controller: %v", err)
   489  	}
   490  	logger.Debugf("got from controller %d metadata", len(stateMetadata))
   491  	// No need to look in data sources if found in state.
   492  	if len(stateMetadata) != 0 {
   493  		return stateMetadata, nil
   494  	}
   495  
   496  	// If no metadata is found in state, fall back to original simple stream search.
   497  	// Currently, an image metadata worker picks up this metadata periodically (daily),
   498  	// and stores it in state. So potentially, this collection could be different
   499  	// to what is in state.
   500  	dsMetadata, err := p.imageMetadataFromDataSources(env, imageConstraint)
   501  	if err != nil {
   502  		if !errors.IsNotFound(err) {
   503  			return nil, errors.Trace(err)
   504  		}
   505  	}
   506  	logger.Debugf("got from data sources %d metadata", len(dsMetadata))
   507  
   508  	return dsMetadata, nil
   509  }
   510  
   511  // imageMetadataFromState returns image metadata stored in state
   512  // that matches given criteria.
   513  func (p *ProvisionerAPI) imageMetadataFromState(constraint *imagemetadata.ImageConstraint) ([]params.CloudImageMetadata, error) {
   514  	filter := cloudimagemetadata.MetadataFilter{
   515  		Series: constraint.Series,
   516  		Arches: constraint.Arches,
   517  		Region: constraint.Region,
   518  		Stream: constraint.Stream,
   519  	}
   520  	stored, err := p.st.CloudImageMetadataStorage.FindMetadata(filter)
   521  	if err != nil {
   522  		return nil, errors.Trace(err)
   523  	}
   524  
   525  	toParams := func(m cloudimagemetadata.Metadata) params.CloudImageMetadata {
   526  		return params.CloudImageMetadata{
   527  			ImageId:         m.ImageId,
   528  			Stream:          m.Stream,
   529  			Region:          m.Region,
   530  			Version:         m.Version,
   531  			Series:          m.Series,
   532  			Arch:            m.Arch,
   533  			VirtType:        m.VirtType,
   534  			RootStorageType: m.RootStorageType,
   535  			RootStorageSize: m.RootStorageSize,
   536  			Source:          m.Source,
   537  			Priority:        m.Priority,
   538  		}
   539  	}
   540  
   541  	var all []params.CloudImageMetadata
   542  	for _, ms := range stored {
   543  		for _, m := range ms {
   544  			all = append(all, toParams(m))
   545  		}
   546  	}
   547  	return all, nil
   548  }
   549  
   550  // imageMetadataFromDataSources finds image metadata that match specified criteria in existing data sources.
   551  func (p *ProvisionerAPI) imageMetadataFromDataSources(env environs.Environ, constraint *imagemetadata.ImageConstraint) ([]params.CloudImageMetadata, error) {
   552  	sources, err := environs.ImageMetadataSources(env)
   553  	if err != nil {
   554  		return nil, errors.Trace(err)
   555  	}
   556  
   557  	cfg := env.Config()
   558  	toModel := func(m *imagemetadata.ImageMetadata, mSeries string, source string, priority int) cloudimagemetadata.Metadata {
   559  		result := cloudimagemetadata.Metadata{
   560  			MetadataAttributes: cloudimagemetadata.MetadataAttributes{
   561  				Region:          m.RegionName,
   562  				Arch:            m.Arch,
   563  				VirtType:        m.VirtType,
   564  				RootStorageType: m.Storage,
   565  				Source:          source,
   566  				Series:          mSeries,
   567  				Stream:          m.Stream,
   568  				Version:         m.Version,
   569  			},
   570  			Priority: priority,
   571  			ImageId:  m.Id,
   572  		}
   573  		// TODO (anastasiamac 2016-08-24) This is a band-aid solution.
   574  		// Once correct value is read from simplestreams, this needs to go.
   575  		// Bug# 1616295
   576  		if result.Stream == "" {
   577  			result.Stream = constraint.Stream
   578  		}
   579  		if result.Stream == "" {
   580  			result.Stream = cfg.ImageStream()
   581  		}
   582  		return result
   583  	}
   584  
   585  	var metadataState []cloudimagemetadata.Metadata
   586  	for _, source := range sources {
   587  		logger.Debugf("looking in data source %v", source.Description())
   588  		found, info, err := imagemetadata.Fetch([]simplestreams.DataSource{source}, constraint)
   589  		if err != nil {
   590  			// Do not stop looking in other data sources if there is an issue here.
   591  			logger.Warningf("encountered %v while getting published images metadata from %v", err, source.Description())
   592  			continue
   593  		}
   594  		for _, m := range found {
   595  			mSeries, err := series.VersionSeries(m.Version)
   596  			if err != nil {
   597  				logger.Warningf("could not determine series for image id %s: %v", m.Id, err)
   598  				continue
   599  			}
   600  			metadataState = append(metadataState, toModel(m, mSeries, info.Source, source.Priority()))
   601  		}
   602  	}
   603  	if len(metadataState) > 0 {
   604  		if err := p.st.CloudImageMetadataStorage.SaveMetadata(metadataState); err != nil {
   605  			// No need to react here, just take note
   606  			logger.Warningf("failed to save published image metadata: %v", err)
   607  		}
   608  	}
   609  
   610  	// Since we've fallen through to data sources search and have saved all needed images into controller,
   611  	// let's try to get them from controller to avoid duplication of conversion logic here.
   612  	all, err := p.imageMetadataFromState(constraint)
   613  	if err != nil {
   614  		return nil, errors.Annotate(err, "could not read metadata from controller after saving it there from data sources")
   615  	}
   616  
   617  	if len(all) == 0 {
   618  		return nil, errors.NotFoundf("image metadata for series %v, arch %v", constraint.Series, constraint.Arches)
   619  	}
   620  
   621  	return all, nil
   622  }
   623  
   624  // metadataList is a convenience type enabling to sort
   625  // a collection of CloudImageMetadata in order of priority.
   626  type metadataList []params.CloudImageMetadata
   627  
   628  // Implements sort.Interface
   629  func (m metadataList) Len() int {
   630  	return len(m)
   631  }
   632  
   633  // Implements sort.Interface and sorts image metadata by priority.
   634  func (m metadataList) Less(i, j int) bool {
   635  	return m[i].Priority < m[j].Priority
   636  }
   637  
   638  // Implements sort.Interface
   639  func (m metadataList) Swap(i, j int) {
   640  	m[i], m[j] = m[j], m[i]
   641  }