17609: Merge branch 'master'
authorTom Clegg <tom@curii.com>
Fri, 11 Jun 2021 14:02:58 +0000 (10:02 -0400)
committerTom Clegg <tom@curii.com>
Fri, 11 Jun 2021 14:02:58 +0000 (10:02 -0400)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@curii.com>

1  2 
go.mod
go.sum
lib/costanalyzer/cmd.go
lib/costanalyzer/costanalyzer.go

diff --combined go.mod
index 9062c667d7838f7684f1ac1fd0e98e94e046fe74,0ff679a576e232d318bdc14873069115036967bc..b70f6f3b476789f69f1845bffb6bfd4fcac80885
--- 1/go.mod
--- 2/go.mod
+++ b/go.mod
@@@ -55,14 -55,14 +55,16 @@@ require 
        github.com/prometheus/common v0.7.0
        github.com/satori/go.uuid v1.2.1-0.20180103174451-36e9d2ebbde5 // indirect
        github.com/sergi/go-diff v1.0.0 // indirect
 -      github.com/sirupsen/logrus v1.4.2
 +      github.com/sirupsen/logrus v1.8.1
        github.com/src-d/gcfg v1.3.0 // indirect
        github.com/xanzy/ssh-agent v0.1.0 // indirect
        golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
-       golang.org/x/net v0.0.0-20201021035429-f5854403a974
+       golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
        golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
 +      golang.org/x/sys v0.0.0-20210603125802-9665404d3644
 +      golang.org/x/tools v0.1.0 // indirect
+       golang.org/x/sys v0.0.0-20210510120138-977fb7262007
+       golang.org/x/tools v0.1.2 // indirect
        google.golang.org/api v0.13.0
        gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
        gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405
diff --combined go.sum
index 23731f69a0861ed859456f3895275782fdeb722d,c28ab4624038a143a4d9adf8a9e2593d123183c0..1fd37ed11ce411e625518eb3189c225e80b74616
--- 1/go.sum
--- 2/go.sum
+++ b/go.sum
@@@ -234,8 -234,6 +234,8 @@@ github.com/shabbyrobe/gocovmerge v0.0.0
  github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
  github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
  github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
 +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
 +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
  github.com/spf13/afero v1.2.1 h1:qgMbHoJbPbw579P+1zVY+6n4nIFuIchaIjzZ/I/Yq8M=
  github.com/spf13/afero v1.2.1/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
  github.com/src-d/gcfg v1.3.0 h1:2BEDr8r0I0b8h/fOqwtxCEiq2HJu8n2JGZJQFGXWLjg=
@@@ -251,6 -249,8 +251,8 @@@ github.com/xanzy/ssh-agent v0.1.0 h1:lO
  github.com/xanzy/ssh-agent v0.1.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8=
  github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
  github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+ github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs=
+ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
  go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
  go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
  golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@@ -268,6 -268,8 +270,8 @@@ golang.org/x/lint v0.0.0-20190409202823
  golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
  golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
  golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+ golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
+ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
  golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
  golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
  golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@@ -282,6 -284,8 +286,8 @@@ golang.org/x/net v0.0.0-20190620200207-
  golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
  golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
  golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
+ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
  golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
  golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
  golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
@@@ -293,6 -297,8 +299,8 @@@ golang.org/x/sync v0.0.0-20190227155943
  golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
  golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
  golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
+ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
  golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
  golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
  golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@@ -303,12 -309,15 +311,18 @@@ golang.org/x/sys v0.0.0-20190422165155-
  golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
  golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
  golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
  golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
  golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
  golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 +golang.org/x/sys v0.0.0-20210603125802-9665404d3644 h1:CA1DEQ4NdKphKeL70tvsWNdT5oFh1lOjihRcEDROi0I=
 +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+ golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
+ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
+ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
  golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
  golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
  golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@@ -326,6 -335,8 +340,8 @@@ golang.org/x/tools v0.0.0-2019050614530
  golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
  golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
  golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+ golang.org/x/tools v0.1.2 h1:kRBLX7v7Af8W7Gdbbc908OJcdgtK8bOz9Uaj8/F1ACA=
+ golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
  golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
  golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
  golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --combined lib/costanalyzer/cmd.go
index b08e943c0120e968862c1a35703dbe2d8c90fa01,525ec619b5e4aeeb0b4e271704f13385a41bcefd..6065ad2c0b2cb934607826d8500215bbabf0d868
@@@ -6,31 -6,41 +6,34 @@@ package costanalyze
  
  import (
        "io"
+       "time"
  
-       "git.arvados.org/arvados.git/lib/config"
 +      "git.arvados.org/arvados.git/lib/cmd"
        "git.arvados.org/arvados.git/sdk/go/ctxlog"
 -      "github.com/sirupsen/logrus"
  )
  
- var Command command
+ var Command = command{}
  
- type command struct{}
+ type command struct {
+       uuids      arrayFlags
+       resultsDir string
+       cache      bool
+       begin      time.Time
+       end        time.Time
+ }
  
 -type NoPrefixFormatter struct{}
 -
 -func (f *NoPrefixFormatter) Format(entry *logrus.Entry) ([]byte, error) {
 -      return []byte(entry.Message), nil
 -}
 -
  // RunCommand implements the subcommand "costanalyzer <collection> <collection> ..."
- func (command) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+ func (c command) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
        var err error
        logger := ctxlog.New(stderr, "text", "info")
 +      logger.SetFormatter(cmd.NoPrefixFormatter{})
        defer func() {
                if err != nil {
 -                      logger.Error("\n" + err.Error() + "\n")
 +                      logger.Error("\n" + err.Error())
                }
        }()
  
-       loader := config.NewLoader(stdin, logger)
-       loader.SkipLegacy = true
 -      logger.SetFormatter(new(NoPrefixFormatter))
--
-       exitcode, err := costanalyzer(prog, args, loader, logger, stdout, stderr)
+       exitcode, err := c.costAnalyzer(prog, args, logger, stdout, stderr)
  
        return exitcode
  }
index 23df754bcac8d86ec694153ef2f95980d0977f6c,edaaa5bd178243f2c9044d32398c5ddccc67b5dc..f9875c6a8e79cf427a378c34a1b0a9d4d9264473
@@@ -9,6 -9,9 +9,6 @@@ import 
        "errors"
        "flag"
        "fmt"
 -      "git.arvados.org/arvados.git/sdk/go/arvados"
 -      "git.arvados.org/arvados.git/sdk/go/arvadosclient"
 -      "git.arvados.org/arvados.git/sdk/go/keepclient"
        "io"
        "io/ioutil"
        "net/http"
        "strings"
        "time"
  
-       "git.arvados.org/arvados.git/lib/config"
 +      "git.arvados.org/arvados.git/sdk/go/arvados"
 +      "git.arvados.org/arvados.git/sdk/go/arvadosclient"
 +      "git.arvados.org/arvados.git/sdk/go/keepclient"
        "github.com/sirupsen/logrus"
  )
  
+ const timestampFormat = "2006-01-02T15:04:05"
  type nodeInfo struct {
        // Legacy (records created by Arvados Node Manager with Arvados <= 1.4.3)
        Properties struct {
@@@ -51,32 -52,42 +52,42 @@@ func (i *arrayFlags) Set(value string) 
        return nil
  }
  
- func parseFlags(prog string, args []string, loader *config.Loader, logger *logrus.Logger, stderr io.Writer) (exitCode int, uuids arrayFlags, resultsDir string, cache bool, err error) {
+ func (c *command) parseFlags(prog string, args []string, logger *logrus.Logger, stderr io.Writer) (exitCode int, err error) {
+       var beginStr, endStr string
        flags := flag.NewFlagSet("", flag.ContinueOnError)
        flags.SetOutput(stderr)
        flags.Usage = func() {
                fmt.Fprintf(flags.Output(), `
  Usage:
-   %s [options ...] uuid [uuid ...]
+   %s [options ...] [UUID ...]
+       This program analyzes the cost of Arvados container requests and calculates
+       the total cost across all requests. At least one UUID or a timestamp range
+       must be specified.
  
-       This program analyzes the cost of Arvados container requests. For each uuid
-       supplied, it creates a CSV report that lists all the containers used to
-       fulfill the container request, together with the machine type and cost of
-       each container. At least one uuid must be specified.
+       When the '-output' option is specified, a set of CSV files with cost details
+       will be written to the provided directory. Each file is a CSV report that lists
+       all the containers used to fulfill the container request, together with the
+       machine type and cost of each container.
  
-       When supplied with the uuid of a container request, it will calculate the
+       When supplied with the UUID of a container request, it will calculate the
        cost of that container request and all its children.
  
-       When supplied with the uuid of a collection, it will see if there is a
-       container_request uuid in the properties of the collection, and if so, it
+       When supplied with the UUID of a collection, it will see if there is a
+       container_request UUID in the properties of the collection, and if so, it
        will calculate the cost of that container request and all its children.
  
-       When supplied with a project uuid or when supplied with multiple container
-       request or collection uuids, it will create a CSV report for each supplied
-       uuid, as well as a CSV file with aggregate cost accounting for all supplied
-       uuids. The aggregate cost report takes container reuse into account: if a
-       container was reused between several container requests, its cost will only
-       be counted once.
+       When supplied with a project UUID or when supplied with multiple container
+       request or collection UUIDs, it will calculate the total cost for all
+       supplied UUIDs.
+       When supplied with a 'begin' and 'end' timestamp (format:
+       %s), it will calculate the cost for all top-level container
+       requests whose containers finished during the specified interval.
+       The total cost calculation takes container reuse into account: if a container
+       was reused between several container requests, its cost will only be counted
+       once.
  
        Caveats:
  
        permanent cloud nodes that provide the Arvados services, the cost of data
        stored in Arvados, etc.
  
-       - When provided with a project uuid, subprojects will not be considered.
+       - When provided with a project UUID, subprojects will not be considered.
  
-       In order to get the data for the uuids supplied, the ARVADOS_API_HOST and
+       In order to get the data for the UUIDs supplied, the ARVADOS_API_HOST and
        ARVADOS_API_TOKEN environment variables must be set.
  
        This program prints the total dollar amount from the aggregate cost
-       accounting across all provided uuids on stdout.
-       When the '-output' option is specified, a set of CSV files with cost details
-       will be written to the provided directory.
+       accounting across all provided UUIDs on stdout.
  
  Options:
- `, prog)
+ `, prog, timestampFormat)
                flags.PrintDefaults()
        }
        loglevel := flags.String("log-level", "info", "logging `level` (debug, info, ...)")
-       flags.StringVar(&resultsDir, "output", "", "output `directory` for the CSV reports")
-       flags.BoolVar(&cache, "cache", true, "create and use a local disk cache of Arvados objects")
+       flags.StringVar(&c.resultsDir, "output", "", "output `directory` for the CSV reports")
+       flags.StringVar(&beginStr, "begin", "", fmt.Sprintf("timestamp `begin` for date range operation (format: %s)", timestampFormat))
+       flags.StringVar(&endStr, "end", "", fmt.Sprintf("timestamp `end` for date range operation (format: %s)", timestampFormat))
+       flags.BoolVar(&c.cache, "cache", true, "create and use a local disk cache of Arvados objects")
        err = flags.Parse(args)
        if err == flag.ErrHelp {
                err = nil
                exitCode = 2
                return
        }
-       uuids = flags.Args()
+       c.uuids = flags.Args()
+       if (len(beginStr) != 0 && len(endStr) == 0) || (len(beginStr) == 0 && len(endStr) != 0) {
+               flags.Usage()
+               err = fmt.Errorf("When specifying a date range, both begin and end must be specified")
+               exitCode = 2
+               return
+       }
+       if len(beginStr) != 0 {
+               var errB, errE error
+               c.begin, errB = time.Parse(timestampFormat, beginStr)
+               c.end, errE = time.Parse(timestampFormat, endStr)
+               if (errB != nil) || (errE != nil) {
+                       flags.Usage()
+                       err = fmt.Errorf("When specifying a date range, both begin and end must be of the format %s %+v, %+v", timestampFormat, errB, errE)
+                       exitCode = 2
+                       return
+               }
+       }
  
-       if len(uuids) < 1 {
+       if (len(c.uuids) < 1) && (len(beginStr) == 0) {
                flags.Usage()
                err = fmt.Errorf("error: no uuid(s) provided")
                exitCode = 2
                return
        }
        logger.SetLevel(lvl)
-       if !cache {
+       if !c.cache {
 -              logger.Debug("Caching disabled\n")
 +              logger.Debug("Caching disabled")
        }
        return
  }
@@@ -220,12 -249,12 +249,12 @@@ func loadCachedObject(logger *logrus.Lo
        case *arvados.ContainerRequest:
                if v.State == arvados.ContainerRequestStateFinal {
                        reload = false
 -                      logger.Debugf("Loaded object %s from local cache (%s)\n", uuid, file)
 +                      logger.Debugf("Loaded object %s from local cache (%s)", uuid, file)
                }
        case *arvados.Container:
                if v.State == arvados.ContainerStateComplete || v.State == arvados.ContainerStateCancelled {
                        reload = false
 -                      logger.Debugf("Loaded object %s from local cache (%s)\n", uuid, file)
 +                      logger.Debugf("Loaded object %s from local cache (%s)", uuid, file)
                }
        }
        return
@@@ -355,7 -384,7 +384,7 @@@ func handleProject(logger *logrus.Logge
                return nil, fmt.Errorf("error querying container_requests: %s", err.Error())
        }
        if value, ok := childCrs["items"]; ok {
 -              logger.Infof("Collecting top level container requests in project %s\n", uuid)
 +              logger.Infof("Collecting top level container requests in project %s", uuid)
                items := value.([]interface{})
                for _, item := range items {
                        itemMap := item.(map[string]interface{})
                        }
                }
        } else {
 -              logger.Infof("No top level container requests found in project %s\n", uuid)
 +              logger.Infof("No top level container requests found in project %s", uuid)
        }
        return
  }
@@@ -381,6 -410,7 +410,7 @@@ func generateCrCsv(logger *logrus.Logge
        var tmpCsv string
        var tmpTotalCost float64
        var totalCost float64
+       fmt.Printf("Processing %s\n", uuid)
  
        var crUUID = uuid
        if strings.Contains(uuid, "-4zz18-") {
        }
        if len(cr.ContainerUUID) == 0 {
                // Nothing to do! E.g. a CR in 'Uncommitted' state.
 -              logger.Infof("No container associated with container request %s, skipping\n", crUUID)
 +              logger.Infof("No container associated with container request %s, skipping", crUUID)
                return nil, nil
        }
        var container arvados.Container
  
        topNode, err := getNode(arv, ac, kc, cr)
        if err != nil {
-               return nil, fmt.Errorf("error getting node %s: %s", cr.UUID, err)
+               logger.Errorf("Skipping container request %s: error getting node %s: %s", cr.UUID, cr.UUID, err)
+               return nil, nil
        }
        tmpCsv, totalCost = addContainerLine(logger, topNode, cr, container)
        csv += tmpCsv
        if err != nil {
                return nil, fmt.Errorf("error querying container_requests: %s", err.Error())
        }
-       logger.Infof("Collecting child containers for container request %s", crUUID)
+       logger.Infof("Collecting child containers for container request %s (%s)", crUUID, container.FinishedAt)
        for _, cr2 := range childCrs.Items {
                logger.Info(".")
                node, err := getNode(arv, ac, kc, cr2)
                if err != nil {
-                       return nil, fmt.Errorf("error getting node %s: %s", cr2.UUID, err)
+                       logger.Errorf("Skipping container request %s: error getting node %s: %s", cr2.UUID, cr2.UUID, err)
+                       continue
                }
 -              logger.Debug("\nChild container: " + cr2.ContainerUUID + "\n")
 +              logger.Debug("\nChild container: " + cr2.ContainerUUID)
                var c2 arvados.Container
                err = loadObject(logger, ac, cr.UUID, cr2.ContainerUUID, cache, &c2)
                if err != nil {
                csv += tmpCsv
                totalCost += tmpTotalCost
        }
 -      logger.Info(" done\n")
 +      logger.Info(" done")
  
        csv += "TOTAL,,,,,,,,," + strconv.FormatFloat(totalCost, 'f', 8, 64) + "\n"
  
                if err != nil {
                        return nil, fmt.Errorf("error writing file with path %s: %s", fName, err.Error())
                }
 -              logger.Infof("\nUUID report in %s\n\n", fName)
 +              logger.Infof("\nUUID report in %s\n", fName)
        }
  
        return
  }
  
- func costanalyzer(prog string, args []string, loader *config.Loader, logger *logrus.Logger, stdout, stderr io.Writer) (exitcode int, err error) {
-       exitcode, uuids, resultsDir, cache, err := parseFlags(prog, args, loader, logger, stderr)
+ func (c *command) costAnalyzer(prog string, args []string, logger *logrus.Logger, stdout, stderr io.Writer) (exitcode int, err error) {
+       exitcode, err = c.parseFlags(prog, args, logger, stderr)
        if exitcode != 0 {
                return
        }
-       if resultsDir != "" {
-               err = ensureDirectory(logger, resultsDir)
+       if c.resultsDir != "" {
+               err = ensureDirectory(logger, c.resultsDir)
                if err != nil {
                        exitcode = 3
                        return
                }
        }
  
+       uuidChannel := make(chan string)
        // Arvados Client setup
        arv, err := arvadosclient.MakeArvadosClient()
        if err != nil {
  
        ac := arvados.NewClientFromEnv()
  
+       // Populate uuidChannel with the requested uuid list
+       go func() {
+               defer close(uuidChannel)
+               for _, uuid := range c.uuids {
+                       uuidChannel <- uuid
+               }
+               if !c.begin.IsZero() {
+                       initialParams := arvados.ResourceListParams{
+                               Filters: []arvados.Filter{{"container.finished_at", ">=", c.begin}, {"container.finished_at", "<", c.end}, {"requesting_container_uuid", "=", nil}},
+                               Order:   "created_at",
+                       }
+                       params := initialParams
+                       for {
+                               // This list variable must be a new one declared
+                               // inside the loop: otherwise, items in the API
+                               // response would get deep-merged into the items
+                               // loaded in previous iterations.
+                               var list arvados.ContainerRequestList
+                               err := ac.RequestAndDecode(&list, "GET", "arvados/v1/container_requests", nil, params)
+                               if err != nil {
+                                       logger.Errorf("Error getting container request list from Arvados API: %s\n", err)
+                                       break
+                               }
+                               if len(list.Items) == 0 {
+                                       break
+                               }
+                               for _, i := range list.Items {
+                                       uuidChannel <- i.UUID
+                               }
+                               params.Offset += len(list.Items)
+                       }
+               }
+       }()
        cost := make(map[string]float64)
-       for _, uuid := range uuids {
+       for uuid := range uuidChannel {
+               fmt.Printf("Considering %s\n", uuid)
                if strings.Contains(uuid, "-j7d0g-") {
                        // This is a project (group)
-                       cost, err = handleProject(logger, uuid, arv, ac, kc, resultsDir, cache)
+                       cost, err = handleProject(logger, uuid, arv, ac, kc, c.resultsDir, c.cache)
                        if err != nil {
                                exitcode = 1
                                return
                } else if strings.Contains(uuid, "-xvhdp-") || strings.Contains(uuid, "-4zz18-") {
                        // This is a container request
                        var crCsv map[string]float64
-                       crCsv, err = generateCrCsv(logger, uuid, arv, ac, kc, resultsDir, cache)
+                       crCsv, err = generateCrCsv(logger, uuid, arv, ac, kc, c.resultsDir, c.cache)
                        if err != nil {
                                err = fmt.Errorf("error generating CSV for uuid %s: %s", uuid, err.Error())
                                exitcode = 2
                        // keep going.
                        logger.Errorf("cost analysis is not supported for the 'Home' project: %s", uuid)
                } else {
 -                      logger.Errorf("this argument does not look like a uuid: %s\n", uuid)
 +                      logger.Errorf("this argument does not look like a uuid: %s", uuid)
                        exitcode = 3
                        return
                }
        }
  
        if len(cost) == 0 {
 -              logger.Info("Nothing to do!\n")
 +              logger.Info("Nothing to do!")
                return
        }
  
        var csv string
  
        csv = "# Aggregate cost accounting for uuids:\n"
-       for _, uuid := range uuids {
+       for _, uuid := range c.uuids {
                csv += "# " + uuid + "\n"
        }
  
  
        csv += "TOTAL," + strconv.FormatFloat(total, 'f', 8, 64) + "\n"
  
-       if resultsDir != "" {
+       if c.resultsDir != "" {
                // Write the resulting CSV file
-               aFile := resultsDir + "/" + time.Now().Format("2006-01-02-15-04-05") + "-aggregate-costaccounting.csv"
+               aFile := c.resultsDir + "/" + time.Now().Format("2006-01-02-15-04-05") + "-aggregate-costaccounting.csv"
                err = ioutil.WriteFile(aFile, []byte(csv), 0644)
                if err != nil {
                        err = fmt.Errorf("error writing file with path %s: %s", aFile, err.Error())
                        exitcode = 1
                        return
                }
 -              logger.Infof("Aggregate cost accounting for all supplied uuids in %s\n", aFile)
 +              logger.Infof("Aggregate cost accounting for all supplied uuids in %s", aFile)
        }
  
        // Output the total dollar amount on stdout