github.com/openshift/installer@v1.4.17/pkg/asset/machines/clusterapi.go (about)

     1  package machines
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net"
     7  	"path/filepath"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/pkg/errors"
    12  	"github.com/sirupsen/logrus"
    13  	"github.com/vmware/govmomi/vim25/soap"
    14  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    15  	"k8s.io/utils/ptr"
    16  	"sigs.k8s.io/cluster-api-provider-aws/v2/api/v1beta2"
    17  	"sigs.k8s.io/controller-runtime/pkg/client"
    18  	"sigs.k8s.io/yaml"
    19  
    20  	configv1 "github.com/openshift/api/config/v1"
    21  	"github.com/openshift/installer/pkg/asset"
    22  	"github.com/openshift/installer/pkg/asset/installconfig"
    23  	icazure "github.com/openshift/installer/pkg/asset/installconfig/azure"
    24  	"github.com/openshift/installer/pkg/asset/machines/aws"
    25  	"github.com/openshift/installer/pkg/asset/machines/azure"
    26  	"github.com/openshift/installer/pkg/asset/machines/gcp"
    27  	nutanixcapi "github.com/openshift/installer/pkg/asset/machines/nutanix"
    28  	"github.com/openshift/installer/pkg/asset/machines/openstack"
    29  	"github.com/openshift/installer/pkg/asset/machines/powervs"
    30  	vspherecapi "github.com/openshift/installer/pkg/asset/machines/vsphere"
    31  	"github.com/openshift/installer/pkg/asset/manifests/capiutils"
    32  	"github.com/openshift/installer/pkg/asset/rhcos"
    33  	"github.com/openshift/installer/pkg/clusterapi"
    34  	rhcosutils "github.com/openshift/installer/pkg/rhcos"
    35  	"github.com/openshift/installer/pkg/types"
    36  	awstypes "github.com/openshift/installer/pkg/types/aws"
    37  	awsdefaults "github.com/openshift/installer/pkg/types/aws/defaults"
    38  	azuretypes "github.com/openshift/installer/pkg/types/azure"
    39  	azuredefaults "github.com/openshift/installer/pkg/types/azure/defaults"
    40  	gcptypes "github.com/openshift/installer/pkg/types/gcp"
    41  	nutanixtypes "github.com/openshift/installer/pkg/types/nutanix"
    42  	openstacktypes "github.com/openshift/installer/pkg/types/openstack"
    43  	powervstypes "github.com/openshift/installer/pkg/types/powervs"
    44  	vspheretypes "github.com/openshift/installer/pkg/types/vsphere"
    45  )
    46  
    47  var _ asset.WritableRuntimeAsset = (*ClusterAPI)(nil)
    48  
    49  var machineManifestDir = filepath.Join(capiutils.ManifestDir, "machines")
    50  
    51  // ClusterAPI is the asset for CAPI control-plane manifests.
    52  type ClusterAPI struct {
    53  	FileList []*asset.RuntimeFile
    54  }
    55  
    56  // Name returns a human friendly name for the operator.
    57  func (c *ClusterAPI) Name() string {
    58  	return "Cluster API Machine Manifests"
    59  }
    60  
    61  // Dependencies returns all of the dependencies directly needed by the
    62  // ClusterAPI machines asset.
    63  func (c *ClusterAPI) Dependencies() []asset.Asset {
    64  	return []asset.Asset{
    65  		&installconfig.InstallConfig{},
    66  		&installconfig.ClusterID{},
    67  		new(rhcos.Image),
    68  	}
    69  }
    70  
    71  // Generate generates Cluster API machine manifests.
    72  //
    73  //nolint:gocyclo
    74  func (c *ClusterAPI) Generate(ctx context.Context, dependencies asset.Parents) error {
    75  	installConfig := &installconfig.InstallConfig{}
    76  	clusterID := &installconfig.ClusterID{}
    77  	rhcosImage := new(rhcos.Image)
    78  	dependencies.Get(installConfig, clusterID, rhcosImage)
    79  
    80  	// If the feature gate is not enabled, do not generate any manifests.
    81  	if !capiutils.IsEnabled(installConfig) {
    82  		return nil
    83  	}
    84  
    85  	c.FileList = []*asset.RuntimeFile{}
    86  
    87  	var err error
    88  	ic := installConfig.Config
    89  	pool := *ic.ControlPlane
    90  
    91  	switch ic.Platform.Name() {
    92  	case awstypes.Name:
    93  		subnets := map[string]string{}
    94  		bootstrapSubnets := map[string]string{}
    95  		if len(ic.Platform.AWS.Subnets) > 0 {
    96  			// fetch private subnets to master nodes.
    97  			subnetMeta, err := installConfig.AWS.PrivateSubnets(ctx)
    98  			if err != nil {
    99  				return err
   100  			}
   101  			for id, subnet := range subnetMeta {
   102  				subnets[subnet.Zone.Name] = id
   103  			}
   104  			// fetch public subnets for bootstrap, when exists, otherwise use private.
   105  			if installConfig.Config.Publish == types.ExternalPublishingStrategy {
   106  				subnetMeta, err := installConfig.AWS.PublicSubnets(ctx)
   107  				if err != nil {
   108  					return err
   109  				}
   110  				for id, subnet := range subnetMeta {
   111  					bootstrapSubnets[subnet.Zone.Name] = id
   112  				}
   113  			} else {
   114  				bootstrapSubnets = subnets
   115  			}
   116  		}
   117  
   118  		mpool := defaultAWSMachinePoolPlatform("master")
   119  
   120  		osImage := strings.SplitN(rhcosImage.ControlPlane, ",", 2)
   121  		osImageID := osImage[0]
   122  		if len(osImage) == 2 {
   123  			osImageID = "" // the AMI will be generated later on
   124  		}
   125  		mpool.AMIID = osImageID
   126  
   127  		mpool.Set(ic.Platform.AWS.DefaultMachinePlatform)
   128  		mpool.Set(pool.Platform.AWS)
   129  		zoneDefaults := false
   130  		if len(mpool.Zones) == 0 {
   131  			if len(subnets) > 0 {
   132  				for zone := range subnets {
   133  					mpool.Zones = append(mpool.Zones, zone)
   134  				}
   135  			} else {
   136  				mpool.Zones, err = installConfig.AWS.AvailabilityZones(ctx)
   137  				if err != nil {
   138  					return err
   139  				}
   140  				zoneDefaults = true
   141  			}
   142  		}
   143  
   144  		if mpool.InstanceType == "" {
   145  			topology := configv1.HighlyAvailableTopologyMode
   146  			if pool.Replicas != nil && *pool.Replicas == 1 {
   147  				topology = configv1.SingleReplicaTopologyMode
   148  			}
   149  			mpool.InstanceType, err = aws.PreferredInstanceType(ctx, installConfig.AWS, awsdefaults.InstanceTypes(installConfig.Config.Platform.AWS.Region, installConfig.Config.ControlPlane.Architecture, topology), mpool.Zones)
   150  			if err != nil {
   151  				logrus.Warn(errors.Wrap(err, "failed to find default instance type"))
   152  				mpool.InstanceType = awsdefaults.InstanceTypes(installConfig.Config.Platform.AWS.Region, installConfig.Config.ControlPlane.Architecture, topology)[0]
   153  			}
   154  		}
   155  
   156  		// if the list of zones is the default we need to try to filter the list in case there are some zones where the instance might not be available
   157  		if zoneDefaults {
   158  			mpool.Zones, err = aws.FilterZonesBasedOnInstanceType(ctx, installConfig.AWS, mpool.InstanceType, mpool.Zones)
   159  			if err != nil {
   160  				logrus.Warn(errors.Wrap(err, "failed to filter zone list"))
   161  			}
   162  		}
   163  
   164  		tags, err := aws.CapaTagsFromUserTags(clusterID.InfraID, installConfig.Config.Platform.AWS.UserTags)
   165  		if err != nil {
   166  			return fmt.Errorf("failed to create CAPA tags from UserTags: %w", err)
   167  		}
   168  
   169  		pool.Platform.AWS = &mpool
   170  		awsMachines, err := aws.GenerateMachines(clusterID.InfraID, &aws.MachineInput{
   171  			Role:     "master",
   172  			Pool:     &pool,
   173  			Subnets:  subnets,
   174  			Tags:     tags,
   175  			PublicIP: false,
   176  			Ignition: &v1beta2.Ignition{
   177  				Version: "3.2",
   178  				// master machines should get ignition from the MCS on the bootstrap node
   179  				StorageType: v1beta2.IgnitionStorageTypeOptionUnencryptedUserData,
   180  			},
   181  		})
   182  		if err != nil {
   183  			return errors.Wrap(err, "failed to create master machine objects")
   184  		}
   185  		c.FileList = append(c.FileList, awsMachines...)
   186  
   187  		ignition, err := aws.CapaIgnitionWithCertBundleAndProxy(installConfig.Config.AdditionalTrustBundle, installConfig.Config.Proxy)
   188  		if err != nil {
   189  			return fmt.Errorf("failed to generation CAPA ignition: %w", err)
   190  		}
   191  		ignition.StorageType = v1beta2.IgnitionStorageTypeOptionClusterObjectStore
   192  
   193  		pool := *ic.ControlPlane
   194  		pool.Name = "bootstrap"
   195  		pool.Replicas = ptr.To[int64](1)
   196  		pool.Platform.AWS = &mpool
   197  		bootstrapAWSMachine, err := aws.GenerateMachines(clusterID.InfraID, &aws.MachineInput{
   198  			Role:           "bootstrap",
   199  			Subnets:        bootstrapSubnets,
   200  			Pool:           &pool,
   201  			Tags:           tags,
   202  			PublicIP:       installConfig.Config.Publish == types.ExternalPublishingStrategy,
   203  			PublicIpv4Pool: ic.Platform.AWS.PublicIpv4Pool,
   204  			Ignition:       ignition,
   205  		})
   206  		if err != nil {
   207  			return fmt.Errorf("failed to create bootstrap machine object: %w", err)
   208  		}
   209  		c.FileList = append(c.FileList, bootstrapAWSMachine...)
   210  	case azuretypes.Name:
   211  		mpool := defaultAzureMachinePoolPlatform()
   212  		mpool.InstanceType = azuredefaults.ControlPlaneInstanceType(
   213  			installConfig.Config.Platform.Azure.CloudName,
   214  			installConfig.Config.Platform.Azure.Region,
   215  			installConfig.Config.ControlPlane.Architecture,
   216  		)
   217  		mpool.OSDisk.DiskSizeGB = 1024
   218  		if installConfig.Config.Platform.Azure.CloudName == azuretypes.StackCloud {
   219  			mpool.OSDisk.DiskSizeGB = azuredefaults.AzurestackMinimumDiskSize
   220  		}
   221  		mpool.Set(ic.Platform.Azure.DefaultMachinePlatform)
   222  		mpool.Set(pool.Platform.Azure)
   223  
   224  		session, err := installConfig.Azure.Session()
   225  		if err != nil {
   226  			return fmt.Errorf("failed to fetch session: %w", err)
   227  		}
   228  		client := icazure.NewClient(session)
   229  
   230  		if len(mpool.Zones) == 0 {
   231  			// if no azs are given we set to []string{""} for convenience over later operations.
   232  			// It means no-zoned for the machine API
   233  			mpool.Zones = []string{""}
   234  		}
   235  		if len(mpool.Zones) == 0 {
   236  			azs, err := client.GetAvailabilityZones(ctx, ic.Platform.Azure.Region, mpool.InstanceType)
   237  			if err != nil {
   238  				return fmt.Errorf("failed to fetch availability zones: %w", err)
   239  			}
   240  			mpool.Zones = azs
   241  			if len(azs) == 0 {
   242  				// if no azs are given we set to []string{""} for convenience over later operations.
   243  				// It means no-zoned for the machine API
   244  				mpool.Zones = []string{""}
   245  			}
   246  		}
   247  		// client.GetControlPlaneSubnet(ctx, ic.Platform.Azure.ResourceGroupName, ic.Platform.Azure.VirtualNetwork, )
   248  
   249  		if mpool.OSImage.Publisher != "" {
   250  			img, ierr := client.GetMarketplaceImage(ctx, ic.Platform.Azure.Region, mpool.OSImage.Publisher, mpool.OSImage.Offer, mpool.OSImage.SKU, mpool.OSImage.Version)
   251  			if ierr != nil {
   252  				return fmt.Errorf("failed to fetch marketplace image: %w", ierr)
   253  			}
   254  			// Publisher is case-sensitive and matched against exactly. Also the
   255  			// Plan's publisher might not be exactly the same as the Image's
   256  			// publisher
   257  			if img.Plan != nil && img.Plan.Publisher != nil {
   258  				mpool.OSImage.Publisher = *img.Plan.Publisher
   259  			}
   260  		}
   261  		capabilities, err := client.GetVMCapabilities(ctx, mpool.InstanceType, installConfig.Config.Platform.Azure.Region)
   262  		if err != nil {
   263  			return err
   264  		}
   265  		if mpool.VMNetworkingType == "" {
   266  			isAccelerated := icazure.GetVMNetworkingCapability(capabilities)
   267  			if isAccelerated {
   268  				mpool.VMNetworkingType = string(azuretypes.VMnetworkingTypeAccelerated)
   269  			} else {
   270  				logrus.Infof("Instance type %s does not support Accelerated Networking. Using Basic Networking instead.", mpool.InstanceType)
   271  			}
   272  		}
   273  		pool.Platform.Azure = &mpool
   274  		subnet := ic.Azure.ControlPlaneSubnet
   275  
   276  		hyperVGen, err := icazure.GetHyperVGenerationVersion(capabilities, "")
   277  		if err != nil {
   278  			return err
   279  		}
   280  
   281  		azureMachines, err := azure.GenerateMachines(clusterID.InfraID,
   282  			installConfig.Config.Azure.ClusterResourceGroupName(clusterID.InfraID),
   283  			session.Credentials.SubscriptionID,
   284  			&azure.MachineInput{
   285  				Subnet:          subnet,
   286  				Role:            "master",
   287  				UserDataSecret:  "master-user-data",
   288  				HyperVGen:       hyperVGen,
   289  				UseImageGallery: false,
   290  				Private:         installConfig.Config.Publish == types.InternalPublishingStrategy,
   291  				UserTags:        installConfig.Config.Platform.Azure.UserTags,
   292  				Platform:        installConfig.Config.Platform.Azure,
   293  				Pool:            &pool,
   294  			},
   295  		)
   296  		if err != nil {
   297  			return fmt.Errorf("failed to create master machine objects: %w", err)
   298  		}
   299  
   300  		c.FileList = append(c.FileList, azureMachines...)
   301  	case gcptypes.Name:
   302  		// Generate GCP master machines using ControPlane machinepool
   303  		mpool := defaultGCPMachinePoolPlatform(pool.Architecture)
   304  		mpool.Set(ic.Platform.GCP.DefaultMachinePlatform)
   305  		mpool.Set(pool.Platform.GCP)
   306  		if len(mpool.Zones) == 0 {
   307  			azs, err := gcp.ZonesForInstanceType(ic.Platform.GCP.ProjectID, ic.Platform.GCP.Region, mpool.InstanceType)
   308  			if err != nil {
   309  				return errors.Wrap(err, "failed to fetch availability zones")
   310  			}
   311  			mpool.Zones = azs
   312  		}
   313  		pool.Platform.GCP = &mpool
   314  
   315  		gcpMachines, err := gcp.GenerateMachines(
   316  			installConfig,
   317  			clusterID.InfraID,
   318  			&pool,
   319  			rhcosImage.ControlPlane,
   320  		)
   321  		if err != nil {
   322  			return fmt.Errorf("failed to create master machine objects %w", err)
   323  		}
   324  		c.FileList = append(c.FileList, gcpMachines...)
   325  
   326  		// Generate GCP bootstrap machines
   327  		bootstrapMachines, err := gcp.GenerateBootstrapMachines(
   328  			capiutils.GenerateBoostrapMachineName(clusterID.InfraID),
   329  			installConfig,
   330  			clusterID.InfraID,
   331  			&pool,
   332  			rhcosImage.ControlPlane,
   333  		)
   334  		if err != nil {
   335  			return fmt.Errorf("failed to create bootstrap machine objects %w", err)
   336  		}
   337  		c.FileList = append(c.FileList, bootstrapMachines...)
   338  	case vspheretypes.Name:
   339  		mpool := defaultVSphereMachinePoolPlatform()
   340  		mpool.NumCPUs = 4
   341  		mpool.NumCoresPerSocket = 4
   342  		mpool.MemoryMiB = 16384
   343  		mpool.Set(ic.Platform.VSphere.DefaultMachinePlatform)
   344  		mpool.Set(pool.Platform.VSphere)
   345  
   346  		platform := ic.VSphere
   347  		resolver := &net.Resolver{
   348  			PreferGo: true,
   349  		}
   350  
   351  		for _, v := range platform.VCenters {
   352  			// Defense against potential issues with assisted installer
   353  			// If the installer is unable to resolve vCenter there is a good possibility
   354  			// that the installer's install-config has been provided with bogus values.
   355  
   356  			// Timeout context for Lookup
   357  			ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
   358  			defer cancel()
   359  
   360  			_, err := resolver.LookupHost(ctx, v.Server)
   361  			if err != nil {
   362  				logrus.Warnf("unable to resolve vSphere server %s", v.Server)
   363  				return nil
   364  			}
   365  
   366  			// Timeout context for Networks
   367  			// vCenter APIs can be unreliable in performance, extended this context
   368  			// timeout to 60 seconds.
   369  			ctx, cancel = context.WithTimeout(ctx, 60*time.Second)
   370  			defer cancel()
   371  
   372  			err = installConfig.VSphere.Networks(ctx, v, platform.FailureDomains)
   373  			if err != nil {
   374  				// If we are receiving an error as a Soap Fault this is caused by
   375  				// incorrect credentials and in the scenario of assisted installer
   376  				// the credentials are never valid. Since vCenter hostname is
   377  				// incorrect as well we shouldn't get this far.
   378  				if soap.IsSoapFault(err) {
   379  					logrus.Warn("authentication failure to vCenter, Cluster API machine manifests not created, cluster may not install")
   380  					return nil
   381  				}
   382  				return err
   383  			}
   384  		}
   385  
   386  		// The machinepool has no zones defined, there are FailureDomains
   387  		// This is a vSphere zonal installation. Generate machinepool zone
   388  		// list.
   389  
   390  		fdCount := int64(len(ic.Platform.VSphere.FailureDomains))
   391  		var idx int64
   392  		if len(mpool.Zones) == 0 && len(ic.VSphere.FailureDomains) != 0 {
   393  			for i := int64(0); i < *(ic.ControlPlane.Replicas); i++ {
   394  				idx = i
   395  				if idx >= fdCount {
   396  					idx = i % fdCount
   397  				}
   398  				mpool.Zones = append(mpool.Zones, ic.VSphere.FailureDomains[idx].Name)
   399  			}
   400  		}
   401  
   402  		pool.Platform.VSphere = &mpool
   403  		templateName := clusterID.InfraID + "-rhcos"
   404  
   405  		c.FileList, err = vspherecapi.GenerateMachines(ctx, clusterID.InfraID, ic, &pool, templateName, "master", installConfig.VSphere)
   406  		if err != nil {
   407  			return fmt.Errorf("unable to generate CAPI machines for vSphere %w", err)
   408  		}
   409  	case openstacktypes.Name:
   410  		mpool := defaultOpenStackMachinePoolPlatform()
   411  		mpool.Set(ic.Platform.OpenStack.DefaultMachinePlatform)
   412  		mpool.Set(pool.Platform.OpenStack)
   413  		pool.Platform.OpenStack = &mpool
   414  
   415  		imageName, _ := rhcosutils.GenerateOpenStackImageName(rhcosImage.ControlPlane, clusterID.InfraID)
   416  
   417  		for _, role := range []string{"master", "bootstrap"} {
   418  			openStackMachines, err := openstack.GenerateMachines(
   419  				clusterID.InfraID,
   420  				ic,
   421  				&pool,
   422  				imageName,
   423  				role,
   424  			)
   425  			if err != nil {
   426  				return fmt.Errorf("failed to create machine objects: %w", err)
   427  			}
   428  			c.FileList = append(c.FileList, openStackMachines...)
   429  		}
   430  	case powervstypes.Name:
   431  		// Generate PowerVS master machines using ControPlane machinepool
   432  		mpool := defaultPowerVSMachinePoolPlatform(ic)
   433  		mpool.Set(ic.Platform.PowerVS.DefaultMachinePlatform)
   434  		mpool.Set(pool.Platform.PowerVS)
   435  		pool.Platform.PowerVS = &mpool
   436  
   437  		powervsMachines, err := powervs.GenerateMachines(
   438  			clusterID.InfraID,
   439  			ic,
   440  			&pool,
   441  			"master",
   442  		)
   443  		if err != nil {
   444  			return fmt.Errorf("failed to create master machine objects %w", err)
   445  		}
   446  
   447  		c.FileList = append(c.FileList, powervsMachines...)
   448  	case nutanixtypes.Name:
   449  		mpool := defaultNutanixMachinePoolPlatform()
   450  		mpool.NumCPUs = 8
   451  		mpool.Set(ic.Platform.Nutanix.DefaultMachinePlatform)
   452  		mpool.Set(pool.Platform.Nutanix)
   453  		if err = mpool.ValidateConfig(ic.Platform.Nutanix, "master"); err != nil {
   454  			return fmt.Errorf("failed to generate Cluster API machine manifests for control-plane: %w", err)
   455  		}
   456  		pool.Platform.Nutanix = &mpool
   457  		templateName := nutanixtypes.RHCOSImageName(clusterID.InfraID)
   458  
   459  		c.FileList, err = nutanixcapi.GenerateMachines(clusterID.InfraID, ic, &pool, templateName, "master")
   460  		if err != nil {
   461  			return fmt.Errorf("unable to generate CAPI machines for Nutanix %w", err)
   462  		}
   463  	default:
   464  		// TODO: support other platforms
   465  	}
   466  
   467  	// Create the machine manifests.
   468  	for _, m := range c.FileList {
   469  		objData, err := yaml.Marshal(m.Object)
   470  		if err != nil {
   471  			return errors.Wrapf(err, "failed to marshal Cluster API machine manifest %s", m.Filename)
   472  		}
   473  		m.Data = objData
   474  
   475  		// If the filename is already a path, do not append the manifest dir.
   476  		if filepath.Dir(m.Filename) == machineManifestDir {
   477  			continue
   478  		}
   479  		m.Filename = filepath.Join(machineManifestDir, m.Filename)
   480  	}
   481  	asset.SortManifestFiles(c.FileList)
   482  	return nil
   483  }
   484  
   485  // Files returns the files generated by the asset.
   486  func (c *ClusterAPI) Files() []*asset.File {
   487  	files := []*asset.File{}
   488  	for _, f := range c.FileList {
   489  		f := f // TODO: remove with golang 1.22
   490  		files = append(files, &f.File)
   491  	}
   492  	return files
   493  }
   494  
   495  // RuntimeFiles returns the files generated by the asset.
   496  func (c *ClusterAPI) RuntimeFiles() []*asset.RuntimeFile {
   497  	return c.FileList
   498  }
   499  
   500  // Load returns the openshift asset from disk.
   501  func (c *ClusterAPI) Load(f asset.FileFetcher) (bool, error) {
   502  	yamlFileList, err := f.FetchByPattern(filepath.Join(machineManifestDir, "*.yaml"))
   503  	if err != nil {
   504  		return false, errors.Wrap(err, "failed to load *.yaml files")
   505  	}
   506  	ymlFileList, err := f.FetchByPattern(filepath.Join(machineManifestDir, "*.yml"))
   507  	if err != nil {
   508  		return false, errors.Wrap(err, "failed to load *.yml files")
   509  	}
   510  	jsonFileList, err := f.FetchByPattern(filepath.Join(machineManifestDir, "*.json"))
   511  	if err != nil {
   512  		return false, errors.Wrap(err, "failed to load *.json files")
   513  	}
   514  	fileList := append(yamlFileList, ymlFileList...) //nolint:gocritic
   515  	fileList = append(fileList, jsonFileList...)
   516  
   517  	for _, file := range fileList {
   518  		u := &unstructured.Unstructured{}
   519  		if err := yaml.Unmarshal(file.Data, u); err != nil {
   520  			return false, errors.Wrap(err, "failed to unmarshal file")
   521  		}
   522  		obj, err := clusterapi.Scheme.New(u.GroupVersionKind())
   523  		if err != nil {
   524  			return false, errors.Wrap(err, "failed to create object")
   525  		}
   526  		if err := clusterapi.Scheme.Convert(u, obj, nil); err != nil {
   527  			return false, errors.Wrap(err, "failed to convert object")
   528  		}
   529  		c.FileList = append(c.FileList, &asset.RuntimeFile{
   530  			File: asset.File{
   531  				Filename: file.Filename,
   532  				Data:     file.Data,
   533  			},
   534  			Object: obj.(client.Object),
   535  		})
   536  	}
   537  
   538  	asset.SortManifestFiles(c.FileList)
   539  	return len(c.FileList) > 0, nil
   540  }