Enhance the tctl notifications command (#48829)

- Add the ability create notifications with a custom TTL
  (previously we relied on the server-provided default of 30 days)
- Add the ability to create notifications with custom labels
- Add (non-system) labels to the `tctl notifications ls` output
- Add the ability to filter by label when listing notifications

Closes #48631
This commit is contained in:
Zac Bergquist
2024-11-13 17:24:38 +00:00
committed by GitHub
parent b6776b4a87
commit 2298901e61
5 changed files with 156 additions and 60 deletions
@@ -228,6 +228,8 @@ type NotificationFilters struct {
GlobalOnly bool `protobuf:"varint,2,opt,name=global_only,json=globalOnly,proto3" json:"global_only,omitempty"`
// user_created_only is whether to only list user-created notifications (ie. notifications created by an admin via the tctl interface).
UserCreatedOnly bool `protobuf:"varint,3,opt,name=user_created_only,json=userCreatedOnly,proto3" json:"user_created_only,omitempty"`
// labels is used to request only notifications with specific labels.
Labels map[string]string `protobuf:"bytes,4,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
func (x *NotificationFilters) Reset() {
@@ -281,6 +283,13 @@ func (x *NotificationFilters) GetUserCreatedOnly() bool {
return false
}
func (x *NotificationFilters) GetLabels() map[string]string {
if x != nil {
return x.Labels
}
return nil
}
// ListNotificationsResponse is the response from listing a user's notifications.
type ListNotificationsResponse struct {
state protoimpl.MessageState
@@ -592,15 +601,24 @@ var file_teleport_notifications_v1_notifications_service_proto_rawDesc = []byte{
0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69,
0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x52, 0x07, 0x66, 0x69, 0x6c, 0x74,
0x65, 0x72, 0x73, 0x22, 0x7e, 0x0a, 0x13, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x75, 0x73,
0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75, 0x73,
0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c,
0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x67, 0x6c, 0x6f,
0x62, 0x61, 0x6c, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x75, 0x73, 0x65, 0x72, 0x5f,
0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x03, 0x20, 0x01,
0x28, 0x08, 0x52, 0x0f, 0x75, 0x73, 0x65, 0x72, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x4f,
0x6e, 0x6c, 0x79, 0x22, 0x80, 0x02, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69,
0x65, 0x72, 0x73, 0x22, 0x8d, 0x02, 0x0a, 0x13, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x75,
0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x75,
0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x67, 0x6c, 0x6f, 0x62, 0x61,
0x6c, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x67, 0x6c,
0x6f, 0x62, 0x61, 0x6c, 0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x2a, 0x0a, 0x11, 0x75, 0x73, 0x65, 0x72,
0x5f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x03, 0x20,
0x01, 0x28, 0x08, 0x52, 0x0f, 0x75, 0x73, 0x65, 0x72, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64,
0x4f, 0x6e, 0x6c, 0x79, 0x12, 0x52, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x04,
0x20, 0x03, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e,
0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x76, 0x31,
0x2e, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x69, 0x6c,
0x74, 0x65, 0x72, 0x73, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79,
0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65,
0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c,
0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a,
0x02, 0x38, 0x01, 0x22, 0x80, 0x02, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x4e, 0x6f, 0x74, 0x69,
0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x4d, 0x0a, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70,
@@ -731,7 +749,7 @@ func file_teleport_notifications_v1_notifications_service_proto_rawDescGZIP() []
return file_teleport_notifications_v1_notifications_service_proto_rawDescData
}
var file_teleport_notifications_v1_notifications_service_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
var file_teleport_notifications_v1_notifications_service_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
var file_teleport_notifications_v1_notifications_service_proto_goTypes = []any{
(*CreateUserNotificationRequest)(nil), // 0: teleport.notifications.v1.CreateUserNotificationRequest
(*DeleteUserNotificationRequest)(nil), // 1: teleport.notifications.v1.DeleteUserNotificationRequest
@@ -742,40 +760,42 @@ var file_teleport_notifications_v1_notifications_service_proto_goTypes = []any{
(*DeleteGlobalNotificationRequest)(nil), // 6: teleport.notifications.v1.DeleteGlobalNotificationRequest
(*UpsertUserNotificationStateRequest)(nil), // 7: teleport.notifications.v1.UpsertUserNotificationStateRequest
(*UpsertUserLastSeenNotificationRequest)(nil), // 8: teleport.notifications.v1.UpsertUserLastSeenNotificationRequest
(*Notification)(nil), // 9: teleport.notifications.v1.Notification
(*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp
(*GlobalNotification)(nil), // 11: teleport.notifications.v1.GlobalNotification
(*UserNotificationState)(nil), // 12: teleport.notifications.v1.UserNotificationState
(*UserLastSeenNotification)(nil), // 13: teleport.notifications.v1.UserLastSeenNotification
(*emptypb.Empty)(nil), // 14: google.protobuf.Empty
nil, // 9: teleport.notifications.v1.NotificationFilters.LabelsEntry
(*Notification)(nil), // 10: teleport.notifications.v1.Notification
(*timestamppb.Timestamp)(nil), // 11: google.protobuf.Timestamp
(*GlobalNotification)(nil), // 12: teleport.notifications.v1.GlobalNotification
(*UserNotificationState)(nil), // 13: teleport.notifications.v1.UserNotificationState
(*UserLastSeenNotification)(nil), // 14: teleport.notifications.v1.UserLastSeenNotification
(*emptypb.Empty)(nil), // 15: google.protobuf.Empty
}
var file_teleport_notifications_v1_notifications_service_proto_depIdxs = []int32{
9, // 0: teleport.notifications.v1.CreateUserNotificationRequest.notification:type_name -> teleport.notifications.v1.Notification
10, // 0: teleport.notifications.v1.CreateUserNotificationRequest.notification:type_name -> teleport.notifications.v1.Notification
3, // 1: teleport.notifications.v1.ListNotificationsRequest.filters:type_name -> teleport.notifications.v1.NotificationFilters
9, // 2: teleport.notifications.v1.ListNotificationsResponse.notifications:type_name -> teleport.notifications.v1.Notification
10, // 3: teleport.notifications.v1.ListNotificationsResponse.user_last_seen_notification_timestamp:type_name -> google.protobuf.Timestamp
11, // 4: teleport.notifications.v1.CreateGlobalNotificationRequest.global_notification:type_name -> teleport.notifications.v1.GlobalNotification
12, // 5: teleport.notifications.v1.UpsertUserNotificationStateRequest.user_notification_state:type_name -> teleport.notifications.v1.UserNotificationState
13, // 6: teleport.notifications.v1.UpsertUserLastSeenNotificationRequest.user_last_seen_notification:type_name -> teleport.notifications.v1.UserLastSeenNotification
0, // 7: teleport.notifications.v1.NotificationService.CreateUserNotification:input_type -> teleport.notifications.v1.CreateUserNotificationRequest
1, // 8: teleport.notifications.v1.NotificationService.DeleteUserNotification:input_type -> teleport.notifications.v1.DeleteUserNotificationRequest
5, // 9: teleport.notifications.v1.NotificationService.CreateGlobalNotification:input_type -> teleport.notifications.v1.CreateGlobalNotificationRequest
6, // 10: teleport.notifications.v1.NotificationService.DeleteGlobalNotification:input_type -> teleport.notifications.v1.DeleteGlobalNotificationRequest
2, // 11: teleport.notifications.v1.NotificationService.ListNotifications:input_type -> teleport.notifications.v1.ListNotificationsRequest
7, // 12: teleport.notifications.v1.NotificationService.UpsertUserNotificationState:input_type -> teleport.notifications.v1.UpsertUserNotificationStateRequest
8, // 13: teleport.notifications.v1.NotificationService.UpsertUserLastSeenNotification:input_type -> teleport.notifications.v1.UpsertUserLastSeenNotificationRequest
9, // 14: teleport.notifications.v1.NotificationService.CreateUserNotification:output_type -> teleport.notifications.v1.Notification
14, // 15: teleport.notifications.v1.NotificationService.DeleteUserNotification:output_type -> google.protobuf.Empty
11, // 16: teleport.notifications.v1.NotificationService.CreateGlobalNotification:output_type -> teleport.notifications.v1.GlobalNotification
14, // 17: teleport.notifications.v1.NotificationService.DeleteGlobalNotification:output_type -> google.protobuf.Empty
4, // 18: teleport.notifications.v1.NotificationService.ListNotifications:output_type -> teleport.notifications.v1.ListNotificationsResponse
12, // 19: teleport.notifications.v1.NotificationService.UpsertUserNotificationState:output_type -> teleport.notifications.v1.UserNotificationState
13, // 20: teleport.notifications.v1.NotificationService.UpsertUserLastSeenNotification:output_type -> teleport.notifications.v1.UserLastSeenNotification
14, // [14:21] is the sub-list for method output_type
7, // [7:14] is the sub-list for method input_type
7, // [7:7] is the sub-list for extension type_name
7, // [7:7] is the sub-list for extension extendee
0, // [0:7] is the sub-list for field type_name
9, // 2: teleport.notifications.v1.NotificationFilters.labels:type_name -> teleport.notifications.v1.NotificationFilters.LabelsEntry
10, // 3: teleport.notifications.v1.ListNotificationsResponse.notifications:type_name -> teleport.notifications.v1.Notification
11, // 4: teleport.notifications.v1.ListNotificationsResponse.user_last_seen_notification_timestamp:type_name -> google.protobuf.Timestamp
12, // 5: teleport.notifications.v1.CreateGlobalNotificationRequest.global_notification:type_name -> teleport.notifications.v1.GlobalNotification
13, // 6: teleport.notifications.v1.UpsertUserNotificationStateRequest.user_notification_state:type_name -> teleport.notifications.v1.UserNotificationState
14, // 7: teleport.notifications.v1.UpsertUserLastSeenNotificationRequest.user_last_seen_notification:type_name -> teleport.notifications.v1.UserLastSeenNotification
0, // 8: teleport.notifications.v1.NotificationService.CreateUserNotification:input_type -> teleport.notifications.v1.CreateUserNotificationRequest
1, // 9: teleport.notifications.v1.NotificationService.DeleteUserNotification:input_type -> teleport.notifications.v1.DeleteUserNotificationRequest
5, // 10: teleport.notifications.v1.NotificationService.CreateGlobalNotification:input_type -> teleport.notifications.v1.CreateGlobalNotificationRequest
6, // 11: teleport.notifications.v1.NotificationService.DeleteGlobalNotification:input_type -> teleport.notifications.v1.DeleteGlobalNotificationRequest
2, // 12: teleport.notifications.v1.NotificationService.ListNotifications:input_type -> teleport.notifications.v1.ListNotificationsRequest
7, // 13: teleport.notifications.v1.NotificationService.UpsertUserNotificationState:input_type -> teleport.notifications.v1.UpsertUserNotificationStateRequest
8, // 14: teleport.notifications.v1.NotificationService.UpsertUserLastSeenNotification:input_type -> teleport.notifications.v1.UpsertUserLastSeenNotificationRequest
10, // 15: teleport.notifications.v1.NotificationService.CreateUserNotification:output_type -> teleport.notifications.v1.Notification
15, // 16: teleport.notifications.v1.NotificationService.DeleteUserNotification:output_type -> google.protobuf.Empty
12, // 17: teleport.notifications.v1.NotificationService.CreateGlobalNotification:output_type -> teleport.notifications.v1.GlobalNotification
15, // 18: teleport.notifications.v1.NotificationService.DeleteGlobalNotification:output_type -> google.protobuf.Empty
4, // 19: teleport.notifications.v1.NotificationService.ListNotifications:output_type -> teleport.notifications.v1.ListNotificationsResponse
13, // 20: teleport.notifications.v1.NotificationService.UpsertUserNotificationState:output_type -> teleport.notifications.v1.UserNotificationState
14, // 21: teleport.notifications.v1.NotificationService.UpsertUserLastSeenNotification:output_type -> teleport.notifications.v1.UserLastSeenNotification
15, // [15:22] is the sub-list for method output_type
8, // [8:15] is the sub-list for method input_type
8, // [8:8] is the sub-list for extension type_name
8, // [8:8] is the sub-list for extension extendee
0, // [0:8] is the sub-list for field type_name
}
func init() { file_teleport_notifications_v1_notifications_service_proto_init() }
@@ -790,7 +810,7 @@ func file_teleport_notifications_v1_notifications_service_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_teleport_notifications_v1_notifications_service_proto_rawDesc,
NumEnums: 0,
NumMessages: 9,
NumMessages: 10,
NumExtensions: 0,
NumServices: 1,
},
@@ -82,6 +82,8 @@ message NotificationFilters {
bool global_only = 2;
// user_created_only is whether to only list user-created notifications (ie. notifications created by an admin via the tctl interface).
bool user_created_only = 3;
// labels is used to request only notifications with specific labels.
map<string, string> labels = 4;
}
// ListNotificationsResponse is the response from listing a user's notifications.
@@ -107,12 +107,25 @@ func NewService(cfg ServiceConfig) (*Service, error) {
// ListNotifications returns a paginated list of notifications which match the user.
func (s *Service) ListNotifications(ctx context.Context, req *notificationsv1.ListNotificationsRequest) (*notificationsv1.ListNotificationsResponse, error) {
labelsMatch := func(resourceLabels map[string]string) bool {
if req.Filters == nil || len(req.Filters.Labels) == 0 {
// no labels to match against
return true
}
for k, v := range req.Filters.Labels {
if resourceLabels[k] != v {
return false
}
}
return true
}
if req.Filters != nil {
if req.Filters.GlobalOnly {
return s.listGlobalNotifications(ctx, req.Filters.UserCreatedOnly, req.PageToken, req.PageSize)
return s.listGlobalNotifications(ctx, req)
}
if req.Filters.Username != "" {
return s.listUserSpecificNotificationsForUser(ctx, req.Filters.Username, req.Filters.UserCreatedOnly, req.PageToken, req.PageSize)
return s.listUserSpecificNotificationsForUser(ctx, req)
}
return nil, trace.BadParameter("Invalid filters were provided, exactly one of GlobalOnly or Username must be defined.")
@@ -168,6 +181,10 @@ func (s *Service) ListNotifications(ctx context.Context, req *notificationsv1.Li
return nil, false
}
if !labelsMatch(n.GetMetadata().GetLabels()) {
return nil, false
}
if !userNotifMatchFn(n) {
return nil, false
}
@@ -184,6 +201,10 @@ func (s *Service) ListNotifications(ctx context.Context, req *notificationsv1.Li
return nil, false
}
if !labelsMatch(gn.GetMetadata().GetLabels()) {
return nil, false
}
if !s.matchGlobalNotification(ctx, authCtx, gn, notificationStatesMap) {
return nil, false
}
@@ -583,8 +604,8 @@ func (s *Service) DeleteUserNotification(ctx context.Context, req *notifications
}
// listUserSpecificNotificationsForUser returns a paginated list of all user-specific notifications for a user. This should only be used by admins.
func (s *Service) listUserSpecificNotificationsForUser(ctx context.Context, username string, userCreatedOnly bool, pageToken string, pageSize int32) (*notificationsv1.ListNotificationsResponse, error) {
if username == "" {
func (s *Service) listUserSpecificNotificationsForUser(ctx context.Context, req *notificationsv1.ListNotificationsRequest) (*notificationsv1.ListNotificationsResponse, error) {
if req.GetFilters().GetUsername() == "" {
return nil, trace.BadParameter("missing username")
}
@@ -602,15 +623,21 @@ func (s *Service) listUserSpecificNotificationsForUser(ctx context.Context, user
}
stream := stream.FilterMap(
s.userNotificationCache.StreamUserNotifications(ctx, username, pageToken),
s.userNotificationCache.StreamUserNotifications(ctx, req.GetFilters().GetUsername(), req.GetPageToken()),
func(n *notificationsv1.Notification) (*notificationsv1.Notification, bool) {
// If only user-created notifications are requested, filter by the user-creatd subkinds.
if userCreatedOnly &&
// If only user-created notifications are requested, filter by the user-created subkinds.
if req.GetFilters().GetUserCreatedOnly() &&
n.GetSubKind() != types.NotificationUserCreatedInformationalSubKind &&
n.GetSubKind() != types.NotificationUserCreatedWarningSubKind {
return nil, false
}
for k, v := range req.GetFilters().GetLabels() {
if n.GetMetadata().GetLabels()[k] != v {
return nil, false
}
}
return n, true
})
@@ -621,7 +648,7 @@ func (s *Service) listUserSpecificNotificationsForUser(ctx context.Context, user
item := stream.Item()
if item != nil {
notifications = append(notifications, item)
if len(notifications) == int(pageSize) {
if len(notifications) == int(req.GetPageSize()) {
nextKey = item.GetMetadata().GetName()
break
}
@@ -635,7 +662,7 @@ func (s *Service) listUserSpecificNotificationsForUser(ctx context.Context, user
}
// listGlobalNotifications returns a paginated list of all global notifications. This should only be used by admins.
func (s *Service) listGlobalNotifications(ctx context.Context, userCreatedOnly bool, pageToken string, pageSize int32) (*notificationsv1.ListNotificationsResponse, error) {
func (s *Service) listGlobalNotifications(ctx context.Context, req *notificationsv1.ListNotificationsRequest) (*notificationsv1.ListNotificationsResponse, error) {
authCtx, err := s.authorizer.Authorize(ctx)
if err != nil {
return nil, trace.Wrap(err)
@@ -646,15 +673,24 @@ func (s *Service) listGlobalNotifications(ctx context.Context, userCreatedOnly b
}
stream := stream.FilterMap(
s.globalNotificationCache.StreamGlobalNotifications(ctx, pageToken),
s.globalNotificationCache.StreamGlobalNotifications(ctx, req.GetPageToken()),
func(gn *notificationsv1.GlobalNotification) (*notificationsv1.GlobalNotification, bool) {
// If only user-created notifications are requested, filter by the user-creatd subkinds.
if userCreatedOnly &&
if req.GetFilters().GetUserCreatedOnly() &&
gn.GetSpec().GetNotification().GetSubKind() != types.NotificationUserCreatedInformationalSubKind &&
gn.GetSpec().GetNotification().GetSubKind() != types.NotificationUserCreatedWarningSubKind {
return nil, false
}
// Pay special attention to the fact that we match against labels on the
// inner-notification resource spec, not the labels in the GlobalNotification's
// resource metadata.
for k, v := range req.GetFilters().GetLabels() {
if gn.GetSpec().GetNotification().GetMetadata().GetLabels()[k] != v {
return nil, false
}
}
return gn, true
})
@@ -669,7 +705,7 @@ func (s *Service) listGlobalNotifications(ctx context.Context, userCreatedOnly b
notifications = append(notifications, notification)
if len(notifications) == int(pageSize) {
if len(notifications) == int(req.GetPageSize()) {
nextKey = item.GetMetadata().GetName()
break
}
+27 -5
View File
@@ -29,6 +29,7 @@ import (
"github.com/alecthomas/kingpin/v2"
"github.com/gravitational/trace"
"github.com/gravitational/trace/trail"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/defaults"
@@ -38,8 +39,10 @@ import (
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/asciitable"
"github.com/gravitational/teleport/lib/auth/authclient"
libclient "github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/tool/common"
)
// NotificationCommand implements the `tctl notifications` family of commands.
@@ -58,6 +61,8 @@ type NotificationCommand struct {
title string
content string
labels string
ttl time.Duration
// stdout allows to switch the standard output source. Used in tests.
stdout io.Writer
@@ -74,11 +79,14 @@ func (n *NotificationCommand) Initialize(app *kingpin.Application, _ *servicecfg
n.create.Flag("title", "Set the notification's title.").Short('t').Required().StringVar(&n.title)
n.create.Flag("content", "Set the notification's content.").Required().StringVar(&n.content)
n.create.Flag("warning", "Set whether this notification is a warning notification.").BoolVar(&n.warning)
n.create.Flag("ttl", "Time duration after which the notification expires (default 30 days).").Default("30d").DurationVar(&n.ttl)
n.create.Flag("labels", "List of labels to attach to the notification. For example: key1=value1,key2=value2.").StringVar(&n.labels)
n.ls = notif.Command("ls", "List notifications which were manually created using `tctl notifications create`. By default, this will list notifications capable of targeting multiple users, such as role-based ones. To list notifications directed only at a specific user, use the --user flag. To include notifications generated by Teleport, use --all.")
n.ls.Flag("user", "Set which user to list user-specific notifications for.").StringVar(&n.user)
n.ls.Flag("format", "Output format, 'yaml', 'json', or 'text'").Default(teleport.Text).EnumVar(&n.format, teleport.YAML, teleport.JSON, teleport.Text)
n.ls.Flag("all", "Set whether all notifications should be included, including those generated by Teleport, as opposed to solely those created using `tctl notifications create`.").BoolVar(&n.allNotifications)
n.ls.Flag("labels", labelHelp).StringVar(&n.labels)
n.rm = notif.Command("rm", "Remove a cluster notification.").Alias("remove")
n.rm.Flag("user", "The user the notification to remove belongs to, if any.").StringVar(&n.user)
@@ -108,11 +116,17 @@ func (n *NotificationCommand) TryRun(ctx context.Context, cmd string, client *au
// Create creates a new notification.
func (n *NotificationCommand) Create(ctx context.Context, client *authclient.Client) error {
labels, err := libclient.ParseLabelSpec(n.labels)
if err != nil {
return trace.Wrap(err)
}
labels[types.NotificationTitleLabel] = n.title
labels[types.NotificationTextContentLabel] = n.content
meta := &headerv1.Metadata{
Labels: map[string]string{
types.NotificationTitleLabel: n.title,
types.NotificationTextContentLabel: n.content,
},
Expires: timestamppb.New(time.Now().Add(n.ttl)),
Labels: labels,
}
subKind := types.NotificationUserCreatedInformationalSubKind
@@ -219,6 +233,11 @@ func (n *NotificationCommand) Create(ctx context.Context, client *authclient.Cli
}
func (n *NotificationCommand) List(ctx context.Context, client notificationspb.NotificationServiceClient) error {
labels, err := libclient.ParseLabelSpec(n.labels)
if err != nil {
return trace.Wrap(err)
}
var result []*notificationspb.Notification
var pageToken string
for {
@@ -233,6 +252,7 @@ func (n *NotificationCommand) List(ctx context.Context, client notificationspb.N
Filters: &notificationspb.NotificationFilters{
Username: n.user,
UserCreatedOnly: !n.allNotifications,
Labels: labels,
},
})
if err != nil {
@@ -245,6 +265,7 @@ func (n *NotificationCommand) List(ctx context.Context, client notificationspb.N
Filters: &notificationspb.NotificationFilters{
GlobalOnly: true,
UserCreatedOnly: !n.allNotifications,
Labels: labels,
},
})
if err != nil {
@@ -266,13 +287,14 @@ func (n *NotificationCommand) List(ctx context.Context, client notificationspb.N
func displayNotifications(format string, notifications []*notificationspb.Notification, w io.Writer) {
switch format {
case teleport.Text:
table := asciitable.MakeTable([]string{"ID", "Created", "Expires", "Title"})
table := asciitable.MakeTable([]string{"ID", "Created", "Expires", "Title", "Labels"})
for _, n := range notifications {
table.AddRow([]string{
n.GetMetadata().GetName(),
n.GetSpec().GetCreated().AsTime().Format(time.RFC822),
n.GetMetadata().GetExpires().AsTime().Format(time.RFC822),
n.GetMetadata().GetLabels()[types.NotificationTitleLabel],
common.FormatLabels(n.GetMetadata().GetLabels(), false),
})
}
fmt.Fprint(w, table.AsBuffer().String())
+17 -1
View File
@@ -64,7 +64,11 @@ func TestNotificationCommmandCRUD(t *testing.T) {
require.Contains(t, buf.String(), "for user manager-user")
// Test creating a global notification for users with the test-1 role.
buf, err = runNotificationsCommand(t, clt, []string{"create", "--roles", "test-1", "--title", "test-1 notification", "--content", "This is a test notification."})
buf, err = runNotificationsCommand(t, clt, []string{
"create", "--roles", "test-1", "--title", "test-1 notification",
"--labels", "forrole=test-1",
"--content", "This is a test notification.",
})
require.NoError(t, err)
require.Contains(t, buf.String(), "for users with one or more of the following roles: [test-1]")
globalNotificationId := strings.Split(buf.String(), " ")[2]
@@ -90,6 +94,18 @@ func TestNotificationCommmandCRUD(t *testing.T) {
assert.NotContains(collectT, buf.String(), "auditor notification")
assert.NotContains(collectT, buf.String(), "manager notification")
// Filter out notifications with a non-existent label and make sure nothing comes back.
buf, err = runNotificationsCommand(t, clt, []string{"ls", "--labels=thislabel=doesnotexist"})
assert.NotContains(collectT, buf.String(), "test-1 notification")
assert.NotContains(collectT, buf.String(), "auditor notification")
assert.NotContains(collectT, buf.String(), "manager notification")
// Filter out global notifications with a valid label.
buf, err = runNotificationsCommand(t, clt, []string{"ls", "--labels=forrole=test-1"})
assert.Contains(collectT, buf.String(), "test-1 notification")
assert.NotContains(collectT, buf.String(), "auditor notification")
assert.NotContains(collectT, buf.String(), "manager notification")
}, 3*time.Second, 100*time.Millisecond)
// Delete the auditor's user-specific notification.