mirror of
https://github.com/ko-build/ko.git
synced 2025-04-17 11:26:39 +02:00
291 lines
10 KiB
Go
291 lines
10 KiB
Go
// Copyright 2019 ko Build Authors All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package options
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
|
|
"github.com/google/go-containerregistry/pkg/name"
|
|
"github.com/google/ko/pkg/build"
|
|
"github.com/mitchellh/mapstructure"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
"golang.org/x/tools/go/packages"
|
|
)
|
|
|
|
const (
|
|
// configDefaultBaseImage is the default base image if not specified in .ko.yaml.
|
|
configDefaultBaseImage = "cgr.dev/chainguard/static:latest"
|
|
)
|
|
|
|
// BuildOptions represents options for the ko builder.
|
|
type BuildOptions struct {
|
|
// BaseImage enables setting the default base image programmatically.
|
|
// If non-empty, this takes precedence over the value in `.ko.yaml`.
|
|
BaseImage string
|
|
|
|
// BaseImageOverrides stores base image overrides for import paths.
|
|
BaseImageOverrides map[string]string
|
|
|
|
// DefaultPlatforms defines the default platforms when Platforms is not explicitly defined
|
|
DefaultPlatforms []string
|
|
|
|
// DefaultEnv defines the default environment when per-build value is not explicitly defined.
|
|
DefaultEnv []string
|
|
|
|
// DefaultFlags defines the default flags when per-build value is not explicitly defined.
|
|
DefaultFlags []string
|
|
|
|
// DefaultLdflags defines the default ldflags when per-build value is not explicitly defined.
|
|
DefaultLdflags []string
|
|
|
|
// WorkingDirectory allows for setting the working directory for invocations of the `go` tool.
|
|
// Empty string means the current working directory.
|
|
WorkingDirectory string
|
|
|
|
ConcurrentBuilds int
|
|
DisableOptimizations bool
|
|
SBOM string
|
|
SBOMDir string
|
|
Platforms []string
|
|
Labels []string
|
|
Annotations []string
|
|
User string
|
|
Debug bool
|
|
// UserAgent enables overriding the default value of the `User-Agent` HTTP
|
|
// request header used when retrieving the base image.
|
|
UserAgent string
|
|
|
|
InsecureRegistry bool
|
|
|
|
// Trimpath controls whether ko adds the `-trimpath` flag to `go build` by default.
|
|
// The `-trimpath` flags aids in achieving reproducible builds, but it removes path information that is useful for interactive debugging.
|
|
// Set this field to `false` and `DisableOptimizations` to `true` if you want to interactively debug the binary in the resulting image.
|
|
// `AddBuildOptions()` defaults this field to `true`.
|
|
Trimpath bool
|
|
|
|
// BuildConfigs stores the per-image build config from `.ko.yaml`.
|
|
BuildConfigs map[string]build.Config
|
|
}
|
|
|
|
func AddBuildOptions(cmd *cobra.Command, bo *BuildOptions) {
|
|
cmd.Flags().IntVarP(&bo.ConcurrentBuilds, "jobs", "j", 0,
|
|
"The maximum number of concurrent builds (default GOMAXPROCS)")
|
|
cmd.Flags().BoolVar(&bo.DisableOptimizations, "disable-optimizations", bo.DisableOptimizations,
|
|
"Disable optimizations when building Go code. Useful when you want to interactively debug the created container.")
|
|
cmd.Flags().StringVar(&bo.SBOM, "sbom", "spdx",
|
|
"The SBOM media type to use (none will disable SBOM synthesis and upload).")
|
|
cmd.Flags().StringVar(&bo.SBOMDir, "sbom-dir", "",
|
|
"Path to file where the SBOM will be written.")
|
|
cmd.Flags().StringSliceVar(&bo.Platforms, "platform", []string{},
|
|
"Which platform to use when pulling a multi-platform base. Format: all | <os>[/<arch>[/<variant>]][,platform]*")
|
|
cmd.Flags().StringSliceVar(&bo.Labels, "image-label", []string{},
|
|
"Which labels (key=value[,key=value]) to add to the image.")
|
|
cmd.Flags().StringSliceVar(&bo.Annotations, "image-annotation", []string{},
|
|
"Which annotations (key=value[,key=value]) to add to the OCI manifest.")
|
|
cmd.Flags().StringVar(&bo.User, "image-user", "",
|
|
"The default user the image should be run as.")
|
|
cmd.Flags().BoolVar(&bo.Debug, "debug", bo.Debug,
|
|
"Include Delve debugger into image and wrap around ko-app. This debugger will listen to port 40000.")
|
|
bo.Trimpath = true
|
|
}
|
|
|
|
// LoadConfig reads build configuration from defaults, environment variables, and the `.ko.yaml` config file.
|
|
func (bo *BuildOptions) LoadConfig() error {
|
|
v := viper.New()
|
|
if bo.WorkingDirectory == "" {
|
|
bo.WorkingDirectory = "."
|
|
}
|
|
// If omitted, use this base image.
|
|
v.SetDefault("defaultBaseImage", configDefaultBaseImage)
|
|
const configName = ".ko"
|
|
|
|
v.SetConfigName(configName) // .yaml is implicit
|
|
v.SetEnvPrefix("KO")
|
|
v.AutomaticEnv()
|
|
|
|
if override := os.Getenv("KO_CONFIG_PATH"); override != "" {
|
|
file, err := os.Stat(override)
|
|
if err != nil {
|
|
return fmt.Errorf("error looking for config file: %w", err)
|
|
}
|
|
if file.Mode().IsRegular() {
|
|
v.SetConfigFile(override)
|
|
} else if file.IsDir() {
|
|
path := filepath.Join(override, ".ko.yaml")
|
|
file, err = os.Stat(path)
|
|
if err != nil {
|
|
return fmt.Errorf("error looking for config file: %w", err)
|
|
}
|
|
if file.Mode().IsRegular() {
|
|
v.SetConfigFile(path)
|
|
} else {
|
|
return fmt.Errorf("config file %s is not a regular file", path)
|
|
}
|
|
} else {
|
|
return fmt.Errorf("config file %s is not a regular file", override)
|
|
}
|
|
}
|
|
v.AddConfigPath(bo.WorkingDirectory)
|
|
|
|
if err := v.ReadInConfig(); err != nil {
|
|
if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
|
|
return fmt.Errorf("error reading config file: %w", err)
|
|
}
|
|
}
|
|
|
|
if dp := v.GetStringSlice("defaultPlatforms"); len(dp) > 0 {
|
|
bo.DefaultPlatforms = dp
|
|
}
|
|
|
|
if env := v.GetStringSlice("defaultEnv"); len(env) > 0 {
|
|
bo.DefaultEnv = env
|
|
}
|
|
|
|
if flags := v.GetStringSlice("defaultFlags"); len(flags) > 0 {
|
|
bo.DefaultFlags = flags
|
|
}
|
|
|
|
if ldflags := v.GetStringSlice("defaultLdflags"); len(ldflags) > 0 {
|
|
bo.DefaultLdflags = ldflags
|
|
}
|
|
|
|
if bo.BaseImage == "" {
|
|
ref := v.GetString("defaultBaseImage")
|
|
if _, err := name.ParseReference(ref); err != nil {
|
|
return fmt.Errorf("'defaultBaseImage': error parsing %q as image reference: %w", ref, err)
|
|
}
|
|
bo.BaseImage = ref
|
|
}
|
|
|
|
if len(bo.BaseImageOverrides) == 0 {
|
|
baseImageOverrides := map[string]string{}
|
|
overrides := v.GetStringMapString("baseImageOverrides")
|
|
for key, value := range overrides {
|
|
if _, err := name.ParseReference(value); err != nil {
|
|
return fmt.Errorf("'baseImageOverrides': error parsing %q as image reference: %w", value, err)
|
|
}
|
|
baseImageOverrides[key] = value
|
|
}
|
|
bo.BaseImageOverrides = baseImageOverrides
|
|
}
|
|
|
|
if len(bo.BuildConfigs) == 0 {
|
|
var builds []build.Config
|
|
useYAMLTagsAndUnmarshallers := func(c *mapstructure.DecoderConfig) {
|
|
c.TagName = "yaml" // defaults to `mapstructure:""`
|
|
c.DecodeHook = yamlUnmarshallerHookFunc
|
|
}
|
|
if err := v.UnmarshalKey("builds", &builds, useYAMLTagsAndUnmarshallers); err != nil {
|
|
return fmt.Errorf("configuration section 'builds' cannot be parsed: %w", err)
|
|
}
|
|
buildConfigs, err := createBuildConfigMap(bo.WorkingDirectory, builds)
|
|
if err != nil {
|
|
return fmt.Errorf("could not create build config map: %w", err)
|
|
}
|
|
bo.BuildConfigs = buildConfigs
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func yamlUnmarshallerHookFunc(_ reflect.Type, to reflect.Type, data any) (any, error) {
|
|
type yamlUnmarshaller interface {
|
|
UnmarshalYAML(func(any) error) error
|
|
}
|
|
result := reflect.New(to).Interface()
|
|
unmarshaller, ok := result.(yamlUnmarshaller)
|
|
if !ok {
|
|
return data, nil
|
|
}
|
|
if err := unmarshaller.UnmarshalYAML(func(target any) error {
|
|
dest := reflect.Indirect(reflect.ValueOf(target))
|
|
src := reflect.ValueOf(data)
|
|
if dest.CanSet() && src.Type().AssignableTo(dest.Type()) {
|
|
dest.Set(src)
|
|
return nil
|
|
}
|
|
return fmt.Errorf("want %v, got %v", dest.Type(), src.Type())
|
|
}); err != nil {
|
|
// We do not implement []string <- []any above, therefore YAML
|
|
// unmarshaller could fail given perfectly valid input. Return
|
|
// data AS IS, allowing mapstructure's logic to perform the
|
|
// conversion.
|
|
return data, nil
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func createBuildConfigMap(workingDirectory string, configs []build.Config) (map[string]build.Config, error) {
|
|
buildConfigsByImportPath := make(map[string]build.Config)
|
|
for i, config := range configs {
|
|
// In case no ID is specified, use the index of the build config in
|
|
// the ko YAML file as a reference (debug help).
|
|
if config.ID == "" {
|
|
config.ID = fmt.Sprintf("#%d", i)
|
|
}
|
|
|
|
// Make sure to behave like GoReleaser by defaulting to the current
|
|
// directory in case the build or main field is not set, check
|
|
// https://goreleaser.com/customization/build/ for details
|
|
if config.Dir == "" {
|
|
config.Dir = "."
|
|
}
|
|
if config.Main == "" {
|
|
config.Main = "."
|
|
}
|
|
|
|
// baseDir is the directory where `go list` will be run to look for package information
|
|
baseDir := filepath.Join(workingDirectory, config.Dir)
|
|
|
|
// To behave like GoReleaser, check whether the configured `main` config value points to a
|
|
// source file, and if so, just use the directory it is in
|
|
path := config.Main
|
|
if fi, err := os.Stat(filepath.Join(baseDir, config.Main)); err == nil && fi.Mode().IsRegular() {
|
|
path = filepath.Dir(config.Main)
|
|
}
|
|
|
|
// Verify that the path actually leads to a local file (https://github.com/google/ko/issues/483)
|
|
if _, err := os.Stat(filepath.Join(baseDir, path)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// By default, paths configured in the builds section are considered
|
|
// local import paths, therefore add a "./" equivalent as a prefix to
|
|
// the constructured import path
|
|
localImportPath := fmt.Sprint(".", string(filepath.Separator), path)
|
|
dir := filepath.Clean(baseDir)
|
|
if dir == "." {
|
|
dir = ""
|
|
}
|
|
pkgs, err := packages.Load(&packages.Config{Mode: packages.NeedName, Dir: dir}, localImportPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("'builds': entry #%d does not contain a valid local import path (%s) for directory (%s): %w", i, localImportPath, baseDir, err)
|
|
}
|
|
|
|
if len(pkgs) != 1 {
|
|
return nil, fmt.Errorf("'builds': entry #%d results in %d local packages, only 1 is expected", i, len(pkgs))
|
|
}
|
|
importPath := pkgs[0].PkgPath
|
|
buildConfigsByImportPath[importPath] = config
|
|
}
|
|
|
|
return buildConfigsByImportPath, nil
|
|
}
|