vhdbuilder/release-notes/autonotes/main.go (255 lines of code) (raw):

package main import ( "context" "flag" "fmt" "os" "os/exec" "os/signal" "path/filepath" "strings" "time" "unicode" ) /** *** This binary autogenerates release notes for AKS VHD releases. *** *** It accepts: *** - a run ID from which to download artifacts. *** - the VHD build date for output naming *** - a comma-separated list of VHD names to include/ignore. *** *** Examples: *** # download ONLY 1804-gen2-gpu release notes from this run ID. *** autonotes --build 40968951 --include 1804-gen2-gpu *** *** # download everything EXCEPT 1804-gen2-gpu release notes from this run ID. *** autonotes --build 40968951 --ignore 1804-gen2-gpu *** *** # download ONLY 1604,1804,1804-containerd release notes from this run ID. *** autonotes --build 40968951 --include 1604,1804,1804-containerd *** # download ONLY 2019-containerd release notes from this run ID. *** autonotes --build 76289801 --include 2019-containerd *** *** # download everything EXCEPT 2022-containerd-gen2 release notes from this run ID. *** autonotes --build 76289801 --ignore 2022-containerd-gen2 *** *** # download ONLY 2022-containerd,2022-containerd-gen2 release notes from this run ID. *** autonotes --build 76289801 --include 2022-containerd,2022-containerd-gen2 **/ type VhdPublishingInfo struct { VhdUrl string `json:"vhd_url"` OsName string `json:"os_name"` SkuName string `json:"sku_name"` OfferName string `json:"offer_name"` HypervGeneration string `json:"hyperv_generation"` ImageArchitecture string `json:"image_architecture"` ImageVersion string `json:"image_version"` } func main() { var fl flags flag.StringVar(&fl.build, "build", "", "run ID for the VHD build.") flag.StringVar(&fl.include, "include", "", "only include this list of VHD release notes.") flag.StringVar(&fl.ignore, "ignore", "", "ignore release notes for these VHDs") flag.StringVar(&fl.path, "path", defaultPath, "output path to root of VHD notes") flag.StringVar(&fl.date, "date", defaultDate, "date of VHD build in format YYYYMM.DD.0") flag.BoolVar(&fl.skipLatest, "skip-latest", false, "if set, skip creating/updating the latest version of each release artifact for each SKU") flag.Parse() int := make(chan os.Signal, 1) signal.Notify(int, os.Interrupt) ctx, cancel := context.WithCancel(context.Background()) go func() { <-int; cancel() }() if errs := run(ctx, cancel, &fl); errs != nil { for _, err := range errs { fmt.Println(err) } os.Exit(1) } } func run(ctx context.Context, cancel context.CancelFunc, fl *flags) []error { var include, ignore map[string]bool includeString := stripWhitespace(fl.include) if len(includeString) > 0 { include = map[string]bool{} includeTokens := strings.Split(includeString, ",") for _, token := range includeTokens { include[token] = true } } ignoreString := stripWhitespace(fl.ignore) if len(ignoreString) > 0 { ignore = map[string]bool{} ignoreTokens := strings.Split(ignoreString, ",") for _, token := range ignoreTokens { ignore[token] = true } } enforceInclude := len(include) > 0 artifactsToDownload := map[string]string{} fmt.Printf("\n") for key, value := range artifactToPath { if ignore[key] { fmt.Printf("Ignoring as artifact explicitly excluded \"%s\" with path \"%s\"\n", key, value) continue } if enforceInclude && !include[key] { fmt.Printf("Ignoring as not artifact not explicitly included \"%s\" with path \"%s\"\n", key, value) continue } artifactsToDownload[key] = value } fmt.Printf("\n") for sku, path := range artifactsToDownload { fmt.Printf("Including artifact \"%s\" with path \"%s\"\n", sku, path) } var errs []error // In theory, this could be done in parallel using a goroutine. // In practice, the "az" command breaks if you call it in parallel. for sku, path := range artifactsToDownload { if strings.Contains(path, "AKSWindows") { err := getReleaseNotesWindows(sku, path, fl) if err != nil { errs = append(errs, err) } } else { err := getReleaseNotes(sku, path, fl) if err != nil { errs = append(errs, err) } } } return errs } func getReleaseNotes(sku, path string, fl *flags) error { // working directory, need one per sku because the file name is // always "release-notes.txt" so they all overwrite each other. tmpdir, err := os.MkdirTemp("", "releasenotes") if err != nil { return fmt.Errorf("failed to create temp working directory: %w", err) } defer os.RemoveAll(tmpdir) artifactsDirOut := filepath.Join(fl.path, path) if err := os.MkdirAll(filepath.Dir(artifactsDirOut), 0644); err != nil { return fmt.Errorf("failed to create parent directory %s with error: %s", artifactsDirOut, err) } if err := os.MkdirAll(artifactsDirOut, 0644); err != nil { return fmt.Errorf("failed to create parent directory %s with error: %s", artifactsDirOut, err) } artifacts := []buildArtifact{ { name: fmt.Sprintf("vhd-release-notes-%s", sku), tempName: "release-notes.txt", outName: fmt.Sprintf("%s.txt", fl.date), latestName: "latest.txt", }, { name: fmt.Sprintf("vhd-image-bom-%s", sku), tempName: "image-bom.json", outName: fmt.Sprintf("%s-image-list.json", fl.date), latestName: "latest-image-list.json", }, } for _, artifact := range artifacts { if err := artifact.process(fl, artifactsDirOut, tmpdir); err != nil { fmt.Printf("processing artifact %s for sku %s", artifact.name, sku) return fmt.Errorf("failed to process VHD build artifact %s: %w", artifact.name, err) } } return nil } func getReleaseNotesWindows(sku, path string, fl *flags) error { releaseNotesName := fmt.Sprintf("vhd-release-notes-%s", sku) imageListName := fmt.Sprintf("vhd-image-list-%s", sku) artifactsDirOut := filepath.Join(fl.path, path) parentDirectory := filepath.Dir(artifactsDirOut) fmt.Printf("\n") fmt.Printf("Creating parent directory for sku'%s': '%s'\n", sku, parentDirectory) if err := os.MkdirAll(parentDirectory, 0644); err != nil { return fmt.Errorf("failed to create parent directory %s with error: %s", artifactsDirOut, err) } fmt.Printf("Creating directory for sku '%s': '%s'\n", sku, artifactsDirOut) if err := os.MkdirAll(artifactsDirOut, 0644); err != nil { return fmt.Errorf("failed to create directory %s with error: %s", artifactsDirOut, err) } fmt.Printf("downloading releaseNotes '%s' from windows build '%s'\n", releaseNotesName, fl.build) cmd := exec.Command("az", "pipelines", "runs", "artifact", "download", "--run-id", fl.build, "--path", artifactsDirOut, "--artifact-name", releaseNotesName) if stdout, err := cmd.CombinedOutput(); err != nil { fmt.Printf("Failed downloading releaseNotes '%s' from windows build '%s'\n", releaseNotesName, fl.build) return fmt.Errorf("failed to download az devops releaseNotes for sku %s, err: %s, output: %s", sku, err, string(stdout)) } fmt.Printf("downloading imageList '%s' from build '%s'\n", imageListName, fl.build) cmd = exec.Command("az", "pipelines", "runs", "artifact", "download", "--run-id", fl.build, "--path", artifactsDirOut, "--artifact-name", imageListName) if stdout, err := cmd.CombinedOutput(); err != nil { fmt.Printf("failed downloading imageList '%s' from windows build '%s'\n", imageListName, fl.build) return fmt.Errorf("failed to download az devops imageList for sku %s, err: %s, output: %s", sku, err, string(stdout)) } return nil } func stripWhitespace(str string) string { var b strings.Builder b.Grow(len(str)) for _, ch := range str { if !unicode.IsSpace(ch) { b.WriteRune(ch) } } return b.String() } type buildArtifact struct { // name is the name of the artifact used to download from ADO name string // tempName is the name of the actual file contained within the artifact bundle we want to extract tempName string // outName is the versioned name of the artifact file to be uploaded outName string // latestName is the latest name of the artifact file to be uploaded latestName string } func (a buildArtifact) process(fl *flags, outdir, tmpdir string) error { tempPath := filepath.Join(tmpdir, a.tempName) outPath := filepath.Join(outdir, a.outName) cmd := exec.Command("az", "pipelines", "runs", "artifact", "download", "--run-id", fl.build, "--path", tmpdir, "--artifact-name", a.name) if stdout, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("failed to download az devops releaseNotes %s, err: %s, output: %s", a.name, err, string(stdout)) } if err := os.Rename(tempPath, outPath); err != nil { return fmt.Errorf("failed to rename file %s to %s, err: %s", tempPath, outPath, err) } if !fl.skipLatest { data, err := os.ReadFile(outPath) if err != nil { return fmt.Errorf("failed to read file %s for copying, err: %s", outPath, err) } latestPath := filepath.Join(outdir, a.latestName) if err = os.WriteFile(latestPath, data, 0644); err != nil { return fmt.Errorf("failed to copy data from file %s to latest version %s, err: %s", outPath, latestPath, err) } } return nil } type flags struct { build string include string // CSV of the map keys below. ignore string // CSV of the map keys below. path string // output path date string // date of vhd build skipLatest bool // whether to skip creating/updating latest version of each artifact for each SKU } var defaultPath = filepath.Join("vhdbuilder", "release-notes") var defaultDate = strings.Split(time.Now().Format("200601.02"), " ")[0] + ".0" // why does ubuntu use subfolders and mariner doesn't // there are dependencies on the folder structure but it would // be nice to fix this. var artifactToPath = map[string]string{ "1804-containerd": filepath.Join("AKSUbuntu", "gen1", "1804containerd"), "1804-gen2-containerd": filepath.Join("AKSUbuntu", "gen2", "1804containerd"), "1804-gpu-containerd": filepath.Join("AKSUbuntu", "gen1", "1804gpucontainerd"), "1804-gen2-gpu-containerd": filepath.Join("AKSUbuntu", "gen2", "1804gpucontainerd"), "1804-fips-containerd": filepath.Join("AKSUbuntu", "gen1", "1804fipscontainerd"), "1804-fips-gen2-containerd": filepath.Join("AKSUbuntu", "gen2", "1804fipscontainerd"), "2004-fips-containerd": filepath.Join("AKSUbuntu", "gen1", "2004fipscontainerd"), "2004-fips-gen2-containerd": filepath.Join("AKSUbuntu", "gen2", "2004fipscontainerd"), "marinerv1": filepath.Join("AKSCBLMariner", "gen1"), "marinerv1-gen2": filepath.Join("AKSCBLMariner", "gen2"), "marinerv2-gen1": filepath.Join("AKSCBLMarinerV2", "gen1"), "marinerv2-gen1-fips": filepath.Join("AKSCBLMarinerV2", "gen1fips"), "marinerv2-gen2-fips": filepath.Join("AKSCBLMarinerV2", "gen2fips"), "marinerv2-gen2": filepath.Join("AKSCBLMarinerV2", "gen2"), "marinerv2-gen2-kata": filepath.Join("AKSCBLMarinerV2", "gen2kata"), "marinerv2-gen2-arm64": filepath.Join("AKSCBLMarinerV2", "gen2arm64"), "marinerv2-gen2-trustedlaunch": filepath.Join("AKSCBLMarinerV2", "gen2tl"), "marinerv2-gen2-kata-trustedlaunch": filepath.Join("AKSCBLMarinerV2", "gen2katatl"), "2004-cvm-gen2-containerd": filepath.Join("AKSUbuntu", "gen2", "2004cvmcontainerd"), "2204-containerd": filepath.Join("AKSUbuntu", "gen1", "2204containerd"), "2204-gen2-containerd": filepath.Join("AKSUbuntu", "gen2", "2204containerd"), "2204-arm64-gen2-containerd": filepath.Join("AKSUbuntu", "gen2", "2204arm64containerd"), "2204-tl-gen2-containerd": filepath.Join("AKSUbuntu", "gen2", "2204tlcontainerd"), "2404-containerd": filepath.Join("AKSUbuntu", "gen1", "2404containerd"), "2404-gen2-containerd": filepath.Join("AKSUbuntu", "gen2", "2404containerd"), "2404-arm64-gen2-containerd": filepath.Join("AKSUbuntu", "gen2", "2404arm64containerd"), "2404-tl-gen2-containerd": filepath.Join("AKSUbuntu", "gen2", "2404tlcontainerd"), "2019-containerd": filepath.Join("AKSWindows", "2019-containerd"), "2022-containerd": filepath.Join("AKSWindows", "2022-containerd"), "2022-containerd-gen2": filepath.Join("AKSWindows", "2022-containerd-gen2"), "23H2": filepath.Join("AKSWindows", "23H2"), "23H2-gen2": filepath.Join("AKSWindows", "23H2-gen2"), "2025": filepath.Join("AKSWindows", "2025"), "2025-gen2": filepath.Join("AKSWindows", "2025-gen2"), "azurelinuxv2-gen1": filepath.Join("AKSAzureLinux", "gen1"), "azurelinuxv2-gen2": filepath.Join("AKSAzureLinux", "gen2"), "azurelinuxv2-gen1-fips": filepath.Join("AKSAzureLinux", "gen1fips"), "azurelinuxv2-gen2-fips": filepath.Join("AKSAzureLinux", "gen2fips"), "azurelinuxv2-gen2-kata": filepath.Join("AKSAzureLinux", "gen2kata"), "azurelinuxv2-gen2-arm64": filepath.Join("AKSAzureLinux", "gen2arm64"), "azurelinuxv2-gen2-trustedlaunch": filepath.Join("AKSAzureLinux", "gen2tl"), "azurelinuxv2-gen2-kata-trustedlaunch": filepath.Join("AKSAzureLinux", "gen2katatl"), "azurelinuxv3-gen1": filepath.Join("AKSAzureLinuxV3", "gen1"), "azurelinuxv3-gen2": filepath.Join("AKSAzureLinuxV3", "gen2"), "azurelinuxv3-gen1-fips": filepath.Join("AKSAzureLinuxV3", "gen1fips"), "azurelinuxv3-gen2-fips": filepath.Join("AKSAzureLinuxV3", "gen2fips"), "azurelinuxv3-gen2-arm64": filepath.Join("AKSAzureLinuxV3", "gen2arm64"), "azurelinuxv3-gen2-trustedlaunch": filepath.Join("AKSAzureLinuxV3", "gen2tl"), }