internal/cleanaeversions/cleanaeversions.go (135 lines of code) (raw):
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Command cleaneversions deletes App Engine versions for a given project, service and/or version ID filter.
//
// Usage of cleanaeversions:
// -async
// Don't wait for successful deletion.
// -filter regexp
// Filter regexp for version IDs. If empty, attempts to clean all versions.
// -n Dry run.
// -project Project ID
// Project ID to clean.
// -service Service/module ID
// Service/module ID to clean. If omitted, cleans all services.
package main
import (
"context"
"flag"
"fmt"
"log"
"math/rand"
"os"
"regexp"
"strings"
"sync"
"sync/atomic"
"time"
"golang.org/x/oauth2/google"
appengine "google.golang.org/api/appengine/v1"
)
var (
proj = flag.String("project", "", "`Project ID` to clean.")
service = flag.String("service", "", "`Service/module ID` to clean. If omitted, cleans all services.")
filter = flag.String("filter", "", "Filter `regexp` for version IDs. If empty, attempts to clean all versions.")
async = flag.Bool("async", false, "Don't wait for successful deletion.")
dryRun = flag.Bool("n", false, "Dry run.")
)
var gae *appengine.APIService
type pendingDelete struct {
service string
version string
op *appengine.Operation
}
func main() {
flag.Parse()
if *proj == "" {
fmt.Fprintln(os.Stderr, "-project flag is required")
flag.Usage()
os.Exit(2)
}
filterRE, err := regexp.Compile(*filter)
if err != nil {
fmt.Fprintf(os.Stderr, "Filter is not a valid regexp: %v", err)
os.Exit(2)
}
_ = filterRE
ctx := context.Background()
hc, err := google.DefaultClient(ctx, appengine.CloudPlatformScope)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not create DefaultClient: %v", err)
os.Exit(1)
}
gae, err = appengine.New(hc)
if err != nil {
fmt.Fprintf(os.Stderr, "Could not create App Engine service: %v", err)
os.Exit(1)
}
var services []string
if *service != "" {
services = append(services, *service)
} else {
if err := gae.Apps.Services.List(*proj).Pages(ctx, func(lsr *appengine.ListServicesResponse) error {
for _, s := range lsr.Services {
services = append(services, s.Id)
}
return nil
}); err != nil {
fmt.Fprintf(os.Stderr, "Could not list App Engine services: %v", err)
os.Exit(1)
}
}
var pending []pendingDelete
for _, service := range services {
if err := gae.Apps.Services.Versions.List(*proj, service).Pages(ctx, func(lvr *appengine.ListVersionsResponse) error {
for _, v := range lvr.Versions {
if !filterRE.MatchString(v.Id) {
continue
}
log.Printf("Deleting %s/%s", service, v.Id)
if *dryRun {
continue
}
op, err := gae.Apps.Services.Versions.Delete(*proj, service, v.Id).Do()
if err != nil {
log.Printf("Could not delete version %s/%s: %v\n", service, v.Id, err)
} else {
pending = append(pending, pendingDelete{service: service, version: v.Id, op: op})
}
}
return nil
}); err != nil {
fmt.Fprintf(os.Stderr, "Could not list versions for %q: %v\n", service, err)
os.Exit(1)
}
}
if *async {
log.Printf("Not waiting for operations to complete. Exiting.")
os.Exit(0)
}
log.Printf("Waiting for operations to complete.")
var failed int64
var wg sync.WaitGroup
wg.Add(len(pending))
for _, pd := range pending {
pd := pd
go func() {
if err := waitForCompletion(pd); err != nil {
log.Printf("FAILED %v/%v/%v: %v", *proj, pd.service, pd.version, err)
atomic.AddInt64(&failed, 1)
} else {
log.Printf("Deleted %v/%v/%v", *proj, pd.service, pd.version)
}
wg.Done()
}()
}
wg.Wait()
if failed != 0 {
log.Printf("FAILED (%d)", failed)
os.Exit(1)
}
}
func waitForCompletion(pd pendingDelete) error {
parts := strings.Split(pd.op.Name, "/")
id := parts[len(parts)-1]
for {
op, err := gae.Apps.Operations.Get(*proj, id).Do()
if err != nil {
return err
}
if !op.Done {
// 5 to 10 second sleep.
time.Sleep(time.Duration(5+rand.Float64()*5) * time.Second)
continue
}
if op.Error == nil {
return nil
}
return fmt.Errorf("%s (code %d)", op.Error.Message, op.Error.Code)
}
}