testing/gimmeproj/main.go (273 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 gimmeproj provides access to a pool of projects. // // The metadata about the project pool is stored in Cloud Datastore in a meta-project. // Projects are leased for a certain duration, and automatically returned to the pool when the lease expires. // Projects should be returned before the lease expires. package main import ( "context" "errors" "flag" "fmt" "log" "os" "runtime/debug" "time" ds "cloud.google.com/go/datastore" ) var ( metaProject = flag.String("project", "", "Meta-project that manages the pool.") format = flag.String("output", "", "Output format for selected operations. Options include: list") waitTime = flag.Duration("timeout", 30*time.Minute, "maximum wait time for leasing a project") datastore *ds.Client version = "dev" buildSource = "unknown" buildDate = "unknown" ErrNoProjects = errors.New("could not find a free project") ) type Pool struct { Projects []Project } type Project struct { ID string LeaseExpiry time.Time } func (p *Pool) Get(projID string) (*Project, bool) { for i := range p.Projects { proj := &p.Projects[i] if proj.ID == projID { return proj, true } } return nil, false } func (p *Pool) Add(proj string) (ok bool) { if _, ok := p.Get(proj); ok { return false } p.Projects = append(p.Projects, Project{ID: proj}) return true } func (p *Pool) Lease(d time.Duration) (*Project, bool) { if len(p.Projects) == 0 { return nil, false } oldest := &p.Projects[0] for i := range p.Projects { proj := &p.Projects[i] if proj.LeaseExpiry.Before(oldest.LeaseExpiry) { oldest = proj } } if !oldest.Expired() { return nil, false } oldest.LeaseExpiry = time.Now().Add(d) return oldest, true } func (p *Project) Expired() bool { return time.Now().After(p.LeaseExpiry) } func startup() { // set version info from embedded details. if bi, ok := debug.ReadBuildInfo(); ok { packagename := bi.Main.Path for _, s := range bi.Settings { switch s.Key { case "vcs": buildSource = fmt.Sprintf("%s://%s", s.Value, packagename) case "vcs.revision": version = s.Value case "vcs.time": buildDate = s.Value } } } } func main() { startup() flag.Parse() if err := submain(); err != nil { fmt.Fprintln(os.Stderr, err.Error()) os.Exit(2) } } func submain() error { ctx := context.Background() usage := errors.New(` Usage: gimmeproj -project=[meta project ID] command gimmeproj -project=[meta project ID] -output=list status Commands: lease [duration] Leases a project for a given duration. Prints the project ID to stdout. done [project ID] Returns a project to the pool. version Prints the version of gimmeproj. Administrative commands: pool-add [project ID] Adds a project to the pool. pool-rm [project ID] Removes a project from the pool. status Displays the current status of the meta project. Respects -output. `) if flag.Arg(0) == "version" { fmt.Printf("gimmeproj %s@%s; built at %s\n", buildSource, version, buildDate) return nil } if *metaProject == "" { fmt.Fprintln(os.Stderr, "-project flag is required.") return usage } if len(flag.Args()) == 0 { fmt.Fprintln(os.Stderr, "Missing command.") return usage } var err error datastore, err = ds.NewClient(ctx, *metaProject) if err != nil { return fmt.Errorf("datastore.NewClient: %w", err) } switch flag.Arg(0) { case "help": fmt.Fprintln(os.Stderr, usage.Error()) return nil case "lease": // When leasing, keep trying until we reach our configured timeout ctx, cancel := context.WithTimeout(ctx, *waitTime) defer cancel() for ctx.Err() == nil { err := lease(ctx, flag.Arg(1)) if err == nil { return err } else if errors.Is(err, ErrNoProjects) { log.Printf("Temporary error: %v\n", err) time.Sleep(30 * time.Second) } else { return err } } return ctx.Err() case "pool-add": return addToPool(ctx, flag.Arg(1)) case "pool-rm": return removeFromPool(ctx, flag.Arg(1)) case "status": return status(ctx) case "done": return done(ctx, flag.Arg(1)) } fmt.Fprintln(os.Stderr, "Unknown command.") return usage } // withPool runs the given function in a transaction, saving the state of the pool if the function returns with a non-nil error. func withPool(ctx context.Context, f func(pool *Pool) error) error { _, err := datastore.RunInTransaction(ctx, func(tx *ds.Transaction) error { key := ds.NameKey("Pool", "pool", nil) var pool Pool if err := tx.Get(key, &pool); err != nil { if err == ds.ErrNoSuchEntity { if _, err := tx.Put(key, &pool); err != nil { return fmt.Errorf("Initial Pool.Put: %w", err) } } else { return fmt.Errorf("Pool.Get: %w", err) } } if err := f(&pool); err != nil { return err } _, err := tx.Put(key, &pool) if err != nil { return fmt.Errorf("Pool.Put: %w", err) } return nil }) if err != nil { return fmt.Errorf("datastore: %w", err) } return nil } func lease(ctx context.Context, duration string) error { if duration == "" { return errors.New("must provide a duration (e.g. 10m). See https://golang.org/pkg/time/#ParseDuration") } d, err := time.ParseDuration(duration) if err != nil { return fmt.Errorf("Could not parse duration: %w", err) } var proj *Project err = withPool(ctx, func(pool *Pool) error { var ok bool proj, ok = pool.Lease(d) if !ok { return ErrNoProjects } return nil }) if err != nil { return err } fmt.Fprintf(os.Stderr, "Leased! %s is yours for %s.\n", proj.ID, d) fmt.Print(proj.ID) return nil } func done(ctx context.Context, projectID string) error { if projectID == "" { return errors.New("must provide project id") } err := withPool(ctx, func(pool *Pool) error { proj, ok := pool.Get(projectID) if !ok { return fmt.Errorf("Could not find project %s in project pool.", projectID) } proj.LeaseExpiry = time.Now().Add(-10 * time.Second) return nil }) if err != nil { return err } fmt.Fprintf(os.Stderr, "Returned %s to the pool.\n", projectID) return nil } func status(ctx context.Context) error { return withPool(ctx, func(pool *Pool) error { if *format == "" { fmt.Printf("%-8s %s\n", "LEASE", "PROJECT") } for _, proj := range pool.Projects { exp := "" if !proj.Expired() { secs := time.Until(proj.LeaseExpiry) exp = secs.String() } switch *format { case "": fmt.Printf("%-8s %s\n", exp, proj.ID) case "list": fmt.Printf("%s\n", proj.ID) default: return errors.New("output may be '', 'list'") } } return nil }) } func addToPool(ctx context.Context, proj string) error { if proj == "" { return errors.New("must provide project id") } return withPool(ctx, func(pool *Pool) error { if !pool.Add(proj) { return fmt.Errorf("%s already in pool", proj) } return nil }) } func removeFromPool(ctx context.Context, projectID string) error { if projectID == "" { return errors.New("must provide project id") } return withPool(ctx, func(pool *Pool) error { if _, ok := pool.Get(projectID); !ok { return fmt.Errorf("%s not in pool", projectID) } projs := make([]Project, 0) for _, proj := range pool.Projects { if proj.ID == projectID { continue } projs = append(projs, proj) } pool.Projects = projs return nil }) }