1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
18 "github.com/arvados/arvados-dev/compute-image-cleaner/config"
20 "github.com/Azure/azure-pipeline-go/pipeline"
21 "github.com/Azure/azure-storage-blob-go/azblob"
23 "code.cloudfoundry.org/bytefmt"
30 deletionCandidate bool
33 func prepAzBlob(storageKey string, account string, container string) (p pipeline.Pipeline, containerURL azblob.ContainerURL) {
34 // Create a default request pipeline using your storage account name and account key.
35 credential, err := azblob.NewSharedKeyCredential(account, storageKey)
37 log.Fatal("Invalid credentials with error: " + err.Error())
39 p = azblob.NewPipeline(credential, azblob.PipelineOptions{})
40 // From the Azure portal, get your storage account blob service URL endpoint.
41 URL, _ := url.Parse(fmt.Sprintf("https://%s.blob.core.windows.net/%s", account, container))
43 // Create a ContainerURL object that wraps the container URL and a request
44 // pipeline to make requests.
45 containerURL = azblob.NewContainerURL(*URL, p)
50 func loadBlobs(p pipeline.Pipeline, containerURL azblob.ContainerURL) (blobs []blob, blobNames map[string]*blob) {
51 blobNames = make(map[string]*blob)
53 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
56 for marker := (azblob.Marker{}); marker.NotDone(); {
57 // Get a result segment starting with the blob indicated by the current Marker.
58 listBlob, err := containerURL.ListBlobsFlatSegment(ctx, marker, azblob.ListBlobsSegmentOptions{})
60 log.Fatal("Error getting blob list: " + err.Error())
63 // ListBlobs returns the start of the next segment; you MUST use this to get
64 // the next segment (after processing the current result segment).
65 marker = listBlob.NextMarker
67 // Process the blobs returned in this result segment (if the segment is empty, the loop body won't execute)
68 for _, blobInfo := range listBlob.Segment.BlobItems {
69 blobs = append(blobs, blob{name: blobInfo.Name, created: *blobInfo.Properties.CreationTime, contentLength: *blobInfo.Properties.ContentLength})
70 blobNames[blobInfo.Name] = &blobs[len(blobs)-1]
73 sort.Slice(blobs, func(i, j int) bool { return blobs[i].created.After(blobs[j].created) })
78 func weedBlobs(blobs []blob, blobNames map[string]*blob, containerURL azblob.ContainerURL, account string, container string, doIt bool) {
79 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
82 var pairedFileName string
85 thirtyDaysAgo := t.AddDate(0, 0, -30)
87 // e.g. su92l-compute-osDisk.866eb426-8d1e-45ad-91be-2bb55b5a8147.vhd
88 vhd := regexp.MustCompile(`^(.*)-compute-osDisk\.(.*)\.vhd$`)
89 // e.g. su92l-compute-vmTemplate.866eb426-8d1e-45ad-91be-2bb55b5a8147.json
90 json := regexp.MustCompile(`^(.*)-compute-vmTemplate\.(.*)\.json$`)
92 for i, blob := range blobs {
93 matches := vhd.FindStringSubmatch(blob.name)
96 pairedFileName = matches[1] + "-compute-vmTemplate." + matches[2] + ".json"
98 matches := json.FindStringSubmatch(blob.name)
101 pairedFileName = matches[1] + "-compute-osDisk." + matches[2] + ".vhd"
103 log.Println("Skipping blob because name does not match a known file name pattern:", blob.name, " ", blob.created)
107 if blob.created.After(thirtyDaysAgo) {
108 log.Println("Skipping blob because it was created less than 30 days ago:", blob.name, " ", blob.created)
109 skipCount = skipCount - 1
113 log.Println("Skipping blob because it's in the top 10 most recent list:", blob.name, " ", blob.created)
114 skipCount = skipCount - 1
117 if _, ok := blobNames[pairedFileName]; !ok {
118 log.Println("Warning: paired file", pairedFileName, "not found for blob", blob.name, " ", blob.created)
120 blobs[i].deletionCandidate = true
123 var reclaimedSpace, otherSpace int64
125 for _, blob := range blobs {
126 if blob.deletionCandidate {
127 log.Println("Candidate for deletion:", blob.name, " ", blob.created)
128 reclaimedSpace = reclaimedSpace + blob.contentLength
131 log.Println("Deleting:", blob.name, " ", blob.created)
132 blockBlobURL := containerURL.NewBlockBlobURL(blob.name)
133 result, err := blockBlobURL.Delete(ctx, azblob.DeleteSnapshotsOptionInclude, azblob.BlobAccessConditions{})
136 log.Fatal("Error deleting blob: ", err.Error(), "\n", result)
140 otherSpace = otherSpace + blob.contentLength
145 log.Println("Reclaimed", bytefmt.ByteSize(uint64(reclaimedSpace)), "or", reclaimedSpace, "bytes.")
147 log.Println("Deletion not requested. Able to reclaim", bytefmt.ByteSize(uint64(reclaimedSpace)), "or", reclaimedSpace, "bytes.")
149 log.Println("Kept", bytefmt.ByteSize(uint64(otherSpace)), "or", otherSpace, "bytes.")
153 func loadStorageAccountKey(resourceGroup string, account string) (key string) {
154 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
157 storageClient := getStorageAccountsClient()
158 keys, err := storageClient.ListKeys(ctx, resourceGroup, account)
160 log.Fatal("Error getting storage account key:", err.Error())
163 key = *(*keys.Keys)[0].Value
168 func validateInputs() (resourceGroup string, account string, container string, doIt bool) {
169 err := config.ParseEnvironment()
171 log.Fatal("Unable to parse environment")
174 if config.ClientID() == "" || config.ClientSecret() == "" || config.TenantID() == "" || config.SubscriptionID() == "" {
175 log.Fatal("Please make sure the environment variables AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID and AZURE_SUBSCRIPTION_ID are set")
178 flags := flag.NewFlagSet("compute-image-cleaner", flag.ExitOnError)
179 flags.StringVar(&resourceGroup, "resourceGroup", "", "Name of the Azure resource group")
180 flags.StringVar(&account, "account", "", "Name of the Azure storage account")
181 flags.StringVar(&container, "container", "", "Name of the container in the Azure storage account")
182 flags.BoolVar(&doIt, "delete", false, "Delete blobs that meet criteria (default: false)")
183 flags.Usage = func() { usage(flags) }
184 err = flags.Parse(os.Args[1:])
186 if err != nil || resourceGroup == "" || account == "" || container == "" {
195 resourceGroup, account, container, doIt := validateInputs()
196 storageKey := loadStorageAccountKey(resourceGroup, account)
197 p, containerURL := prepAzBlob(storageKey, account, container)
199 blobs, blobNames := loadBlobs(p, containerURL)
200 weedBlobs(blobs, blobNames, containerURL, account, container, doIt)