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 }