Repository: puppetlabs/wash
Pull request #468 (2019-08-20 21:46:51)

Add crtime attribute
Resolves https://github.com/puppetlabs/wash/issues/405


Patch:
diff --git a/cmd/internal/find/primary/timeAttr.go b/cmd/internal/find/primary/timeAttr.go
index ec46bea..17686df 100644
--- a/cmd/internal/find/primary/timeAttr.go
+++ b/cmd/internal/find/primary/timeAttr.go
@@ -22,6 +22,8 @@ func getTimeAttrValue(name string, e types.Entry) (time.Time, bool) {
 		return e.Attributes.Mtime(), e.Attributes.HasMtime()
 	case "atime":
 		return e.Attributes.Atime(), e.Attributes.HasAtime()
+	case "crtime":
+		return e.Attributes.Crtime(), e.Attributes.HasCrtime()
 	default:
 		panic(fmt.Sprintf("cmdfind.getTimeAttrValue called with nonexistent time attribute %v", name))
 	}
@@ -120,3 +122,8 @@ var Mtime = newTimeAttrPrimary("mtime")
 //
 //nolint
 var Atime = newTimeAttrPrimary("atime")
+
+// Crtime is the crtime primary
+//
+//nolint
+var Crtime = newTimeAttrPrimary("crtime")
diff --git a/fuse/core.go b/fuse/core.go
index a085092..e6a21cf 100644
--- a/fuse/core.go
+++ b/fuse/core.go
@@ -116,6 +116,9 @@ func (f *fuseNode) applyAttr(a *fuse.Attr, attr *plugin.EntryAttributes, isdir b
 		a.Ctime = attr.Ctime()
 	}
 	a.Crtime = startTime
+	if attr.HasCrtime() {
+		a.Crtime = attr.Crtime()
+	}
 	a.BlockSize = blockSize
 	a.Uid = uid
 	a.Gid = gid
diff --git a/plugin/aws/ec2Instance.go b/plugin/aws/ec2Instance.go
index af83a57..7165c79 100644
--- a/plugin/aws/ec2Instance.go
+++ b/plugin/aws/ec2Instance.go
@@ -121,19 +121,19 @@ type ec2InstanceMetadata struct {
 func getAttributes(inst *ec2Client.Instance) plugin.EntryAttributes {
 	attr := plugin.EntryAttributes{}
 
-	// AWS does not include the EC2 instance's ctime in its
+	// AWS does not include the EC2 instance's crtime in its
 	// metadata. It also does not include the EC2 instance's
 	// last state transition time (mtime). Thus, we try to "guess"
-	// reasonable values for ctime and mtime by looping over each
+	// reasonable values for crtime and mtime by looping over each
 	// block device's attachment time and the instance's launch time.
-	// The oldest of these times is the ctime; the newest is the mtime.
-	ctime := awsSDK.TimeValue(inst.LaunchTime)
-	mtime := ctime
+	// The oldest of these times is the crtime; the newest is the mtime.
+	crtime := awsSDK.TimeValue(inst.LaunchTime)
+	mtime := crtime
 	for _, mapping := range inst.BlockDeviceMappings {
 		attachTime := awsSDK.TimeValue(mapping.Ebs.AttachTime)
 
-		if attachTime.Before(ctime) {
-			ctime = attachTime
+		if attachTime.Before(crtime) {
+			crtime = attachTime
 		}
 
 		if attachTime.After(mtime) {
@@ -143,12 +143,12 @@ func getAttributes(inst *ec2Client.Instance) plugin.EntryAttributes {
 
 	meta := plugin.ToJSONObject(ec2InstanceMetadata{
 		Instance:         inst,
-		CreationTime:     ctime,
+		CreationTime:     crtime,
 		LastModifiedTime: mtime,
 	})
 
 	attr.
-		SetCtime(ctime).
+		SetCrtime(crtime).
 		SetMtime(mtime).
 		SetMeta(meta)
 
diff --git a/plugin/aws/ec2InstanceConsoleOutput.go b/plugin/aws/ec2InstanceConsoleOutput.go
index 733d056..fabdb20 100644
--- a/plugin/aws/ec2InstanceConsoleOutput.go
+++ b/plugin/aws/ec2InstanceConsoleOutput.go
@@ -33,8 +33,9 @@ func newEC2InstanceConsoleOutput(ctx context.Context, inst *ec2Instance, latest
 
 	cl.
 		Attributes().
-		SetCtime(output.mtime).
+		SetCrtime(output.mtime).
 		SetMtime(output.mtime).
+		SetCtime(output.mtime).
 		SetAtime(output.mtime).
 		SetSize(uint64(len(output.content)))
 
diff --git a/plugin/aws/s3Bucket.go b/plugin/aws/s3Bucket.go
index cdb9bb4..4a758ee 100644
--- a/plugin/aws/s3Bucket.go
+++ b/plugin/aws/s3Bucket.go
@@ -146,23 +146,24 @@ func listObjects(ctx context.Context, client *s3Client.S3, bucket string, prefix
 // s3Bucket represents an S3 bucket.
 type s3Bucket struct {
 	plugin.EntryBase
-	ctime   time.Time
+	crtime  time.Time
 	client  *s3Client.S3
 	session *session.Session
 }
 
-func newS3Bucket(name string, ctime time.Time, session *session.Session) *s3Bucket {
+func newS3Bucket(name string, crtime time.Time, session *session.Session) *s3Bucket {
 	bucket := &s3Bucket{
 		EntryBase: plugin.NewEntry(name),
 	}
-	bucket.ctime = ctime
+	bucket.crtime = crtime
 	bucket.client = s3Client.New(session)
 	bucket.session = session
 	bucket.
 		Attributes().
-		SetCtime(bucket.ctime).
-		SetMtime(bucket.ctime).
-		SetAtime(bucket.ctime)
+		SetCrtime(bucket.crtime).
+		SetMtime(bucket.crtime).
+		SetCtime(bucket.crtime).
+		SetAtime(bucket.crtime)
 
 	return bucket
 }
@@ -187,7 +188,7 @@ func (b *s3Bucket) List(ctx context.Context) ([]plugin.Entry, error) {
 type bucketMetadata struct {
 	TagSet []*s3Client.Tag
 	Region string
-	Ctime  time.Time
+	Crtime time.Time
 }
 
 func (b *s3Bucket) Metadata(ctx context.Context) (plugin.JSONObject, error) {
@@ -222,7 +223,7 @@ func (b *s3Bucket) Metadata(ctx context.Context) (plugin.JSONObject, error) {
 		return nil, err
 	}
 	metadata.Region = region
-	metadata.Ctime = b.ctime
+	metadata.Crtime = b.crtime
 
 	return plugin.ToJSONObject(metadata), nil
 }
diff --git a/plugin/aws/s3Object.go b/plugin/aws/s3Object.go
index 0711812..c723d02 100644
--- a/plugin/aws/s3Object.go
+++ b/plugin/aws/s3Object.go
@@ -39,8 +39,9 @@ func newS3Object(o *s3Client.Object, name string, bucket string, key string, cli
 	mtime := awsSDK.TimeValue(o.LastModified)
 	s3Obj.
 		Attributes().
-		SetCtime(mtime).
+		SetCrtime(mtime).
 		SetMtime(mtime).
+		SetCtime(mtime).
 		SetAtime(mtime).
 		SetSize(uint64(awsSDK.Int64Value(o.Size))).
 		SetMeta(o)
diff --git a/plugin/docker/container.go b/plugin/docker/container.go
index 5df89b6..6012301 100644
--- a/plugin/docker/container.go
+++ b/plugin/docker/container.go
@@ -40,8 +40,9 @@ func newContainer(inst types.Container, client *client.Client) *container {
 	startTime := time.Unix(inst.Created, 0)
 	cont.
 		Attributes().
-		SetCtime(startTime).
+		SetCrtime(startTime).
 		SetMtime(startTime).
+		SetCtime(startTime).
 		SetAtime(startTime).
 		SetMeta(inst)
 
diff --git a/plugin/docker/volume.go b/plugin/docker/volume.go
index da23372..51c1aaa 100644
--- a/plugin/docker/volume.go
+++ b/plugin/docker/volume.go
@@ -38,8 +38,9 @@ func newVolume(c *client.Client, v *types.Volume) (*volume, error) {
 	vol.client = c
 	vol.
 		Attributes().
-		SetCtime(startTime).
+		SetCrtime(startTime).
 		SetMtime(startTime).
+		SetCtime(startTime).
 		SetAtime(startTime).
 		SetMeta(v)
 
diff --git a/plugin/entryAttributes.go b/plugin/entryAttributes.go
index 3c0a782..0a597fc 100644
--- a/plugin/entryAttributes.go
+++ b/plugin/entryAttributes.go
@@ -46,7 +46,7 @@ to do something like
 
 	attr := plugin.EntryAttributes{}
 	attr.
-		SetCtime(ctime).
+		SetCrtime(crtime).
 		SetMtime(mtime).
 		SetMeta(meta)
 	entry.SetAttributes(attr)
@@ -55,6 +55,7 @@ type EntryAttributes struct {
 	atime   time.Time
 	mtime   time.Time
 	ctime   time.Time
+	crtime  time.Time
 	mode    os.FileMode
 	hasMode bool
 	size    uint64
@@ -112,22 +113,38 @@ func (a *EntryAttributes) SetMtime(mtime time.Time) *EntryAttributes {
 	return a
 }
 
-// HasCtime returns true if the entry has a creation time
+// HasCtime returns true if the entry has a change time
 func (a *EntryAttributes) HasCtime() bool {
 	return !a.ctime.IsZero()
 }
 
-// Ctime returns the entry's creation time
+// Ctime returns the entry's change time
 func (a *EntryAttributes) Ctime() time.Time {
 	return a.ctime
 }
 
-// SetCtime sets the entry's creation time
+// SetCtime sets the entry's change time
 func (a *EntryAttributes) SetCtime(ctime time.Time) *EntryAttributes {
 	a.ctime = ctime
 	return a
 }
 
+// HasCrtime returns true if the entry has a creation time
+func (a *EntryAttributes) HasCrtime() bool {
+	return !a.crtime.IsZero()
+}
+
+// Crtime returns the entry's creation time
+func (a *EntryAttributes) Crtime() time.Time {
+	return a.crtime
+}
+
+// SetCrtime sets the entry's creation time
+func (a *EntryAttributes) SetCrtime(crtime time.Time) *EntryAttributes {
+	a.crtime = crtime
+	return a
+}
+
 // HasMode returns true if the entry has a mode
 func (a *EntryAttributes) HasMode() bool {
 	return a.hasMode
@@ -202,6 +219,9 @@ func (a *EntryAttributes) ToMap(includeMeta bool) map[string]interface{} {
 	if a.HasCtime() {
 		mp["ctime"] = a.Ctime()
 	}
+	if a.HasCrtime() {
+		mp["crtime"] = a.Crtime()
+	}
 	if a.HasMode() {
 		// The mode string representation is the only portable representation. FileMode uses its own
 		// definitions for type bits, not those in http://man7.org/linux/man-pages/man7/inode.7.html.
@@ -257,6 +277,13 @@ func (a *EntryAttributes) UnmarshalJSON(data []byte) error {
 		}
 		a.SetCtime(t)
 	}
+	if crtime, ok := mp["crtime"]; ok {
+		t, err := munge.ToTime(crtime)
+		if err != nil {
+			return attrMungeError("crtime", err)
+		}
+		a.SetCrtime(t)
+	}
 	if mode, ok := mp["mode"]; ok {
 		// Even though os.FileModes are uint32 types, json.Unmarshal unmarshals them as float64.
 		// That's ok, because float64 has sufficient precision to represent all uint32 types.
diff --git a/plugin/entryBase.go b/plugin/entryBase.go
index 21c8032..677b2ca 100644
--- a/plugin/entryBase.go
+++ b/plugin/entryBase.go
@@ -30,7 +30,7 @@ to do something like
 	e.
 		DisableCachingFor(plugin.ListOp).
 		Attributes().
-		SetCtime(ctime).
+		SetCrtime(crtime).
 		SetMtime(mtime).
 		SetMeta(meta)
 */
diff --git a/plugin/kubernetes/pod.go b/plugin/kubernetes/pod.go
index f3c4362..0a895c5 100644
--- a/plugin/kubernetes/pod.go
+++ b/plugin/kubernetes/pod.go
@@ -33,7 +33,7 @@ func newPod(ctx context.Context, client *k8s.Clientset, config *rest.Config, ns
 
 	pd.
 		Attributes().
-		SetCtime(p.CreationTimestamp.Time).
+		SetCrtime(p.CreationTimestamp.Time).
 		SetAtime(p.CreationTimestamp.Time).
 		SetMeta(plugin.ToJSONObject(p))
 
diff --git a/plugin/kubernetes/pvc.go b/plugin/kubernetes/pvc.go
index 9458376..f075320 100644
--- a/plugin/kubernetes/pvc.go
+++ b/plugin/kubernetes/pvc.go
@@ -37,8 +37,9 @@ func newPVC(pi typedv1.PersistentVolumeClaimInterface, pd typedv1.PodInterface,
 
 	vol.
 		Attributes().
-		SetCtime(p.CreationTimestamp.Time).
+		SetCrtime(p.CreationTimestamp.Time).
 		SetMtime(p.CreationTimestamp.Time).
+		SetCtime(p.CreationTimestamp.Time).
 		SetAtime(p.CreationTimestamp.Time).
 		SetMeta(p)
 
diff --git a/website/content/_index.md b/website/content/_index.md
index 895acad..06d7d3c 100644
--- a/website/content/_index.md
+++ b/website/content/_index.md
@@ -198,6 +198,8 @@ spec:
 EOF
 ```
 
+**NOTE:** `find \( -k 'docker/*container' -o -k 'kubernetes/*pod' \) -crtime -1d` is an easier (and more expressive) way to solve this problem. This example's only here to introduce the meta primary.
+
 ### Listing AWS resources
 
 Wash also includes support for AWS. If you have your own and you've configured the AWS CLI on your workstation, you'll be able to use Wash to explore EC2 instances and S3 buckets.
diff --git a/website/content/docs/_index.md b/website/content/docs/_index.md
index 23b53ec..5c50381 100644
--- a/website/content/docs/_index.md
+++ b/website/content/docs/_index.md
@@ -284,7 +284,7 @@ Jun 20 13:47:09.886 FUSE: Listed in /goodreads: [{Inode:0 Type:dir Name:read} {I
 
 All entries have metadata, which is a JSON object containing a complete description of the entry. For example, a Docker container's metadata includes its labels, its state, its start time, the image it was built from, its mounted volumes, etc. [`wash find`](#wash-find) can filter on this metadata. In our example, you can use `find docker/containers -daystart -fullmeta -m .state .startedAt -{1d} -a .status running` to see a list of all running containers that started today (try it out!). Thus, metadata filtering is powerful. However, it also requires the user to query an entry's metadata to construct the filter. Creating a filter on the same property that's shared by many different kinds of entries is repetitive, error-prone, and an obvious candidate for usability improvement. For example, metadata filtering gets annoying when you are trying to filter on an EC2 instance's/Docker container's/Kubernetes pod's state due to the structural differences in their metadata (e.g. an EC2 instance's state is contained in the `.state.name` key, while a Kubernetes pod's state is contained in the `.status.phase` key). Metadata filtering is also slow. It requires O(N) API requests, where N is the number of visited entries.
 
-To make `wash find`'s filtering less tedious and better performing, entries can also have attributes. The attributes represent common metadata properties that people filter on. Currently, these are the traditional `ctime`, `mtime`, `atime`, `size`, and `mode` filesystem attributes, along with a special `meta` attribute representing a subset of the entry's metadata (useful for fast metadata filtering). The attributes are fetched in bulk when the entry's parent is listed. Typically, the bulk fetch is done through an API's `list` endpoint. This endpoint returns an array of JSON objects representing the entries. The `meta` attribute is set to this JSON object while the remaining attributes are parsed from the object's fields. For example, `list docker/containers` will fetch all of your containers by querying Docker's `/containers/json` endpoint. That endpoint's response is then used to create the container entry objects, where each container entry's `meta` attribute is set to a `/containers/json` object and the containers' `ctime`/`mtime` attributes are parsed from it.
+To make `wash find`'s filtering less tedious and better performing, entries can also have attributes. The attributes represent common metadata properties that people filter on. Currently, these are the traditional `crtime`, `mtime`, `ctime`, `atime`, `size`, and `mode` filesystem attributes, along with a special `meta` attribute representing a subset of the entry's metadata (useful for fast metadata filtering). The attributes are fetched in bulk when the entry's parent is listed. Typically, the bulk fetch is done through an API's `list` endpoint. This endpoint returns an array of JSON objects representing the entries. The `meta` attribute is set to this JSON object while the remaining attributes are parsed from the object's fields. For example, `list docker/containers` will fetch all of your containers by querying Docker's `/containers/json` endpoint. That endpoint's response is then used to create the container entry objects, where each container entry's `meta` attribute is set to a `/containers/json` object and the containers' `crtime`/`mtime` attributes are parsed from it.
 
 NOTE: _All_ attributes are optional, so set the ones that you think make sense. For example, if the `mode` or `size` attributes don't make sense for your entry, then feel free to ignore them. However, we recommend that you try to set the `meta` attribute when you can to take advantage of metadata filtering.
 


Test patch:
diff --git a/cmd/internal/find/primary/parser_test.go b/cmd/internal/find/primary/parser_test.go
index ef99476..5ca5c6c 100644
--- a/cmd/internal/find/primary/parser_test.go
+++ b/cmd/internal/find/primary/parser_test.go
@@ -24,6 +24,7 @@ func (s *ParserTestSuite) TestRegisteredPrimaries() {
 		Ctime,
 		Mtime,
 		Atime,
+		Crtime,
 		Kind,
 	}
 	expectedMp := map[string]*Primary{
@@ -38,6 +39,7 @@ func (s *ParserTestSuite) TestRegisteredPrimaries() {
 		"-ctime":  Ctime,
 		"-mtime":  Mtime,
 		"-atime":  Atime,
+		"-crtime": Crtime,
 		"-kind":   Kind,
 		"-k":      Kind,
 	}
diff --git a/cmd/internal/find/primary/timeAttr_test.go b/cmd/internal/find/primary/timeAttr_test.go
index 27aa8cc..00af1bb 100644
--- a/cmd/internal/find/primary/timeAttr_test.go
+++ b/cmd/internal/find/primary/timeAttr_test.go
@@ -48,6 +48,16 @@ func (s *TimeAttrPrimaryTestSuite) TestGetTimeAttrValue() {
 	if s.True(ok) {
 		s.Equal(expected, actual)
 	}
+
+	// Test crtime
+	_, ok = getTimeAttrValue("crtime", e)
+	s.False(ok)
+	expected = time.Now()
+	e.Attributes.SetCrtime(expected)
+	actual, ok = getTimeAttrValue("crtime", e)
+	if s.True(ok) {
+		s.Equal(expected, actual)
+	}
 }
 
 // These tests use the ctimePrimary as the representative test case
@@ -70,15 +80,15 @@ func (s *TimeAttrPrimaryTestSuite) TestErrors() {
 
 func (s *TimeAttrPrimaryTestSuite) TestValidInput() {
 	// We set trueCtime to 1.5 days in order to test roundDiff
-	s.RTC("2", "", 1*numeric.DurationOf('d') + 12*numeric.DurationOf('h'), 1 * numeric.DurationOf('d'))
+	s.RTC("2", "", 1*numeric.DurationOf('d')+12*numeric.DurationOf('h'), 1*numeric.DurationOf('d'))
 	// +1 means p will return true if diff > 1 day
-	s.RTC("+1", "", 2 * numeric.DurationOf('d'), 0 * numeric.DurationOf('d'))
+	s.RTC("+1", "", 2*numeric.DurationOf('d'), 0*numeric.DurationOf('d'))
 	// -2 means p will return true if diff < 2 days
-	s.RTC("-2", "", 1 * numeric.DurationOf('d'), 3 * numeric.DurationOf('d'))
+	s.RTC("-2", "", 1*numeric.DurationOf('d'), 3*numeric.DurationOf('d'))
 	// time.Time has nanosecond precision so units like "1h30m" aren't really
 	// useful unless they're used with the +/- modifiers.
-	s.RTC("+1h", "", 2 * numeric.DurationOf('h'), 1 * numeric.DurationOf('m'))
-	s.RTC("-1h", "", 1 * numeric.DurationOf('m'), 1 * numeric.DurationOf('h'))
+	s.RTC("+1h", "", 2*numeric.DurationOf('h'), 1*numeric.DurationOf('m'))
+	s.RTC("-1h", "", 1*numeric.DurationOf('m'), 1*numeric.DurationOf('h'))
 }
 
 func TestTimeAttrPrimary(t *testing.T) {
diff --git a/plugin/entryAttributes_test.go b/plugin/entryAttributes_test.go
index 69c2ece..c249ddb 100644
--- a/plugin/entryAttributes_test.go
+++ b/plugin/entryAttributes_test.go
@@ -96,6 +96,17 @@ func (suite *EntryAttributesTestSuite) TestEntryAttributes() {
 	suite.Equal(expectedMp, attr.ToMap(true))
 	doUnmarshalJSONTests()
 
+	// Tests for Crtime
+	suite.Equal(false, attr.HasCrtime())
+	suite.Equal(expectedMp, attr.ToMap(true))
+	t = timeNow()
+	attr.SetCrtime(t)
+	expectedMp["crtime"] = t
+	suite.Equal(t, attr.Crtime())
+	suite.Equal(true, attr.HasCrtime())
+	suite.Equal(expectedMp, attr.ToMap(true))
+	doUnmarshalJSONTests()
+
 	// Tests for Mode
 	suite.Equal(false, attr.HasMode())
 	suite.Equal(expectedMp, attr.ToMap(true))