tools/dockerversioning/scripts/cloudbuild/main.go (353 lines of code) (raw):

/* Command line tool for generating a Cloud Build yaml file based on versions.yaml. */ package main import ( "bytes" "fmt" "io/ioutil" "log" "math/rand" "os" "strings" "text/template" "github.com/GoogleCloudPlatform/click-to-deploy/tools/dockerversioning/versions" ) type cloudBuildOptions struct { // Whether to restrict to a particular set of Dockerfile directories. // If empty, all directories are used. Directories []string // Whether to run tests as part of the build. RunTests bool // Whether to require that image tags do not already exist in the repo. RequireNewTags bool // Whether to push to all declared tags FirstTagOnly bool // Optional timeout duration. If not specified, the Cloud Builder default timeout is used. TimeoutSeconds int // Optional machine type used to run the build, must be one of: N1_HIGHCPU_8, N1_HIGHCPU_32, E2_HIGHCPU_8, E2_HIGHCPU_32. If not specified, the default machine is used. MachineType string // Optional parallel build. If specified, images can be build on bigger machines in parallel. EnableParallel bool // Forces parallel build. If specified, images are build on bigger machines in parallel. Overrides EnableParallel. ForceParallel bool // Defines the reference for the docker Cloud Build builder (https://cloud.google.com/build/docs/cloud-builders#supported_builder_images_provided_by) DockerImage string } // TODO(huyhg): Replace "gcr.io/$PROJECT_ID/functional_test" with gcp-runtimes one. const cloudBuildTemplateString = `steps: {{- $parallel := .Parallel }} {{- $dockerImage := .DockerImage }} {{- if .RequireNewTags }} # Check if tags exist. {{- range .Images }} - name: gcr.io/gcp-runtimes/check_if_tag_exists args: - 'python' - '/main.py' - '--image={{ . }}' {{- end }} {{- end }} # Build and push annotated image - name: {{ $dockerImage }} args: - buildx - create - --name - temp-builder - --use waitFor: ['-'] id: docker-create-env - name: {{ $dockerImage }} args: - buildx - inspect - temp-builder - --bootstrap waitFor: ['docker-create-env'] id: docker-bootstrap-env # Build images {{- range .ImageBuilds }} {{- if .Builder }} - name: {{ $dockerImage }} args: - 'build' - '--tag={{ .Tag }}' - '{{ .Directory }}' {{- if $parallel }} waitFor: ['-'] id: 'image-{{ .Tag }}' {{- end }} {{- else }} {{- if .BuilderImage }} - name: {{ .BuilderImage }} args: {{ .BuilderArgs }} {{- if $parallel }} waitFor: ['image-{{ .BuilderImage }}'] id: 'image-{{ .Tag }}' {{- end }} {{- else }} {{- $testCounter := 0 }} {{- $primary := .Tag }} # Build test target image: {{ $primary }} - name: {{ $dockerImage }} args: - 'build' - '-t' - '{{ $primary }}' - '{{ .Directory }}' id: image-test-{{ $primary }} {{- if $parallel }} waitFor: ['docker-bootstrap-env'] {{- end }} {{- range $testIndex, $test := .StructureTests }} # Run structure test: {{ $primary }} - name: gcr.io/gcp-runtimes/structure_test args: - '--image' - '{{ $primary }}' - '--config' - '{{ $test }}' waitFor: ['image-test-{{ $primary }}'] id: 'structure-test-{{ $primary }}-{{ $testIndex }}' {{ end }} {{- range $testIndex, $test := .FunctionalTests }} # Run functional test: {{ $primary }} - name: gcr.io/$PROJECT_ID/functional_test args: - '--verbose' - '--vars' - 'IMAGE={{ $primary }}' - '--vars' - 'UNIQUE={{ randomString 8 }}' - '--test_spec' - '{{ $test }}' waitFor: ['image-test-{{ $primary }}'] id: 'functional-test-{{ $primary }}-{{ $testIndex }}' {{- end }} - name: {{ $dockerImage }} args: - 'buildx' - 'build' - '--push' {{- range .Aliases }} - '--tag' - '{{ . }}' {{- end }} {{- range .Annotations }} - '--annotation=index,manifest:{{ .Key }}={{ .Value }}' {{- end }} {{- range .Labels }} - '--label={{ .Key }}={{ .Value }}' {{- end }} - '{{ .Directory }}' id: build-and-push-image-{{ $primary }} waitFor: {{- range $testIndex, $test := .StructureTests }} - 'structure-test-{{ $primary }}-{{ $testIndex }}' {{- end}} {{- range $testIndex, $test := .FunctionalTests }} - 'functional-test-{{ $primary }}-{{ $testIndex }}' {{- end}} {{- end }} {{- end }} {{- end }} {{- range $imageIndex, $image := .ImageBuilds }} {{- $primary := $image.Tag }} {{- range $testIndex, $test := $image.StructureTests }} {{- if and (eq $imageIndex 0) (eq $testIndex 0) }} # Run structure tests {{- end}} - name: gcr.io/gcp-runtimes/structure_test args: - '--image' - '{{ $primary }}' - '--config' - '{{ $test }}' {{- end }} {{- end }} {{- if not (eq .TimeoutSeconds 0) }} timeout: {{ .TimeoutSeconds }}s {{- end }} {{- if $parallel }} options: machineType: 'E2_HIGHCPU_8' {{- else }} {{- if .MachineType }} options: machineType: '{{ .MachineType }}' {{- end }} {{- end }} ` const testsDir = "tests" const functionalTestsDir = "tests/functional_tests" const structureTestsDir = "tests/structure_tests" const testJsonSuffix = "_test.json" const testYamlSuffix = "_test.yaml" const workspacePrefix = "/workspace/" type imageBuildTemplateData struct { Directory string Tag string Aliases []string StructureTests []string FunctionalTests []string Builder bool BuilderImage string BuilderArgs []string ImageNameFromBuilder string Annotations []versions.Annotation Labels []versions.Annotation } type cloudBuildTemplateData struct { RequireNewTags bool Parallel bool DockerImage string ImageBuilds []imageBuildTemplateData AllImages []string TimeoutSeconds int MachineType string } func shouldParallelize(options cloudBuildOptions, numberOfVersions int, numberOfTests int) bool { if options.ForceParallel { return true } if !options.EnableParallel { return false } return numberOfVersions > 1 || numberOfTests > 1 } func newCloudBuildTemplateData( registry string, spec versions.Spec, options cloudBuildOptions) cloudBuildTemplateData { data := cloudBuildTemplateData{} data.RequireNewTags = options.RequireNewTags // Defines the default docker image, if its not set if (options.DockerImage == "") { data.DockerImage = "gcr.io/cloud-builders/docker" } else { data.DockerImage = options.DockerImage } // Determine the set of directories to operate on. dirs := make(map[string]bool) if len(options.Directories) > 0 { for _, d := range options.Directories { dirs[d] = true } } else { for _, v := range spec.Versions { dirs[v.Dir] = true } } // Extract tests to run. var structureTests []string var functionalTests []string if options.RunTests { // Legacy structure tests reside in the root tests/ directory. structureTests = append(structureTests, readTests(testsDir)...) structureTests = append(structureTests, readTests(structureTestsDir)...) functionalTests = append(functionalTests, readTests(functionalTestsDir)...) } // Extract a list of full image names to build. for _, v := range spec.Versions { if !dirs[v.Dir] { continue } var images []string for _, t := range v.Tags { image := fmt.Sprintf("%v/%v:%v", registry, v.Repo, t) images = append(images, image) if options.FirstTagOnly { break } } // Ignore builder images from images list if !v.Builder { data.AllImages = append(data.AllImages, images...) } versionSTests, versionFTests := filterTests(structureTests, functionalTests, v) // Enforce to use ImageNameFromBuilder as reference to create tags if v.BuilderImage != "" { BuilderImageFull := fmt.Sprintf("%v/%v", registry, v.BuilderImage) data.ImageBuilds = append( data.ImageBuilds, imageBuildTemplateData{v.Dir, v.ImageNameFromBuilder, images, versionSTests, versionFTests, v.Builder, BuilderImageFull, v.BuilderArgs, v.ImageNameFromBuilder, v.Annotations, v.Labels}) } else { data.ImageBuilds = append( data.ImageBuilds, imageBuildTemplateData{v.Dir, images[0], images[1:], versionSTests, versionFTests, v.Builder, v.BuilderImage, v.BuilderArgs, v.ImageNameFromBuilder, v.Annotations, v.Labels}) } } data.TimeoutSeconds = options.TimeoutSeconds data.MachineType = options.MachineType data.Parallel = shouldParallelize(options, len(spec.Versions), len(functionalTests)) return data } func readTests(testsDir string) (tests []string) { if info, err := os.Stat(testsDir); err == nil && info.IsDir() { files, err := ioutil.ReadDir(testsDir) check(err) for _, f := range files { if f.IsDir() { continue } if strings.HasSuffix(f.Name(), testJsonSuffix) || strings.HasSuffix(f.Name(), testYamlSuffix) { tests = append(tests, workspacePrefix+fmt.Sprintf("%s/%s", testsDir, f.Name())) } } } return } func filterTests(structureTests []string, functionalTests []string, version versions.Version) (outStructureTests []string, outFunctionalTests []string) { included := make(map[string]bool, len(structureTests)+len(functionalTests)) for _, test := range append(structureTests, functionalTests...) { included[test] = true } for _, excluded := range version.ExcludeTests { if !included[workspacePrefix+excluded] { log.Fatalf("No such test to exclude: %s", excluded) } included[workspacePrefix+excluded] = false } outStructureTests = make([]string, 0, len(structureTests)) for _, test := range structureTests { if included[test] { outStructureTests = append(outStructureTests, test) } } outFunctionalTests = make([]string, 0, len(functionalTests)) for _, test := range functionalTests { if included[test] { outFunctionalTests = append(outFunctionalTests, test) } } return } func renderCloudBuildConfig( registry string, spec versions.Spec, options cloudBuildOptions) string { data := newCloudBuildTemplateData(registry, spec, options) funcMap := template.FuncMap{ "randomString": func(length int) string { bytes := make([]byte, length) for i := 0; i < length; i++ { bytes[i] = byte(rand.Intn(26) + 'a') } return string(bytes) }, } tmpl, _ := template. New("cloudBuildTemplate"). Funcs(funcMap). Parse(cloudBuildTemplateString) var result bytes.Buffer tmpl.Execute(&result, data) return result.String() } func check(e error) { if e != nil { panic(e) } } func main() { config := versions.LoadConfig("versions.yaml", "cloudbuild") registryPtr := config.StringOption("registry", "gcr.io/$PROJECT_ID", "Registry, e.g: 'gcr.io/my-project'") dirsPtr := config.StringOption("dirs", "", "Comma separated list of Dockerfile dirs to use.") testsPtr := config.BoolOption("tests", true, "Run tests.") newTagsPtr := config.BoolOption("new_tags", false, "Require that image tags do not already exist.") firstTagOnly := config.BoolOption("first_tag", false, "Build only the first per version.") timeoutPtr := config.IntOption("timeout", 0, "Timeout in seconds. If not set, the default Cloud Build timeout is used.") machineTypePtr := config.StringOption("machineType","", "Optional machine type used to run the build, , must be one of: N1_HIGHCPU_8, N1_HIGHCPU_32, E2_HIGHCPU_8, E2_HIGHCPU_32. If not specified, the default machine is used.") enableParallel := config.BoolOption("enable_parallel", false, "Enable parallel build and bigger VM") forceParallel := config.BoolOption("force_parallel", false, "Force parallel build and bigger VM") dockerImage := config.StringOption("docker_image", "gcr.io/cloud-builders/docker", "Optional docker builder reference") config.Parse() if *registryPtr == "" { log.Fatalf("--registry flag is required") } if strings.Contains(*registryPtr, ":") { *registryPtr = strings.Replace(*registryPtr, ":", "/", 1) } var dirs []string if *dirsPtr != "" { dirs = strings.Split(*dirsPtr, ",") } spec := versions.LoadVersions("versions.yaml") options := cloudBuildOptions{dirs, *testsPtr, *newTagsPtr, *firstTagOnly, *timeoutPtr, *machineTypePtr, *enableParallel, *forceParallel, *dockerImage} result := renderCloudBuildConfig(*registryPtr, spec, options) fmt.Println(result) }