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 }