istio.io/istio@v0.0.0-20240520182934-d79c90f27776/tools/docker-builder/main.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package main
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"os"
    21  	"os/exec"
    22  	"path/filepath"
    23  	"runtime"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/spf13/cobra"
    28  	"sigs.k8s.io/yaml"
    29  
    30  	"istio.io/istio/pkg/log"
    31  	testenv "istio.io/istio/pkg/test/env"
    32  	"istio.io/istio/pkg/tracing"
    33  	"istio.io/istio/pkg/util/sets"
    34  	pkgversion "istio.io/istio/pkg/version"
    35  )
    36  
    37  func main() {
    38  	rootCmd.Flags().StringSliceVar(&globalArgs.Hubs, "hub", globalArgs.Hubs, "docker hub(s)")
    39  	rootCmd.Flags().StringSliceVar(&globalArgs.Tags, "tag", globalArgs.Tags, "docker tag(s)")
    40  
    41  	rootCmd.Flags().StringVar(&globalArgs.BaseVersion, "base-version", globalArgs.BaseVersion, "base version to use")
    42  	rootCmd.Flags().StringVar(&globalArgs.BaseImageRegistry, "image-base-registry", globalArgs.BaseImageRegistry, "base image registry to use")
    43  	rootCmd.Flags().StringVar(&globalArgs.ProxyVersion, "proxy-version", globalArgs.ProxyVersion, "proxy version to use")
    44  	rootCmd.Flags().StringVar(&globalArgs.ZtunnelVersion, "ztunnel-version", globalArgs.ZtunnelVersion, "ztunnel version to use")
    45  	rootCmd.Flags().StringVar(&globalArgs.IstioVersion, "istio-version", globalArgs.IstioVersion, "istio version to use")
    46  
    47  	rootCmd.Flags().StringSliceVar(&globalArgs.Targets, "targets", globalArgs.Targets, "targets to build")
    48  	rootCmd.Flags().StringSliceVar(&globalArgs.Variants, "variants", globalArgs.Variants, "variants to build")
    49  	rootCmd.Flags().StringSliceVar(&globalArgs.Architectures, "architectures", globalArgs.Architectures, "architectures to build")
    50  	rootCmd.Flags().BoolVar(&globalArgs.Push, "push", globalArgs.Push, "push targets to registry")
    51  	rootCmd.Flags().BoolVar(&globalArgs.Save, "save", globalArgs.Save, "save targets to tar.gz")
    52  	rootCmd.Flags().BoolVar(&globalArgs.NoCache, "no-cache", globalArgs.NoCache, "disable caching")
    53  	rootCmd.Flags().BoolVar(&globalArgs.NoClobber, "no-clobber", globalArgs.NoClobber, "do not allow pushing images that already exist")
    54  	rootCmd.Flags().StringVar(&globalArgs.Builder, "builder", globalArgs.Builder, "type of builder to use. options are crane or docker")
    55  	rootCmd.Flags().BoolVar(&version, "version", version, "show build version")
    56  
    57  	rootCmd.Flags().BoolVar(&globalArgs.SupportsEmulation, "qemu", globalArgs.SupportsEmulation, "if enable, allows building images that require emulation")
    58  
    59  	if err := rootCmd.Execute(); err != nil {
    60  		os.Exit(-1)
    61  	}
    62  }
    63  
    64  var privilegedHubs = sets.New[string](
    65  	"docker.io/istio",
    66  	"istio",
    67  	"gcr.io/istio-release",
    68  	"gcr.io/istio-testing",
    69  )
    70  
    71  var rootCmd = &cobra.Command{
    72  	SilenceUsage: true,
    73  	Short:        "Builds Istio docker images",
    74  	RunE: func(cmd *cobra.Command, _ []string) error {
    75  		t0 := time.Now()
    76  		defer func() {
    77  			log.WithLabels("runtime", time.Since(t0)).Infof("build complete")
    78  		}()
    79  		ctx, shutdown, err := tracing.InitializeFullBinary("docker-builder")
    80  		if err != nil {
    81  			return err
    82  		}
    83  		defer shutdown()
    84  		if version {
    85  			fmt.Println(pkgversion.Info.GitRevision)
    86  			os.Exit(0)
    87  		}
    88  		log.Infof("Args: %s", globalArgs)
    89  		if err := ValidateArgs(globalArgs); err != nil {
    90  			return err
    91  		}
    92  
    93  		args, err := ReadPlan(ctx, globalArgs)
    94  		if err != nil {
    95  			return fmt.Errorf("plan: %v", err)
    96  		}
    97  
    98  		// The Istio image builder has two building modes - one utilizing docker, and one manually constructing
    99  		// images using the go-containerregistry (crane) libraries.
   100  		// The crane builder is much faster but less tested.
   101  		// Neither builder is doing standard logic; see each builder for details.
   102  		if args.Builder == CraneBuilder {
   103  			return RunCrane(ctx, args)
   104  		}
   105  
   106  		return RunDocker(args)
   107  	},
   108  }
   109  
   110  func ValidateArgs(a Args) error {
   111  	if len(a.Targets) == 0 {
   112  		return fmt.Errorf("no targets specified")
   113  	}
   114  	if a.Push && a.Save {
   115  		// TODO(https://github.com/moby/buildkit/issues/1555) support both
   116  		return fmt.Errorf("--push and --save are mutually exclusive")
   117  	}
   118  	_, inCI := os.LookupEnv("CI")
   119  	if a.Push && len(privilegedHubs.Intersection(sets.New(a.Hubs...))) > 0 && !inCI {
   120  		// Safety check against developer error. If they have a legitimate use case, they can set CI var
   121  		return fmt.Errorf("pushing to official registry only supported in CI")
   122  	}
   123  	if !sets.New(DockerBuilder, CraneBuilder).Contains(a.Builder) {
   124  		return fmt.Errorf("unknown builder %v", a.Builder)
   125  	}
   126  
   127  	if a.Builder == CraneBuilder && a.Save {
   128  		return fmt.Errorf("crane builder does not support save")
   129  	}
   130  	if a.Builder == CraneBuilder && a.NoClobber {
   131  		return fmt.Errorf("crane builder does not support no-clobber")
   132  	}
   133  	if a.Builder == CraneBuilder && a.NoCache {
   134  		return fmt.Errorf("crane builder does not support no-cache")
   135  	}
   136  	if a.Builder == CraneBuilder && !a.Push {
   137  		return fmt.Errorf("crane builder only supports pushing")
   138  	}
   139  	return nil
   140  }
   141  
   142  func ReadPlanTargets() ([]string, []string, error) {
   143  	by, err := os.ReadFile(filepath.Join(testenv.IstioSrc, "tools", "docker.yaml"))
   144  	if err != nil {
   145  		return nil, nil, err
   146  	}
   147  	plan := BuildPlan{}
   148  	if err := yaml.Unmarshal(by, &plan); err != nil {
   149  		return nil, nil, err
   150  	}
   151  	bases := sets.New[string]()
   152  	nonBases := sets.New[string]()
   153  	for _, i := range plan.Images {
   154  		if i.Base {
   155  			bases.Insert(i.Name)
   156  		} else {
   157  			nonBases.Insert(i.Name)
   158  		}
   159  	}
   160  	return sets.SortedList(bases), sets.SortedList(nonBases), nil
   161  }
   162  
   163  var LocalArch = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
   164  
   165  func ReadPlan(ctx context.Context, a Args) (Args, error) {
   166  	_, span := tracing.Start(ctx, "ReadPlan")
   167  	defer span.End()
   168  	by, err := os.ReadFile(filepath.Join(testenv.IstioSrc, "tools", "docker.yaml"))
   169  	if err != nil {
   170  		return a, err
   171  	}
   172  	a.Plan = map[string]BuildPlan{}
   173  	for _, arch := range a.Architectures {
   174  		plan := BuildPlan{}
   175  
   176  		// We allow variables in the plan
   177  		input := os.Expand(string(by), func(s string) string {
   178  			data := archToEnvMap(arch)
   179  			data["SIDECAR"] = "envoy"
   180  			if _, f := os.LookupEnv("DEBUG_IMAGE"); f {
   181  				data["RELEASE_MODE"] = "debug"
   182  			} else {
   183  				data["RELEASE_MODE"] = "release"
   184  			}
   185  			if r, f := data[s]; f {
   186  				return r
   187  			}
   188  
   189  			// Fallback to env
   190  			return os.Getenv(s)
   191  		})
   192  		if err := yaml.Unmarshal([]byte(input), &plan); err != nil {
   193  			return a, err
   194  		}
   195  
   196  		// Check targets are valid
   197  		tgt := sets.New(a.Targets...)
   198  		known := sets.New[string]()
   199  		for _, img := range plan.Images {
   200  			known.Insert(img.Name)
   201  		}
   202  		if unknown := sets.SortedList(tgt.Difference(known)); len(unknown) > 0 {
   203  			return a, fmt.Errorf("unknown targets: %v", unknown)
   204  		}
   205  
   206  		// Filter down to requested targets
   207  		// This is not arch specific, so we can just let it run for each arch.
   208  		desiredImages := []ImagePlan{}
   209  		for _, i := range plan.Images {
   210  			canBuild := !i.EmulationRequired || (arch == LocalArch)
   211  			if tgt.Contains(i.Name) {
   212  				if !canBuild {
   213  					log.Infof("Skipping %s for %s as --qemu is not passed", i.Name, arch)
   214  					continue
   215  				}
   216  				desiredImages = append(desiredImages, i)
   217  			}
   218  		}
   219  		plan.Images = desiredImages
   220  
   221  		a.Plan[arch] = plan
   222  	}
   223  	return a, nil
   224  }
   225  
   226  // VerboseCommand runs a command, outputting stderr and stdout
   227  func VerboseCommand(name string, arg ...string) *exec.Cmd {
   228  	log.Infof("Running command: %v %v", name, strings.Join(arg, " "))
   229  	cmd := exec.Command(name, arg...)
   230  	cmd.Stderr = os.Stderr
   231  	cmd.Stdout = os.Stdout
   232  	return cmd
   233  }
   234  
   235  func StandardEnv(args Args) []string {
   236  	env := os.Environ()
   237  	if len(sets.New(args.Targets...).Delete("proxyv2")) <= 1 {
   238  		// If we are building multiple, it is faster to build all binaries in a single invocation
   239  		// Otherwise, build just the single item. proxyv2 is special since it is always built separately with tag=agent.
   240  		// Ideally we would just always build the targets we need but our Makefile is not that smart
   241  		env = append(env, "BUILD_ALL=false")
   242  	}
   243  
   244  	env = append(env,
   245  		// Build should already run in container, having multiple layers of docker causes issues
   246  		"BUILD_WITH_CONTAINER=0",
   247  	)
   248  	return env
   249  }
   250  
   251  var SkipMake = os.Getenv("SKIP_MAKE")
   252  
   253  // RunMake runs a make command for the repo, with standard environment variables set
   254  func RunMake(ctx context.Context, args Args, arch string, c ...string) error {
   255  	_, span := tracing.Start(ctx, "RunMake")
   256  	defer span.End()
   257  	if len(c) == 0 {
   258  		log.Infof("nothing to make")
   259  		return nil
   260  	}
   261  	if SkipMake == "true" {
   262  		return nil
   263  	}
   264  	shortArgs := []string{}
   265  	// Shorten output to avoid a ton of long redundant paths
   266  	for _, cs := range c {
   267  		shortArgs = append(shortArgs, filepath.Base(cs))
   268  	}
   269  	if len(c) == 0 {
   270  		log.Infof("Nothing to make")
   271  		return nil
   272  	}
   273  	log.Infof("Running make for %v: %v", arch, strings.Join(shortArgs, " "))
   274  	env := StandardEnv(args)
   275  	env = append(env, archToGoFlags(arch)...)
   276  	makeArgs := []string{"--no-print-directory"}
   277  	makeArgs = append(makeArgs, c...)
   278  	cmd := exec.Command("make", makeArgs...)
   279  	log.Infof("env: %v", archToGoFlags(arch))
   280  	cmd.Env = env
   281  	cmd.Stderr = os.Stderr
   282  	cmd.Stdout = os.Stdout
   283  	cmd.Dir = testenv.IstioSrc
   284  	if err := cmd.Run(); err != nil {
   285  		return err
   286  	}
   287  	return nil
   288  }
   289  
   290  func archToGoFlags(a string) []string {
   291  	s := []string{}
   292  	for k, v := range archToEnvMap(a) {
   293  		s = append(s, k+"="+v)
   294  	}
   295  	return s
   296  }
   297  
   298  func archToEnvMap(a string) map[string]string {
   299  	os, arch, _ := strings.Cut(a, "/")
   300  	return map[string]string{
   301  		"TARGET_OS":        os,
   302  		"TARGET_ARCH":      arch,
   303  		"TARGET_OUT":       filepath.Join(testenv.IstioSrc, "out", fmt.Sprintf("%s_%s", os, arch)),
   304  		"TARGET_OUT_LINUX": filepath.Join(testenv.IstioSrc, "out", fmt.Sprintf("linux_%s", arch)),
   305  	}
   306  }
   307  
   308  // RunCommand runs a command for the repo, with standard environment variables set
   309  func RunCommand(args Args, c string, cargs ...string) error {
   310  	cmd := VerboseCommand(c, cargs...)
   311  	cmd.Env = StandardEnv(args)
   312  	cmd.Stderr = os.Stderr
   313  	cmd.Stdout = os.Stdout
   314  	cmd.Dir = testenv.IstioSrc
   315  	return cmd.Run()
   316  }