get.porter.sh/porter@v1.3.0/pkg/porter/build.go (about)

     1  package porter
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  
     9  	"get.porter.sh/porter/pkg"
    10  	"get.porter.sh/porter/pkg/build"
    11  	"get.porter.sh/porter/pkg/cnab"
    12  	configadapter "get.porter.sh/porter/pkg/cnab/config-adapter"
    13  	"get.porter.sh/porter/pkg/config"
    14  	"get.porter.sh/porter/pkg/manifest"
    15  	"get.porter.sh/porter/pkg/mixin"
    16  	"get.porter.sh/porter/pkg/printer"
    17  	"get.porter.sh/porter/pkg/storage"
    18  	"get.porter.sh/porter/pkg/tracing"
    19  	"github.com/Masterminds/semver/v3"
    20  	"github.com/opencontainers/go-digest"
    21  	"golang.org/x/sync/errgroup"
    22  )
    23  
    24  type BuildOptions struct {
    25  	BundleDefinitionOptions
    26  	MetadataOpts
    27  	build.BuildImageOptions
    28  
    29  	// NoLint indicates if lint should be run before build.
    30  	NoLint bool
    31  
    32  	// Driver to use when building the bundle image.
    33  	Driver string
    34  
    35  	// Custom is the unparsed list of NAME=VALUE custom inputs set on the command line.
    36  	Customs []string
    37  
    38  	// InsecureRegistry allows connecting to an unsecured registry or one without verifiable certificates.
    39  	InsecureRegistry bool
    40  
    41  	// parsedCustoms is the parsed set of custom inputs from Customs.
    42  	parsedCustoms map[string]string
    43  }
    44  
    45  const BuildDriverDefault = config.BuildDriverBuildkit
    46  
    47  var BuildDriverAllowedValues = []string{config.BuildDriverBuildkit}
    48  
    49  func (o *BuildOptions) Validate(p *Porter) error {
    50  	if o.Version != "" {
    51  		v, err := semver.NewVersion(o.Version)
    52  		if err != nil {
    53  			return fmt.Errorf("invalid bundle version: %q is not a valid semantic version", o.Version)
    54  		}
    55  		o.Version = v.String()
    56  	}
    57  
    58  	if o.Driver == "" {
    59  		o.Driver = p.GetBuildDriver()
    60  	}
    61  	if !stringSliceContains(BuildDriverAllowedValues, o.Driver) {
    62  		return fmt.Errorf("invalid --driver value %s", o.Driver)
    63  	}
    64  
    65  	// Syncing value back to the config, and we will always use the config
    66  	// to determine the driver
    67  	// This would be less awkward if we didn't do an automatic build during publish
    68  	p.Data.BuildDriver = o.Driver
    69  
    70  	err := o.parseCustomInputs()
    71  	if err != nil {
    72  		return err
    73  	}
    74  
    75  	err = o.BundleDefinitionOptions.Validate(p.Context)
    76  	if err != nil {
    77  		return err
    78  	}
    79  
    80  	if o.File == "" {
    81  		return fmt.Errorf("could not find porter.yaml in the current directory %s, make sure you are in the right directory or specify the porter manifest with --file", o.Dir)
    82  	}
    83  
    84  	return nil
    85  }
    86  
    87  func stringSliceContains(allowedValues []string, value string) bool {
    88  	for _, allowed := range allowedValues {
    89  		if value == allowed {
    90  			return true
    91  		}
    92  	}
    93  	return false
    94  }
    95  
    96  func (o *BuildOptions) parseCustomInputs() error {
    97  	p, err := storage.ParseVariableAssignments(o.Customs)
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	o.parsedCustoms = p
   103  
   104  	return nil
   105  }
   106  
   107  func (p *Porter) Build(ctx context.Context, opts BuildOptions) error {
   108  	ctx, span := tracing.StartSpan(ctx)
   109  	defer span.EndSpan()
   110  
   111  	span.Debugf("Using %s build driver", p.GetBuildDriver())
   112  
   113  	// Start with a fresh .cnab directory before building
   114  	err := p.FileSystem.RemoveAll(build.LOCAL_CNAB)
   115  	if err != nil {
   116  		return span.Error(fmt.Errorf("could not cleanup generated .cnab directory before building: %w", err))
   117  	}
   118  
   119  	// Generate Porter's canonical version of the user-provided manifest
   120  	if err := p.generateInternalManifest(ctx, opts); err != nil {
   121  		return fmt.Errorf("unable to generate manifest: %w", err)
   122  	}
   123  
   124  	m, err := manifest.LoadManifestFrom(ctx, p.Config, build.LOCAL_MANIFEST)
   125  	if err != nil {
   126  		return err
   127  	}
   128  
   129  	// Capture the path to the original, user-provided manifest.
   130  	// This value will be referenced elsewhere, for instance by
   131  	// the digest logic (to dictate auto-rebuild)
   132  	m.ManifestPath = opts.File
   133  
   134  	if !opts.NoLint {
   135  		if err := p.preLint(ctx, opts.File); err != nil {
   136  			return err
   137  		}
   138  	}
   139  
   140  	// Build bundle so that resulting bundle.json is available for inclusion
   141  	// into the bundle image.
   142  	// Note: the content digest field on the bundle image section of the
   143  	// bundle.json will *not* be correct until the image is actually pushed
   144  	// to a registry.  The bundle.json will need to be updated after publishing
   145  	// and provided just-in-time during bundle execution.
   146  	if err := p.buildBundle(ctx, m, "", opts.PreserveTags); err != nil {
   147  		return span.Error(fmt.Errorf("unable to build bundle: %w", err))
   148  	}
   149  
   150  	generator := build.NewDockerfileGenerator(p.Config, m, p.Templates, p.Mixins)
   151  
   152  	if err := generator.PrepareFilesystem(); err != nil {
   153  		return span.Error(fmt.Errorf("unable to copy run script, runtimes or mixins: %s", err))
   154  	}
   155  	if err := generator.GenerateDockerFile(ctx); err != nil {
   156  		return span.Error(fmt.Errorf("unable to generate Dockerfile: %s", err))
   157  	}
   158  
   159  	builder := p.GetBuilder(ctx)
   160  
   161  	err = builder.BuildBundleImage(ctx, m, opts.BuildImageOptions)
   162  	if err != nil {
   163  		return span.Error(fmt.Errorf("unable to build bundle image: %w", err))
   164  	}
   165  
   166  	return nil
   167  }
   168  
   169  func (p *Porter) preLint(ctx context.Context, file string) error {
   170  	lintOpts := LintOptions{
   171  		PrintOptions: printer.PrintOptions{},
   172  		File:         file,
   173  	}
   174  	lintOpts.RawFormat = string(printer.FormatPlaintext)
   175  	err := lintOpts.Validate(p.Context)
   176  	if err != nil {
   177  		return err
   178  	}
   179  
   180  	results, err := p.Lint(ctx, lintOpts)
   181  	if err != nil {
   182  		return err
   183  	}
   184  
   185  	if len(results) > 0 {
   186  		fmt.Fprintln(p.Out, results.String())
   187  	}
   188  
   189  	if results.HasError() {
   190  		// An error was found during linting, stop and let the user correct it
   191  		return errors.New("lint errors were detected. Rerun with --no-lint ignore the errors")
   192  	}
   193  
   194  	return nil
   195  }
   196  
   197  func (p *Porter) getUsedMixins(ctx context.Context, m *manifest.Manifest) ([]mixin.Metadata, error) {
   198  	ctx, span := tracing.StartSpan(ctx)
   199  	defer span.EndSpan()
   200  
   201  	g := new(errgroup.Group)
   202  	results := make(chan mixin.Metadata, len(m.Mixins))
   203  	for _, m := range m.Mixins {
   204  		m := m
   205  		g.Go(func() error {
   206  			result, err := p.Mixins.GetMetadata(ctx, m.Name)
   207  			if err != nil {
   208  				return err
   209  			}
   210  
   211  			mixinMetadata := result.(*mixin.Metadata)
   212  			results <- *mixinMetadata
   213  			return nil
   214  		})
   215  	}
   216  
   217  	if err := g.Wait(); err != nil {
   218  		return nil, err
   219  	}
   220  
   221  	usedMixins := make([]mixin.Metadata, len(m.Mixins))
   222  	for i := 0; i < len(usedMixins); i++ {
   223  		result := <-results
   224  		usedMixins[i] = result
   225  	}
   226  
   227  	return usedMixins, nil
   228  }
   229  
   230  func (p *Porter) buildBundle(ctx context.Context, m *manifest.Manifest, digest digest.Digest, preserveTags bool) error {
   231  	imageDigests := map[string]string{m.Image: digest.String()}
   232  
   233  	mixins, err := p.getUsedMixins(ctx, m)
   234  	if err != nil {
   235  		return err
   236  	}
   237  
   238  	converter := configadapter.NewManifestConverter(p.Config, m, imageDigests, mixins, preserveTags)
   239  	bun, err := converter.ToBundle(ctx)
   240  	if err != nil {
   241  		return err
   242  	}
   243  
   244  	return p.writeBundle(bun)
   245  }
   246  
   247  func (p Porter) writeBundle(b cnab.ExtendedBundle) error {
   248  	f, err := p.Config.FileSystem.OpenFile(build.LOCAL_BUNDLE, os.O_RDWR|os.O_CREATE|os.O_TRUNC, pkg.FileModeWritable)
   249  	if err != nil {
   250  		return fmt.Errorf("error creating %s: %w", build.LOCAL_BUNDLE, err)
   251  	}
   252  	defer f.Close()
   253  	_, err = b.WriteTo(f)
   254  	if err != nil {
   255  		return fmt.Errorf("error writing to %s: %w", build.LOCAL_BUNDLE, err)
   256  	}
   257  
   258  	return nil
   259  }