versioning/scripts/cloudbuild/main.go (294 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" "os" "strings" "text/template" "github.com/GoogleCloudPlatform/runtimes-common/versioning/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 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 } // TODO(huyhg): Replace "gcr.io/$PROJECT_ID/functional_test" with gcp-runtimes one. const cloudBuildTemplateString = `steps: {{- $parallel := .Parallel }} {{- 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 images {{- range .ImageBuilds }} {{- if .Builder }} - name: gcr.io/cloud-builders/docker 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 }} - name: gcr.io/cloud-builders/docker args: - 'build' - '--tag={{ .Tag }}' - '{{ .Directory }}' {{- if $parallel }} waitFor: ['-'] id: 'image-{{ .Tag }}' {{- 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 }} {{- range $imageIndex, $image := .ImageBuilds }} {{- $primary := $image.Tag }} {{- range $testIndex, $test := $image.FunctionalTests }} {{- if and (eq $imageIndex 0) (eq $testIndex 0) }} # Run functional tests {{- end }} - name: gcr.io/$PROJECT_ID/functional_test args: - '--verbose' - '--vars' - 'IMAGE={{ $primary }}' - '--vars' - 'UNIQUE={{ $imageIndex }}-{{ $testIndex }}' - '--test_spec' - '{{ $test }}' {{- if $parallel }} waitFor: ['image-{{ $primary }}'] id: 'test-{{ $primary }}-{{ $testIndex }}' {{- end }} {{- end }} {{- end }} # Add alias tags {{- range $imageIndex, $image := .ImageBuilds }} {{- $primary := $image.Tag }} {{- range .Aliases }} - name: gcr.io/cloud-builders/docker args: - 'tag' - '{{ $primary }}' - '{{ . }}' {{- if $parallel }} waitFor: - 'image-{{ $primary }}' {{- range $testIndex, $test := $image.FunctionalTests }} - 'test-{{ $primary }}-{{ $testIndex }}' {{- end }} {{- end }} {{- end }} {{- end }} images: {{- range .AllImages }} - '{{ . }}' {{- end }} {{- if not (eq .TimeoutSeconds 0) }} timeout: {{ .TimeoutSeconds }}s {{- end }} {{- if $parallel }} options: machineType: 'N1_HIGHCPU_8' {{- 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 } type cloudBuildTemplateData struct { RequireNewTags bool Parallel bool ImageBuilds []imageBuildTemplateData AllImages []string TimeoutSeconds int } 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 // 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}) } else { data.ImageBuilds = append( data.ImageBuilds, imageBuildTemplateData{v.Dir, images[0], images[1:], versionSTests, versionFTests, v.Builder, v.BuilderImage, v.BuilderArgs, v.ImageNameFromBuilder}) } } data.TimeoutSeconds = options.TimeoutSeconds 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) tmpl, _ := template. New("cloudBuildTemplate"). 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.") enableParallel := config.BoolOption("enable_parallel", false, "Enable parallel build and bigger VM") forceParallel := config.BoolOption("force_parallel", false, "Force parallel build and bigger VM") 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, *enableParallel, *forceParallel} result := renderCloudBuildConfig(*registryPtr, spec, options) fmt.Println(result) }