Repository: Azure/ARO-RP
Pull request #1248 (2021-01-14 14:55:25)

Adds cluster creation timestamp to the internal document
### Which issue this PR addresses:

[Work item 9057017](https://msazure.visualstudio.com/AzureRedHatOpenShift/_workitems/edit/9057017/)


### What this PR does / why we need it:

We want to have better visibility into cluster age. This PR:

* Adds `createdAt` field to the internal document which is being populated by front-end when we receive a cluster creation request
* Adds temporary piece of code required to populate `createdAt` for older clusters. We do it based on the creation timestamp from the default namespaec
* Adds the cluster creation timestamp to the `cluster.summary` metric

### Test plan for issue:

Includes tests

### Is there any documentation that needs to be updated for this PR?

No



Patch:
diff --git a/pkg/api/openshiftcluster.go b/pkg/api/openshiftcluster.go
index 6327a497a..288064133 100644
--- a/pkg/api/openshiftcluster.go
+++ b/pkg/api/openshiftcluster.go
@@ -71,6 +71,8 @@ type OpenShiftClusterProperties struct {
 	FailedProvisioningState ProvisioningState   `json:"failedProvisioningState,omitempty"`
 	LastAdminUpdateError    string              `json:"lastAdminUpdateError,omitempty"`
 
+	CreatedAt time.Time `json:"createdAt,omitempty"`
+
 	// CreatedBy is the RP version (Git commit hash) that created this cluster
 	CreatedBy string `json:"createdBy,omitempty"`
 
diff --git a/pkg/cluster/createdat.go b/pkg/cluster/createdat.go
new file mode 100644
index 000000000..826de0076
--- /dev/null
+++ b/pkg/cluster/createdat.go
@@ -0,0 +1,32 @@
+package cluster
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"github.com/Azure/ARO-RP/pkg/api"
+)
+
+// populateCreatedAt updates DB document with cluster creation date from the default namespace
+// This is a temporary piece of code to populate the relevant field for older clusters.
+// TODO(mikalai): Remove the function after a round of admin updates
+func (m *manager) populateCreatedAt(ctx context.Context) error {
+	if !m.doc.OpenShiftCluster.Properties.CreatedAt.IsZero() {
+		return nil
+	}
+
+	ns, err := m.kubernetescli.CoreV1().Namespaces().Get(ctx, "default", metav1.GetOptions{})
+	if err != nil {
+		return err
+	}
+
+	m.doc, err = m.db.PatchWithLease(ctx, m.doc.Key, func(doc *api.OpenShiftClusterDocument) error {
+		doc.OpenShiftCluster.Properties.CreatedAt = ns.CreationTimestamp.Time
+		return nil
+	})
+	return err
+}
diff --git a/pkg/cluster/install.go b/pkg/cluster/install.go
index bf2dca87a..6ec42fc5d 100644
--- a/pkg/cluster/install.go
+++ b/pkg/cluster/install.go
@@ -33,6 +33,7 @@ func (m *manager) AdminUpdate(ctx context.Context) error {
 		steps.Condition(m.apiServersReady, 30*time.Minute),
 		steps.Action(m.ensureBillingRecord), // belt and braces
 		steps.Action(m.fixSSH),
+		steps.Action(m.populateCreatedAt), // TODO(mikalai): Remove after a round of admin updates
 		steps.Action(m.fixSREKubeconfig),
 		steps.Action(m.ensureAROOperator),
 		steps.Condition(m.aroDeploymentReady, 20*time.Minute),
diff --git a/pkg/frontend/frontend.go b/pkg/frontend/frontend.go
index ec3791b19..15c4172cb 100644
--- a/pkg/frontend/frontend.go
+++ b/pkg/frontend/frontend.go
@@ -66,6 +66,8 @@ type frontend struct {
 
 	startTime time.Time
 	ready     atomic.Value
+
+	now func() time.Time
 }
 
 // Runnable represents a runnable object
@@ -102,6 +104,8 @@ func NewFrontend(ctx context.Context,
 		bucketAllocator: &bucket.Random{},
 
 		startTime: time.Now(),
+
+		now: time.Now,
 	}
 
 	l, err := f.env.Listen()
diff --git a/pkg/frontend/openshiftcluster_putorpatch.go b/pkg/frontend/openshiftcluster_putorpatch.go
index 132166e86..468b7d89c 100644
--- a/pkg/frontend/openshiftcluster_putorpatch.go
+++ b/pkg/frontend/openshiftcluster_putorpatch.go
@@ -73,6 +73,7 @@ func (f *frontend) _putOrPatchOpenShiftCluster(ctx context.Context, r *http.Requ
 				Properties: api.OpenShiftClusterProperties{
 					ArchitectureVersion: version.InstallArchitectureVersion,
 					ProvisioningState:   api.ProvisioningStateSucceeded,
+					CreatedAt:           f.now().UTC(),
 					CreatedBy:           version.GitCommit,
 					ProvisionedBy:       version.GitCommit,
 					ClusterProfile: api.ClusterProfile{
diff --git a/pkg/monitor/cluster/summary.go b/pkg/monitor/cluster/summary.go
index caf1e10ca..70ee9cf61 100644
--- a/pkg/monitor/cluster/summary.go
+++ b/pkg/monitor/cluster/summary.go
@@ -46,6 +46,7 @@ func (mon *Monitor) emitSummary(ctx context.Context) error {
 		"masterCount":       strconv.Itoa(masterCount),
 		"workerCount":       strconv.Itoa(workerCount),
 		"provisioningState": mon.oc.Properties.ProvisioningState.String(),
+		"createdAt":         mon.oc.Properties.CreatedAt.String(),
 	})
 
 	return nil


Test patch:
diff --git a/pkg/cluster/createdat_test.go b/pkg/cluster/createdat_test.go
new file mode 100644
index 000000000..b05ad9c16
--- /dev/null
+++ b/pkg/cluster/createdat_test.go
@@ -0,0 +1,136 @@
+package cluster
+
+// Copyright (c) Microsoft Corporation.
+// Licensed under the Apache License 2.0.
+
+import (
+	"context"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/sirupsen/logrus"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	k8sfake "k8s.io/client-go/kubernetes/fake"
+
+	"github.com/Azure/ARO-RP/pkg/api"
+	testdatabase "github.com/Azure/ARO-RP/test/database"
+)
+
+func TestPopulateCreatedAt(t *testing.T) {
+	ctx := context.Background()
+	resourceID := "/subscriptions/00000000-0000-0000-0000-000000000000/resourcegroups/resourceGroup/providers/microsoft.redhatopenshift/openshiftclusters/resourceName"
+	mockCreationTimestamp := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
+
+	for _, tt := range []struct {
+		name     string
+		ns       *corev1.Namespace
+		doc      *api.OpenShiftClusterDocument
+		wantTime time.Time
+		wantErr  string
+	}{
+		{
+			name: "doc without timestamp",
+			ns: &corev1.Namespace{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:              "default",
+					CreationTimestamp: metav1.NewTime(mockCreationTimestamp),
+				},
+			},
+			doc: &api.OpenShiftClusterDocument{
+				Key: strings.ToLower(resourceID),
+				OpenShiftCluster: &api.OpenShiftCluster{
+					ID: resourceID,
+					Properties: api.OpenShiftClusterProperties{
+						ProvisioningState: api.ProvisioningStateAdminUpdating,
+					},
+				},
+			},
+			wantTime: mockCreationTimestamp,
+		},
+		{
+			name: "doc with timestamp",
+			ns: &corev1.Namespace{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:              "default",
+					CreationTimestamp: metav1.NewTime(mockCreationTimestamp),
+				},
+			},
+			doc: &api.OpenShiftClusterDocument{
+				Key: strings.ToLower(resourceID),
+				OpenShiftCluster: &api.OpenShiftCluster{
+					ID: resourceID,
+					Properties: api.OpenShiftClusterProperties{
+						CreatedAt:         time.Date(1970, 1, 1, 0, 0, 0, 1, time.UTC),
+						ProvisioningState: api.ProvisioningStateAdminUpdating,
+					},
+				},
+			},
+			wantTime: time.Date(1970, 1, 1, 0, 0, 0, 1, time.UTC),
+		},
+		{
+			name: "default namespace doesn't exist",
+			ns:   &corev1.Namespace{},
+			doc: &api.OpenShiftClusterDocument{
+				Key: strings.ToLower(resourceID),
+				OpenShiftCluster: &api.OpenShiftCluster{
+					ID: resourceID,
+					Properties: api.OpenShiftClusterProperties{
+						ProvisioningState: api.ProvisioningStateAdminUpdating,
+					},
+				},
+			},
+			wantErr: "namespaces \"default\" not found",
+		},
+	} {
+		t.Run(tt.name, func(t *testing.T) {
+			fakeOpenShiftClustersDatabase, _ := testdatabase.NewFakeOpenShiftClusters()
+			fixture := testdatabase.NewFixture().WithOpenShiftClusters(fakeOpenShiftClustersDatabase)
+			fixture.AddOpenShiftClusterDocuments(tt.doc)
+			err := fixture.Create()
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			clusterdoc, err := fakeOpenShiftClustersDatabase.Dequeue(ctx)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			m := &manager{
+				log:           logrus.NewEntry(logrus.StandardLogger()),
+				kubernetescli: k8sfake.NewSimpleClientset(tt.ns),
+				doc:           clusterdoc,
+				db:            fakeOpenShiftClustersDatabase,
+			}
+
+			err = m.populateCreatedAt(ctx)
+			if err != nil && err.Error() != tt.wantErr ||
+				err == nil && tt.wantErr != "" {
+				t.Fatal(err)
+			}
+
+			if err == nil {
+				if tt.wantErr != "" {
+					t.Error(err)
+				}
+
+				doc, err := fakeOpenShiftClustersDatabase.Get(ctx, strings.ToLower(resourceID))
+				if err != nil {
+					t.Fatal(err)
+				}
+				if tt.wantTime != doc.OpenShiftCluster.Properties.CreatedAt {
+					t.Error(doc.OpenShiftCluster.Properties.CreatedAt)
+				}
+				if tt.wantTime != m.doc.OpenShiftCluster.Properties.CreatedAt {
+					t.Error(m.doc.OpenShiftCluster.Properties.CreatedAt)
+				}
+			} else {
+				if err.Error() != tt.wantErr {
+					t.Error(err)
+				}
+			}
+		})
+	}
+}
diff --git a/pkg/frontend/openshiftcluster_putorpatch_test.go b/pkg/frontend/openshiftcluster_putorpatch_test.go
index 013356996..b9e2baffb 100644
--- a/pkg/frontend/openshiftcluster_putorpatch_test.go
+++ b/pkg/frontend/openshiftcluster_putorpatch_test.go
@@ -9,6 +9,7 @@ import (
 	"net/http"
 	"strings"
 	"testing"
+	"time"
 
 	"github.com/Azure/ARO-RP/pkg/api"
 	admin "github.com/Azure/ARO-RP/pkg/api/admin"
@@ -281,6 +282,7 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) {
 	}
 
 	mockSubID := "00000000-0000-0000-0000-000000000000"
+	mockCurrentTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
 
 	type test struct {
 		name           string
@@ -331,6 +333,7 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) {
 							ArchitectureVersion: version.InstallArchitectureVersion,
 							ProvisioningState:   api.ProvisioningStateCreating,
 							ProvisionedBy:       version.GitCommit,
+							CreatedAt:           mockCurrentTime,
 							CreatedBy:           version.GitCommit,
 							ClusterProfile: api.ClusterProfile{
 								Version: "4.3.0",
@@ -878,6 +881,7 @@ func TestPutOrPatchOpenShiftCluster(t *testing.T) {
 			}
 			f.(*frontend).bucketAllocator = bucket.Fixed(1)
 			f.(*frontend).ocEnricher = ti.enricher
+			f.(*frontend).now = func() time.Time { return mockCurrentTime }
 
 			go f.Run(ctx, nil, nil)
 
diff --git a/pkg/monitor/cluster/summary_test.go b/pkg/monitor/cluster/summary_test.go
index bb1a8463c..d8509aef9 100644
--- a/pkg/monitor/cluster/summary_test.go
+++ b/pkg/monitor/cluster/summary_test.go
@@ -6,6 +6,7 @@ package cluster
 import (
 	"context"
 	"testing"
+	"time"
 
 	"github.com/golang/mock/gomock"
 	configv1 "github.com/openshift/api/config/v1"
@@ -68,6 +69,8 @@ func TestEmitSummary(t *testing.T) {
 
 	m := mock_metrics.NewMockInterface(controller)
 
+	mockCreatedAt := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
+
 	mon := &Monitor{
 		configcli: configcli,
 		cli:       cli,
@@ -75,6 +78,7 @@ func TestEmitSummary(t *testing.T) {
 		oc: &api.OpenShiftCluster{
 			Properties: api.OpenShiftClusterProperties{
 				ProvisioningState: api.ProvisioningStateFailed,
+				CreatedAt:         mockCreatedAt,
 			},
 		},
 		hourlyRun: true,
@@ -86,6 +90,7 @@ func TestEmitSummary(t *testing.T) {
 		"masterCount":       "1",
 		"workerCount":       "2",
 		"provisioningState": api.ProvisioningStateFailed.String(),
+		"createdAt":         mockCreatedAt.String(),
 	})
 
 	err := mon.emitSummary(ctx)