diff --git a/fleetshard/pkg/central/reconciler/reconciler.go b/fleetshard/pkg/central/reconciler/reconciler.go index acad53ef5e..5d7b00c4a7 100644 --- a/fleetshard/pkg/central/reconciler/reconciler.go +++ b/fleetshard/pkg/central/reconciler/reconciler.go @@ -31,6 +31,7 @@ import ( "github.com/stackrox/rox/operator/apis/platform/v1alpha1" "github.com/stackrox/rox/pkg/declarativeconfig" "github.com/stackrox/rox/pkg/random" + "golang.org/x/exp/maps" "gopkg.in/yaml.v2" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chartutil" @@ -62,6 +63,7 @@ const ( instanceTypeLabelKey = "rhacs.redhat.com/instance-type" orgIDLabelKey = "rhacs.redhat.com/org-id" tenantIDLabelKey = "rhacs.redhat.com/tenant" + centralExpiredAtKey = "rhacs.redhat.com/expired-at" auditLogNotifierKey = "com.redhat.rhacs.auditLogNotifier" auditLogNotifierName = "Platform Audit Logs" @@ -200,7 +202,10 @@ func (r *CentralReconciler) Reconcile(ctx context.Context, remoteCentral private namespaceAnnotations := map[string]string{ orgNameAnnotationKey: remoteCentral.Spec.Auth.OwnerOrgName, } - if err := r.ensureNamespaceExists(remoteCentralNamespace, namespaceLabels, namespaceAnnotations); err != nil { + if remoteCentral.Metadata.ExpiredAt != nil { + namespaceAnnotations[centralExpiredAtKey] = remoteCentral.Metadata.ExpiredAt.Format(time.RFC3339) + } + if err := r.reconcileNamespace(ctx, remoteCentralNamespace, namespaceLabels, namespaceAnnotations); err != nil { return nil, errors.Wrapf(err, "unable to ensure that namespace %s exists", remoteCentralNamespace) } @@ -309,11 +314,11 @@ func (r *CentralReconciler) applyCentralConfig(remoteCentral *private.ManagedCen r.applyRoutes(central) r.applyProxyConfig(central) r.applyDeclarativeConfig(central) - r.applyAnnotations(central) + r.applyAnnotations(remoteCentral, central) return nil } -func (r *CentralReconciler) applyAnnotations(central *v1alpha1.Central) { +func (r *CentralReconciler) applyAnnotations(remoteCentral *private.ManagedCentral, central *v1alpha1.Central) { if central.Spec.Customize == nil { central.Spec.Customize = &v1alpha1.CustomizeSpec{} } @@ -322,6 +327,9 @@ func (r *CentralReconciler) applyAnnotations(central *v1alpha1.Central) { } central.Spec.Customize.Annotations[envAnnotationKey] = r.environment central.Spec.Customize.Annotations[clusterNameAnnotationKey] = r.clusterName + if remoteCentral.Metadata.ExpiredAt != nil { + central.Spec.Customize.Annotations[centralExpiredAtKey] = remoteCentral.Metadata.ExpiredAt.Format(time.RFC3339) + } } func (r *CentralReconciler) applyDeclarativeConfig(central *v1alpha1.Central) { @@ -668,6 +676,13 @@ func (r *CentralReconciler) reconcileCentral(ctx context.Context, remoteCentral centralExists = false } + if remoteCentral.Metadata.ExpiredAt != nil { + if central.GetAnnotations() == nil { + central.Annotations = map[string]string{} + } + central.Annotations[centralExpiredAtKey] = remoteCentral.Metadata.ExpiredAt.Format(time.RFC3339) + } + if !centralExists { if central.GetAnnotations() == nil { central.Annotations = map[string]string{} @@ -1170,15 +1185,24 @@ func (r *CentralReconciler) createTenantNamespace(ctx context.Context, namespace return nil } -func (r *CentralReconciler) ensureNamespaceExists(name string, labels map[string]string, annotations map[string]string) error { +func (r *CentralReconciler) reconcileNamespace(ctx context.Context, name string, labels map[string]string, annotations map[string]string) error { namespace, err := r.getNamespace(name) if err != nil { if apiErrors.IsNotFound(err) { namespace.Annotations = annotations namespace.Labels = labels - return r.createTenantNamespace(context.Background(), namespace) + return r.createTenantNamespace(ctx, namespace) } return fmt.Errorf("getting namespace %s: %w", name, err) + } else if !maps.Equal(labels, namespace.Labels) || + !maps.Equal(annotations, namespace.Annotations) { + namespace.Annotations = annotations + namespace.Labels = labels + if err = r.client.Update(ctx, namespace, &ctrlClient.UpdateOptions{ + FieldManager: "fleetshard-sync", + }); err != nil { + return fmt.Errorf("updating namespace %s: %w", name, err) + } } return nil } diff --git a/fleetshard/pkg/central/reconciler/reconciler_test.go b/fleetshard/pkg/central/reconciler/reconciler_test.go index ac1f007e34..8e2a9dec5c 100644 --- a/fleetshard/pkg/central/reconciler/reconciler_test.go +++ b/fleetshard/pkg/central/reconciler/reconciler_test.go @@ -2153,11 +2153,18 @@ func TestReconciler_applyAnnotations(t *testing.T) { }, }, } - r.applyAnnotations(c) + date := time.Date(2024, 01, 01, 0, 0, 0, 0, time.UTC) + rc := &private.ManagedCentral{ + Metadata: private.ManagedCentralAllOfMetadata{ + ExpiredAt: &date, + }, + } + r.applyAnnotations(rc, c) assert.Equal(t, map[string]string{ "rhacs.redhat.com/environment": "test", "rhacs.redhat.com/cluster-name": "test", "foo": "bar", + "rhacs.redhat.com/expired-at": "2024-01-01T00:00:00Z", }, c.Spec.Customize.Annotations) } diff --git a/internal/dinosaur/pkg/api/admin/private/api/openapi.yaml b/internal/dinosaur/pkg/api/admin/private/api/openapi.yaml index 85bcfaee16..caaf9e40c1 100644 --- a/internal/dinosaur/pkg/api/admin/private/api/openapi.yaml +++ b/internal/dinosaur/pkg/api/admin/private/api/openapi.yaml @@ -342,6 +342,7 @@ paths: name: reason required: true schema: + format: date-time type: string style: form responses: diff --git a/internal/dinosaur/pkg/api/admin/private/api_default.go b/internal/dinosaur/pkg/api/admin/private/api_default.go index b25f54015b..16d4bba3d6 100644 --- a/internal/dinosaur/pkg/api/admin/private/api_default.go +++ b/internal/dinosaur/pkg/api/admin/private/api_default.go @@ -17,6 +17,7 @@ import ( _nethttp "net/http" _neturl "net/url" "strings" + "time" ) // Linger please @@ -885,7 +886,7 @@ UpdateCentralExpiredAtById Update `expired_at` central property @return Central */ -func (a *DefaultApiService) UpdateCentralExpiredAtById(ctx _context.Context, id string, reason string, localVarOptionals *UpdateCentralExpiredAtByIdOpts) (Central, *_nethttp.Response, error) { +func (a *DefaultApiService) UpdateCentralExpiredAtById(ctx _context.Context, id string, reason time.Time, localVarOptionals *UpdateCentralExpiredAtByIdOpts) (Central, *_nethttp.Response, error) { var ( localVarHTTPMethod = _nethttp.MethodPatch localVarPostBody interface{} diff --git a/internal/dinosaur/pkg/api/private/api/openapi.yaml b/internal/dinosaur/pkg/api/private/api/openapi.yaml index 6f653f67e1..7ec6b23486 100644 --- a/internal/dinosaur/pkg/api/private/api/openapi.yaml +++ b/internal/dinosaur/pkg/api/private/api/openapi.yaml @@ -444,6 +444,10 @@ components: additionalProperties: type: string type: object + expired-at: + format: date-time + nullable: true + type: string ManagedCentral_allOf_spec_auth: properties: clientSecret: diff --git a/internal/dinosaur/pkg/api/private/model_managed_central_all_of_metadata.go b/internal/dinosaur/pkg/api/private/model_managed_central_all_of_metadata.go index d50ae48c5e..8abc1c1245 100644 --- a/internal/dinosaur/pkg/api/private/model_managed_central_all_of_metadata.go +++ b/internal/dinosaur/pkg/api/private/model_managed_central_all_of_metadata.go @@ -10,6 +10,10 @@ // Code generated by OpenAPI Generator (https://openapi-generator.tech). DO NOT EDIT. package private +import ( + "time" +) + // ManagedCentralAllOfMetadata struct for ManagedCentralAllOfMetadata type ManagedCentralAllOfMetadata struct { Name string `json:"name,omitempty"` @@ -19,4 +23,5 @@ type ManagedCentralAllOfMetadata struct { DeletionTimestamp string `json:"deletionTimestamp,omitempty"` SecretsStored []string `json:"secretsStored,omitempty"` Secrets map[string]string `json:"secrets,omitempty"` + ExpiredAt *time.Time `json:"expired-at,omitempty"` } diff --git a/internal/dinosaur/pkg/presenters/managedcentral.go b/internal/dinosaur/pkg/presenters/managedcentral.go index df321ab66b..d87de99115 100644 --- a/internal/dinosaur/pkg/presenters/managedcentral.go +++ b/internal/dinosaur/pkg/presenters/managedcentral.go @@ -119,6 +119,7 @@ func (c *ManagedCentralPresenter) presentManagedCentral(gitopsConfig gitops.Conf }, Internal: from.Internal, SecretsStored: getSecretNames(from), // pragma: allowlist secret + ExpiredAt: from.ExpiredAt, }, Spec: private.ManagedCentralAllOfSpec{ Owners: []string{ diff --git a/internal/dinosaur/pkg/services/dinosaur.go b/internal/dinosaur/pkg/services/dinosaur.go index 24e57b029e..932af5465d 100644 --- a/internal/dinosaur/pkg/services/dinosaur.go +++ b/internal/dinosaur/pkg/services/dinosaur.go @@ -129,13 +129,14 @@ type dinosaurService struct { amsClient ocm.AMSClient iamConfig *iam.IAMConfig rhSSODynamicClientsAPI *dynamicClientAPI.AcsTenantsApiService + telemetry *Telemetry } // NewDinosaurService ... func NewDinosaurService(connectionFactory *db.ConnectionFactory, clusterService ClusterService, iamConfig *iam.IAMConfig, dinosaurConfig *config.CentralConfig, dataplaneClusterConfig *config.DataplaneClusterConfig, awsConfig *config.AWSConfig, quotaServiceFactory QuotaServiceFactory, awsClientFactory aws.ClientFactory, - clusterPlacementStrategy ClusterPlacementStrategy, amsClient ocm.AMSClient) DinosaurService { + clusterPlacementStrategy ClusterPlacementStrategy, amsClient ocm.AMSClient, telemetry *Telemetry) DinosaurService { return &dinosaurService{ connectionFactory: connectionFactory, clusterService: clusterService, @@ -148,6 +149,7 @@ func NewDinosaurService(connectionFactory *db.ConnectionFactory, clusterService clusterPlacementStrategy: clusterPlacementStrategy, amsClient: amsClient, rhSSODynamicClientsAPI: dynamicclients.NewDynamicClientsAPI(iamConfig.RedhatSSORealm), + telemetry: telemetry, } } @@ -689,7 +691,7 @@ func (k *dinosaurService) Update(dinosaurRequest *dbapi.CentralRequest) *errors. if err := dbConn.Updates(dinosaurRequest).Error; err != nil { return errors.NewWithCause(errors.ErrorGeneral, err, "Failed to update central") } - + k.telemetry.UpdateTenantProperties(dinosaurRequest) return nil } @@ -702,7 +704,10 @@ func (k *dinosaurService) Updates(dinosaurRequest *dbapi.CentralRequest, fields if err := dbConn.Updates(fields).Error; err != nil { return errors.NewWithCause(errors.ErrorGeneral, err, "Failed to update central") } - + // Get all request properties, not only the ones provided with fields. + if dinosaurRequest, svcErr := k.GetByID(dinosaurRequest.ID); svcErr == nil { + k.telemetry.UpdateTenantProperties(dinosaurRequest) + } return nil } @@ -750,7 +755,7 @@ func (k *dinosaurService) UpdateStatus(id string, status dinosaurConstants.Centr if err := dbConn.Model(&dbapi.CentralRequest{Meta: api.Meta{ID: id}}).Updates(update).Error; err != nil { return true, errors.NewWithCause(errors.ErrorGeneral, err, "Failed to update central status") } - + k.telemetry.UpdateTenantProperties(dinosaur) return true, nil } diff --git a/internal/dinosaur/pkg/services/telemetry.go b/internal/dinosaur/pkg/services/telemetry.go index 69698c75c1..650cc118c4 100644 --- a/internal/dinosaur/pkg/services/telemetry.go +++ b/internal/dinosaur/pkg/services/telemetry.go @@ -61,18 +61,8 @@ func (t *Telemetry) enabled() bool { return t != nil && t.config != nil && t.config.Enabled() } -// setTenantProperties emits a group event that captures meta data of the input central instance. -// Adds the token user to the tenant group. -func (t *Telemetry) setTenantProperties(ctx context.Context, central *dbapi.CentralRequest) { - if !t.enabled() { - return - } - - user, err := t.auth.getUserFromContext(ctx) - if err != nil { - glog.Error(errors.Wrap(err, "cannot get telemetry user from context claims")) - return - } +// getTenantProperties returns the tenant group properties map. +func (t *Telemetry) getTenantProperties(central *dbapi.CentralRequest) map[string]any { props := map[string]any{ "Cloud Account": central.CloudAccountID, "Cloud Provider": central.CloudProvider, @@ -80,13 +70,17 @@ func (t *Telemetry) setTenantProperties(ctx context.Context, central *dbapi.Cent "Organisation ID": central.OrganisationID, "Region": central.Region, "Tenant ID": central.ID, + "Status": central.Status, } - // Group call will issue a supporting Track event to force group properties - // update. - t.config.Telemeter().Group(props, - telemeter.WithUserID(user), - telemeter.WithGroups(TenantGroupName, central.ID), - ) + if central.ExpiredAt != nil { + props["Expired At"] = central.ExpiredAt.UTC().Format(time.RFC3339) + } else { + // An instance may loose its expiration date after quota is granted, so + // we need to reset the group property, hence never report nil time, as + // nil is not a value on Amplitude. + props["Expired At"] = time.Time{}.Format(time.RFC3339) + } + return props } // trackCreationRequested emits a track event that signals the creation request of a Central instance. @@ -123,7 +117,21 @@ func (t *Telemetry) trackCreationRequested(ctx context.Context, tenantID string, // RegisterTenant initializes the tenant group with the associated properties // and issues a following event tracking the central creation request. func (t *Telemetry) RegisterTenant(ctx context.Context, convCentral *dbapi.CentralRequest, isAdmin bool, err error) { - t.setTenantProperties(ctx, convCentral) + user, err := t.auth.getUserFromContext(ctx) + if err != nil { + glog.Error(errors.Wrap(err, "cannot get telemetry user from context claims")) + return + } + + props := t.getTenantProperties(convCentral) + // Adds the token user to the tenant group. + // Group call will issue a supporting Track event to force group properties + // update. + t.config.Telemeter().Group(props, + telemeter.WithUserID(user), + telemeter.WithGroups(TenantGroupName, convCentral.ID), + ) + go func() { // This is to raise the chances for the tenant group properties be // procesed by Segment: @@ -132,6 +140,15 @@ func (t *Telemetry) RegisterTenant(ctx context.Context, convCentral *dbapi.Centr }() } +// UpdateTenant updates tenant group properties. +func (t *Telemetry) UpdateTenantProperties(convCentral *dbapi.CentralRequest) { + props := t.getTenantProperties(convCentral) + // Update tenant group properties from the name of fleet-manager backend. + t.config.Telemeter().Group(props, + telemeter.WithGroups(TenantGroupName, convCentral.ID), + ) +} + // TrackDeletionRequested emits a track event that signals the deletion request of a Central instance. func (t *Telemetry) TrackDeletionRequested(ctx context.Context, tenantID string, isAdmin bool, requestErr error) { if !t.enabled() { diff --git a/openapi/fleet-manager-private-admin.yaml b/openapi/fleet-manager-private-admin.yaml index 0be614dd36..0974c72b2f 100644 --- a/openapi/fleet-manager-private-admin.yaml +++ b/openapi/fleet-manager-private-admin.yaml @@ -222,6 +222,7 @@ paths: name: reason schema: type: string + format: date-time required: true security: - Bearer: [ ] diff --git a/openapi/fleet-manager-private.yaml b/openapi/fleet-manager-private.yaml index a53d7c1daa..9b20766ca6 100644 --- a/openapi/fleet-manager-private.yaml +++ b/openapi/fleet-manager-private.yaml @@ -269,6 +269,10 @@ components: type: object additionalProperties: type: string + expired-at: + type: string + format: date-time + nullable: true spec: type: object properties: