From 04af1d9d9c0e5540d32893c7363cb5e5ce8b801d Mon Sep 17 00:00:00 2001 From: Thorsten Date: Tue, 12 Jan 2021 15:27:14 +0100 Subject: [PATCH 01/56] [#400] Load messages of conversations (#567) * added structure * messages are displayed * better structure for messages in redux * messages being displayed properly * eslint and some small fixes * better loading of messages * fix some suggestions * linting * remove unused code * use HttpClient for messages * restore right version * revision of code Co-authored-by: Aitor Algorta --- frontend/demo/src/App.module.scss | 4 +- frontend/demo/src/App.tsx | 2 +- frontend/demo/src/actions/messages/index.ts | 31 ++++++++ .../images/empty-state/inbox-empty-state.svg | 1 + .../Inbox/ConversationListItem/index.tsx | 4 +- .../Inbox/Messenger/MessageList/Avatar.scss | 9 +++ .../Inbox/Messenger/MessageList/Avatar.tsx | 33 ++++++++ .../Messenger/MessageList/index.module.scss | 9 +++ .../Inbox/Messenger/MessageList/index.tsx | 78 +++++++++++++++++++ .../MessengerContainer/index.module.scss | 38 +++++++++ .../Messenger/MessengerContainer/index.tsx | 36 +++++++++ .../MessengerListItem/index.module.scss | 50 ++++++++++++ .../Messenger/MessengerListItem/index.tsx | 56 +++++++++++++ .../demo/src/pages/Inbox/Messenger/index.scss | 2 +- .../demo/src/pages/Inbox/Messenger/index.tsx | 7 +- frontend/demo/src/pages/Inbox/index.tsx | 4 +- frontend/demo/src/reducers/data/index.ts | 3 + .../demo/src/reducers/data/messages/index.ts | 43 ++++++++++ lib/typescript/httpclient/endpoints/index.ts | 1 + .../httpclient/endpoints/listConversations.ts | 4 +- .../httpclient/endpoints/listMessages.ts | 36 +++++++++ lib/typescript/httpclient/index.ts | 2 + lib/typescript/httpclient/model/Message.ts | 16 +++- .../payload/ListMessagesRequestPayload.ts | 5 ++ .../httpclient/payload/MessagePayload.ts | 8 +- lib/typescript/httpclient/payload/index.ts | 1 + package.json | 2 + yarn.lock | 7 +- 28 files changed, 468 insertions(+), 24 deletions(-) create mode 100644 frontend/demo/src/actions/messages/index.ts create mode 100644 frontend/demo/src/assets/images/empty-state/inbox-empty-state.svg create mode 100644 frontend/demo/src/pages/Inbox/Messenger/MessageList/Avatar.scss create mode 100644 frontend/demo/src/pages/Inbox/Messenger/MessageList/Avatar.tsx create mode 100644 frontend/demo/src/pages/Inbox/Messenger/MessageList/index.module.scss create mode 100644 frontend/demo/src/pages/Inbox/Messenger/MessageList/index.tsx create mode 100644 frontend/demo/src/pages/Inbox/Messenger/MessengerContainer/index.module.scss create mode 100644 frontend/demo/src/pages/Inbox/Messenger/MessengerContainer/index.tsx create mode 100644 frontend/demo/src/pages/Inbox/Messenger/MessengerListItem/index.module.scss create mode 100644 frontend/demo/src/pages/Inbox/Messenger/MessengerListItem/index.tsx create mode 100644 frontend/demo/src/reducers/data/messages/index.ts create mode 100644 lib/typescript/httpclient/endpoints/listMessages.ts create mode 100644 lib/typescript/httpclient/payload/ListMessagesRequestPayload.ts diff --git a/frontend/demo/src/App.module.scss b/frontend/demo/src/App.module.scss index c47a1ee7ac..4ec34471b1 100644 --- a/frontend/demo/src/App.module.scss +++ b/frontend/demo/src/App.module.scss @@ -5,7 +5,7 @@ text-align: center; flex-grow: 1; width: 100vw; - min-height: 100vh; + height: 100vh; margin-left: 250px; overflow: visible; background: var(--color-blue-white); @@ -29,8 +29,8 @@ @include font-base; display: flex; width: 100vw; + height: 100vh; min-width: 550px; - min-height: 100vh; justify-content: center; flex-direction: column; } diff --git a/frontend/demo/src/App.tsx b/frontend/demo/src/App.tsx index 9dac7583b1..fc7e2b5779 100644 --- a/frontend/demo/src/App.tsx +++ b/frontend/demo/src/App.tsx @@ -62,7 +62,7 @@ class App extends Component & RouteComponentPro - + diff --git a/frontend/demo/src/actions/messages/index.ts b/frontend/demo/src/actions/messages/index.ts new file mode 100644 index 0000000000..1ddd704f36 --- /dev/null +++ b/frontend/demo/src/actions/messages/index.ts @@ -0,0 +1,31 @@ +import {Dispatch} from 'redux'; +import {createAction} from 'typesafe-actions'; +import {HttpClient, Message, ResponseMetadataPayload} from 'httpclient'; + +export const MESSAGES_LOADING = '@@messages/LOADING'; + +export const loadingMessagesAction = createAction( + MESSAGES_LOADING, + resolve => (messagesInfo: {conversationId: string; messages: Message[]}) => resolve(messagesInfo) +); + +export function listMessages(conversationId: string) { + return async (dispatch: Dispatch) => { + return HttpClient.listMessages({ + conversationId, + pageSize: 10, + }) + .then((response: {data: Message[]; metadata: ResponseMetadataPayload}) => { + dispatch( + loadingMessagesAction({ + conversationId, + messages: response.data, + }) + ); + return Promise.resolve(true); + }) + .catch((error: Error) => { + return Promise.reject(error); + }); + }; +} diff --git a/frontend/demo/src/assets/images/empty-state/inbox-empty-state.svg b/frontend/demo/src/assets/images/empty-state/inbox-empty-state.svg new file mode 100644 index 0000000000..5cb6c9669b --- /dev/null +++ b/frontend/demo/src/assets/images/empty-state/inbox-empty-state.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/demo/src/pages/Inbox/ConversationListItem/index.tsx b/frontend/demo/src/pages/Inbox/ConversationListItem/index.tsx index 697d3cad91..33df9c34a4 100644 --- a/frontend/demo/src/pages/Inbox/ConversationListItem/index.tsx +++ b/frontend/demo/src/pages/Inbox/ConversationListItem/index.tsx @@ -31,8 +31,8 @@ const mapStateToProps = (state: StateModel) => { const connector = connect(mapStateToProps, null); const FormattedMessage = ({message}: FormattedMessageProps) => { - if (message && message.content) { - return <>{message.content.text}; + if (message && message.content[0]) { + return <>{message.content[0].text}; } return
; }; diff --git a/frontend/demo/src/pages/Inbox/Messenger/MessageList/Avatar.scss b/frontend/demo/src/pages/Inbox/Messenger/MessageList/Avatar.scss new file mode 100644 index 0000000000..28d396dedd --- /dev/null +++ b/frontend/demo/src/pages/Inbox/Messenger/MessageList/Avatar.scss @@ -0,0 +1,9 @@ +.avatarImage { + height: 40px; + width: 40px; + background-color: green; +} + +.noAvatar { + background-color: red; +} diff --git a/frontend/demo/src/pages/Inbox/Messenger/MessageList/Avatar.tsx b/frontend/demo/src/pages/Inbox/Messenger/MessageList/Avatar.tsx new file mode 100644 index 0000000000..7f73202843 --- /dev/null +++ b/frontend/demo/src/pages/Inbox/Messenger/MessageList/Avatar.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import './Avatar.scss'; +import airyAvatarImage from '../../../../assets/images/icons/airy_avatar.svg'; + +type AvatarProps = { + avatarUrl?: string; + isLastMessage: boolean; +}; + +const NoAvatar = () => { + return ( +
+ +
+ ); +}; + +const Avatar = (props: AvatarProps) => { + const {avatarUrl, isLastMessage} = props; + + if (avatarUrl && isLastMessage) { + return ( +
+ +
+ ); + } + + return ; +}; + +export default Avatar; diff --git a/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.module.scss b/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.module.scss new file mode 100644 index 0000000000..56a57d76dc --- /dev/null +++ b/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.module.scss @@ -0,0 +1,9 @@ +.messageList { + display: flex; + flex-direction: column; + padding: 16px; + height: 100%; + overflow-y: scroll; + flex-grow: 1; + overflow-x: hidden; +} diff --git a/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.tsx b/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.tsx new file mode 100644 index 0000000000..0895dd276f --- /dev/null +++ b/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.tsx @@ -0,0 +1,78 @@ +import React, {useEffect, createRef} from 'react'; +import {RouteComponentProps, useParams} from 'react-router-dom'; +import _, {connect, ConnectedProps} from 'react-redux'; +import _redux from 'redux'; + +import {Message} from 'httpclient'; + +import {StateModel} from '../../../../reducers'; +import {MessageById} from '../../../../reducers/data/messages'; + +import MessageListItem from '../MessengerListItem'; + +import {listMessages} from '../../../../actions/messages'; +import {allConversationSelector} from '../../../../selectors/conversations'; + +import styles from './index.module.scss'; + +type MessageListProps = {conversationId: string} & ConnectedProps & + RouteComponentProps<{conversationId: string}>; + +const messagesMapToArray = ( + messageInfo: {[conversationId: string]: MessageById}, + conversationId: string +): Message[] => { + const messageById = messageInfo[conversationId]; + if (messageById) { + return Object.keys(messageById).map((cId: string) => ({...messageById[cId]})); + } + return []; +}; + +const mapStateToProps = (state: StateModel, ownProps: {conversationId: string}) => { + return { + conversations: allConversationSelector(state), + messages: messagesMapToArray(state.data.messages.all, ownProps.conversationId), + }; +}; + +const mapDispatchToProps = { + listMessages, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +const MessageList = (props: MessageListProps) => { + const {listMessages, messages} = props; + const conversationIdParams = useParams(); + const currentConversationId = conversationIdParams[Object.keys(conversationIdParams)[0]]; + + const messageListRef = createRef(); + + useEffect(() => { + currentConversationId && listMessages(currentConversationId); + scrollBottom(); + }, [currentConversationId]); + + const scrollBottom = () => { + messageListRef.current.scrollTop = messageListRef.current.scrollHeight; + }; + + return ( +
+ {messages.map((message: Message) => { + return ( + + ); + })} +
+ ); +}; + +export default connector(MessageList); diff --git a/frontend/demo/src/pages/Inbox/Messenger/MessengerContainer/index.module.scss b/frontend/demo/src/pages/Inbox/Messenger/MessengerContainer/index.module.scss new file mode 100644 index 0000000000..49e1aa191e --- /dev/null +++ b/frontend/demo/src/pages/Inbox/Messenger/MessengerContainer/index.module.scss @@ -0,0 +1,38 @@ +.messengerContainer { + display: flex; + flex: 1; + height: auto; + flex-direction: column; + overflow: hidden; + background-color: #fff; + margin: 16px 8px 0 8px; + border-top-left-radius: 8px; + border-top-right-radius: 8px; +} + +.emptyState { + align-self: center; + margin-top: 8%; + margin-bottom: auto; + max-width: 440px; + h1 { + font-size: 20px; + line-height: 24px; + font-weight: bold; + color: var(--color-airy-blue); + margin-bottom: 8px; + } + p { + font-size: 16px; + line-height: 24px; + font-weight: 400; + color: var(--color-text-gray); + margin-bottom: 32px; + } + svg { + margin-left: 50px; + } +} + +.notSelectedState { +} diff --git a/frontend/demo/src/pages/Inbox/Messenger/MessengerContainer/index.tsx b/frontend/demo/src/pages/Inbox/Messenger/MessengerContainer/index.tsx new file mode 100644 index 0000000000..3e1c13a6c5 --- /dev/null +++ b/frontend/demo/src/pages/Inbox/Messenger/MessengerContainer/index.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import _, {connect, ConnectedProps} from 'react-redux'; +import {StateModel} from '../../../../reducers'; +import MessageList from '../MessageList'; +import {ReactComponent as EmptyStateImage} from '../../../../assets/images/empty-state/inbox-empty-state.svg'; +import styles from './index.module.scss'; + +const mapStateToProps = (state: StateModel) => { + return { + conversations: state.data.conversations.all.items, + }; +}; + +const connector = connect(mapStateToProps, null); + +type MessengerContainerProps = {match: any} & ConnectedProps; + +const MessengerContainer = (props: MessengerContainerProps) => { + const {conversations, match} = props; + + return ( +
+ {!conversations ? ( +
+

Your conversations will appear here as soon as a contact messages you.

+

Airy Messenger only shows new conversations from the moment you connect at least one channel.

+ +
+ ) : ( + + )} +
+ ); +}; + +export default connector(MessengerContainer); diff --git a/frontend/demo/src/pages/Inbox/Messenger/MessengerListItem/index.module.scss b/frontend/demo/src/pages/Inbox/Messenger/MessengerListItem/index.module.scss new file mode 100644 index 0000000000..45194c1f5d --- /dev/null +++ b/frontend/demo/src/pages/Inbox/Messenger/MessengerListItem/index.module.scss @@ -0,0 +1,50 @@ +@import '../../../../assets//scss/colors.scss'; + +.messageListItemContainer { + display: flex; + flex: none; +} + +.messageListItem { + display: flex; + align-self: flex-end; + max-width: 100%; + overflow-wrap: break-word; + word-break: break-word; +} + +.messageListUserContainer { + display: flex; + flex-direction: row; + + img { + height: 40px; + width: 40px; + margin-top: 5px; + } +} + +.messageListItemUser { + display: inline-flex; + align-self: flex-start; + padding: 10px; + margin-top: 5px; + background: var(--color-background-blue); + color: #212428; + position: relative; + text-align: left; + border-radius: 8px; +} + +.messageListItemMember { + margin-top: 5px; + display: inline-flex; + align-self: flex-end; + padding: 10px; + position: relative; + background: var(--color-airy-blue); + color: white; + position: relative; + text-align: left; + border-radius: 8px; +} diff --git a/frontend/demo/src/pages/Inbox/Messenger/MessengerListItem/index.tsx b/frontend/demo/src/pages/Inbox/Messenger/MessengerListItem/index.tsx new file mode 100644 index 0000000000..e2f1f649c3 --- /dev/null +++ b/frontend/demo/src/pages/Inbox/Messenger/MessengerListItem/index.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import _redux from 'redux'; +import _, {connect, ConnectedProps} from 'react-redux'; +import {Message, SenderType} from 'httpclient'; +import {StateModel} from '../../../../reducers'; +import Avatar from '../MessageList/Avatar'; + +import styles from './index.module.scss'; + +type MessengerListItemProps = { + messageText: string; + messageSenderType: string; + messageDate: Date; + message: Message; +} & ConnectedProps; + +const mapStateToProps = (state: StateModel) => { + return { + lastMessages: state.data.conversations.all.items, + }; +}; + +const connector = connect(mapStateToProps, null); + +const MessengerListItem = (props: MessengerListItemProps) => { + const {messageText, messageSenderType, message, lastMessages} = props; + const isUser = messageSenderType !== SenderType.appUser; + + const messageAvatar = (messageId: string) => { + Object.values(lastMessages).forEach(lastMessage => { + return ( + + ); + }); + }; + + return ( +
+
+ {!isUser ? ( +
+ {messageText} + {messageAvatar(message.id)} +
+ ) : ( +
+ {messageAvatar(message.id)} +
{messageText}
+
+ )} +
+
+ ); +}; + +export default connector(MessengerListItem); diff --git a/frontend/demo/src/pages/Inbox/Messenger/index.scss b/frontend/demo/src/pages/Inbox/Messenger/index.scss index e333f9d510..6c59177b52 100644 --- a/frontend/demo/src/pages/Inbox/Messenger/index.scss +++ b/frontend/demo/src/pages/Inbox/Messenger/index.scss @@ -6,7 +6,7 @@ width: 100%; align-items: stretch; overflow: hidden; - margin: 72px 90px 0; + margin: 72px 8px 0 90px; } .messengerContainerMiddlePanel { diff --git a/frontend/demo/src/pages/Inbox/Messenger/index.tsx b/frontend/demo/src/pages/Inbox/Messenger/index.tsx index a5612743e9..a3ac4bdef5 100644 --- a/frontend/demo/src/pages/Inbox/Messenger/index.tsx +++ b/frontend/demo/src/pages/Inbox/Messenger/index.tsx @@ -8,6 +8,7 @@ import {StateModel} from '../../../reducers'; import {AllConversationsState} from '../../../reducers/data/conversations'; import './index.scss'; +import MessengerContainer from './MessengerContainer'; const mapStateToProps = (state: StateModel) => { return { @@ -43,7 +44,11 @@ const Messenger = (props: ConnectedProps & RouteComponentProps } + render={props => ( + + + + )} /> ); diff --git a/frontend/demo/src/pages/Inbox/index.tsx b/frontend/demo/src/pages/Inbox/index.tsx index ff07645e13..0d2d32ccd4 100644 --- a/frontend/demo/src/pages/Inbox/index.tsx +++ b/frontend/demo/src/pages/Inbox/index.tsx @@ -23,7 +23,7 @@ const mapDispatchToProps = { const connector = connect(mapStateToProps, mapDispatchToProps); -const MessengerContainer = (props: InboxProps & ConnectedProps) => { +const ConversationContainer = (props: InboxProps & ConnectedProps) => { useEffect(() => { props.listConversations(); }); @@ -31,4 +31,4 @@ const MessengerContainer = (props: InboxProps & ConnectedProps return ; }; -export default connector(MessengerContainer); +export default connector(ConversationContainer); diff --git a/frontend/demo/src/reducers/data/index.ts b/frontend/demo/src/reducers/data/index.ts index 1c593309f3..3fbe7a23db 100644 --- a/frontend/demo/src/reducers/data/index.ts +++ b/frontend/demo/src/reducers/data/index.ts @@ -8,6 +8,7 @@ import conversations, {ConversationsState} from './conversations'; import tags from './tags'; import settings from './settings'; import channels from './channels'; +import messages, {Messages} from './messages'; export * from './channels'; export * from './conversations'; @@ -18,6 +19,7 @@ export {initialState} from './user'; export type DataState = { user: User; conversations: ConversationsState; + messages: Messages; tags: Tags; settings: Settings; channels: Channel[]; @@ -26,6 +28,7 @@ export type DataState = { const reducers: Reducer = combineReducers({ user, conversations, + messages, tags, settings, channels, diff --git a/frontend/demo/src/reducers/data/messages/index.ts b/frontend/demo/src/reducers/data/messages/index.ts new file mode 100644 index 0000000000..66bed466c5 --- /dev/null +++ b/frontend/demo/src/reducers/data/messages/index.ts @@ -0,0 +1,43 @@ +import {ActionType, getType} from 'typesafe-actions'; + +import * as actions from '../../../actions/messages'; +import {Message} from 'httpclient'; +import {DataState} from '..'; +import _ from 'lodash-es'; + +type Action = ActionType; + +export type MessagesState = { + data: DataState; +}; + +export type MessageById = { + [messageId: string]: Message; +}; + +export type Messages = { + all: {[conversationId: string]: MessageById}; +}; + +const initialState = { + all: {}, +}; + +function organiseMessages(messages: Message[]): MessageById { + return _.keyBy(messages, 'id'); +} + +export default function messagesReducer(state = initialState, action: Action): any { + switch (action.type) { + case getType(actions.loadingMessagesAction): + return { + ...state, + all: { + ...state.all, + [action.payload.conversationId]: organiseMessages(action.payload.messages), + }, + }; + default: + return state; + } +} diff --git a/lib/typescript/httpclient/endpoints/index.ts b/lib/typescript/httpclient/endpoints/index.ts index cb394896b7..235c75ff29 100644 --- a/lib/typescript/httpclient/endpoints/index.ts +++ b/lib/typescript/httpclient/endpoints/index.ts @@ -3,6 +3,7 @@ export * from './exploreChannels'; export * from './connectChannel'; export * from './disconnectChannel'; export * from './listConversations'; +export * from './listMessages'; export * from './listTags'; export * from './createTag'; export * from './updateTag'; diff --git a/lib/typescript/httpclient/endpoints/listConversations.ts b/lib/typescript/httpclient/endpoints/listConversations.ts index 1234aa1023..f82bd4cfeb 100644 --- a/lib/typescript/httpclient/endpoints/listConversations.ts +++ b/lib/typescript/httpclient/endpoints/listConversations.ts @@ -9,8 +9,8 @@ const messageMapper = (payload: MessagePayload): Message => { const message: Message = { id: payload.id, content: payload.content, - state: payload.state, - alignment: payload.alignment, + deliveryState: payload.delivery_state, + senderType: payload.sender_type, sentAt: payload.sent_at, }; return message; diff --git a/lib/typescript/httpclient/endpoints/listMessages.ts b/lib/typescript/httpclient/endpoints/listMessages.ts new file mode 100644 index 0000000000..b71b497ed4 --- /dev/null +++ b/lib/typescript/httpclient/endpoints/listMessages.ts @@ -0,0 +1,36 @@ +import {doFetchFromBackend} from '../api'; +import {Message, MessagePayloadData} from '../model'; +import {ListMessagesRequestPayload} from '../payload/ListMessagesRequestPayload'; +import {MessagePayload} from '../payload/MessagePayload'; +import {PaginatedPayload} from '../payload/PaginatedPayload'; + +export function listMessages(conversationListRequest: ListMessagesRequestPayload) { + conversationListRequest.pageSize = conversationListRequest.pageSize ?? 10; + conversationListRequest.cursor = conversationListRequest.cursor ?? null; + + return doFetchFromBackend('messages.list', { + conversation_id: conversationListRequest.conversationId, + cursor: conversationListRequest.cursor, + page_size: conversationListRequest.pageSize, + }) + .then((response: PaginatedPayload) => { + const {responseMetadata} = response; + return {data: messageMapperData(response), metadata: responseMetadata}; + }) + .catch((error: Error) => { + return error; + }); +} + +const messageMapperData = (payload: MessagePayloadData): Message[] => { + return payload.data.map((messagePayload: MessagePayload) => { + const message: Message = { + id: messagePayload.id, + content: messagePayload.content, + deliveryState: messagePayload.delivery_state, + senderType: messagePayload.sender_type, + sentAt: messagePayload.sent_at, + }; + return message; + }); +}; diff --git a/lib/typescript/httpclient/index.ts b/lib/typescript/httpclient/index.ts index ebe9609742..16599b6b1a 100644 --- a/lib/typescript/httpclient/index.ts +++ b/lib/typescript/httpclient/index.ts @@ -4,6 +4,7 @@ import { connectChannel, disconnectChannel, listConversations, + listMessages, listTags, createTag, updateTag, @@ -18,6 +19,7 @@ export const HttpClient = (function() { connectChannel: connectChannel, disconnectChannel: disconnectChannel, listConversations: listConversations, + listMessages: listMessages, listTags: listTags, createTag: createTag, updateTag: updateTag, diff --git a/lib/typescript/httpclient/model/Message.ts b/lib/typescript/httpclient/model/Message.ts index 106d1a6d0c..1b32c14848 100644 --- a/lib/typescript/httpclient/model/Message.ts +++ b/lib/typescript/httpclient/model/Message.ts @@ -1,3 +1,5 @@ +import {MessagePayload} from '../payload/MessagePayload'; + export interface Attachement { type: string; payload: { @@ -28,13 +30,21 @@ export enum MessageState { delivered = 'DELIVERED', } +export enum SenderType { + sourceContact = 'source_contact', + sourceUser = 'source_user', + appUser = 'app_user', +} export interface Message { id: string; content: { text: string; type: MessageType; }; - state: MessageState; - alignment: MessageAlignment; - sentAt: string | Date; + deliveryState: MessageState; + senderType: SenderType; + sentAt: Date; +} +export interface MessagePayloadData { + data: MessagePayload[]; } diff --git a/lib/typescript/httpclient/payload/ListMessagesRequestPayload.ts b/lib/typescript/httpclient/payload/ListMessagesRequestPayload.ts new file mode 100644 index 0000000000..7c4839721d --- /dev/null +++ b/lib/typescript/httpclient/payload/ListMessagesRequestPayload.ts @@ -0,0 +1,5 @@ +export interface ListMessagesRequestPayload { + conversationId: string; + cursor?: string | null; + pageSize?: number; +} diff --git a/lib/typescript/httpclient/payload/MessagePayload.ts b/lib/typescript/httpclient/payload/MessagePayload.ts index 587e3b7ead..5b75c905d0 100644 --- a/lib/typescript/httpclient/payload/MessagePayload.ts +++ b/lib/typescript/httpclient/payload/MessagePayload.ts @@ -1,4 +1,4 @@ -import {MessageType, MessageState, MessageAlignment} from '../model'; +import {MessageType, MessageState, SenderType} from '../model'; export interface MessagePayload { id: string; @@ -6,7 +6,7 @@ export interface MessagePayload { text: string; type: MessageType; }; - state: MessageState; - alignment: MessageAlignment; - sent_at: string | Date; + delivery_state: MessageState; + sender_type: SenderType; + sent_at: Date; } diff --git a/lib/typescript/httpclient/payload/index.ts b/lib/typescript/httpclient/payload/index.ts index ee0ddcfcd6..70e7fc7ad6 100644 --- a/lib/typescript/httpclient/payload/index.ts +++ b/lib/typescript/httpclient/payload/index.ts @@ -7,3 +7,4 @@ export * from './LoginViaEmailRequestPayload'; export * from './ListTagsResponsePayload'; export * from './CreateTagRequestPayload'; export * from './ListConversationsRequestPayload'; +export * from './ResponseMetadataPayload'; diff --git a/package.json b/package.json index 9bf313abec..204a88155a 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,9 @@ "lodash-es": "^4.17.15", "react-window": "1.8.5", "react-window-infinite-loader": "1.0.5", + "reselect": "4.0.0" + }, "devDependencies": { "@babel/core": "7.8.4", diff --git a/yarn.lock b/yarn.lock index f4d6c9dfa6..955ccffe65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7423,16 +7423,11 @@ regenerator-runtime@^0.13.2: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz#7cf6a77d8f5c6f60eb73c5fc1955b2ceb01e6bf5" integrity sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw== -regenerator-runtime@^0.13.4: +regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.5: version "0.13.7" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== -regenerator-runtime@^0.13.5: - version "0.13.5" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz#d878a1d094b4306d10b9096484b33ebd55e26697" - integrity sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA== - regenerator-transform@^0.14.0: version "0.14.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.14.1.tgz#3b2fce4e1ab7732c08f665dfdb314749c7ddd2fb" From 471db5e51943660fd6fbda98a5e61dc6b40d49c2 Mon Sep 17 00:00:00 2001 From: Paulo Diniz Date: Wed, 13 Jan 2021 09:04:19 +0100 Subject: [PATCH 02/56] [#614] Fix release script (#615) Fixes #614 --- docs/docs/overview/release-process.md | 24 +++--------------------- scripts/release.sh | 19 +++++++++---------- 2 files changed, 12 insertions(+), 31 deletions(-) diff --git a/docs/docs/overview/release-process.md b/docs/docs/overview/release-process.md index 6f70200d67..10d70fa83d 100644 --- a/docs/docs/overview/release-process.md +++ b/docs/docs/overview/release-process.md @@ -10,30 +10,12 @@ users in a timely manner. Here's an outline of the process: -- We branch from `develop` unless it's a hot-fix (we'd use `main` in that case) +- We need a `GITHUB_TOKEN` environment variable with write permission to the org +- We run `./scripts/release.sh start x.y.z` - Once release days comes, we execute the following steps: - - We create an issue "Release x.y.z" - - We create a release branch `release/x.y.z` from the latest `develop` and push it: - - `git checkout develop` - - `git pull origin develop` - - `git checkout -b release/x.y.z` - - `git push origin release/x.y.z` - We test our release (`AIRY_VERSION=release ./scripts/bootstrap.sh`) and any additional hot-fix is committed directly to the release branch - - Once we're satisfied with the release, we update the `VERSION` file with the - current release number. The commit message must be `Fixes #issue-number` - where `issue-number` is the number of the current release issue - - We merge the release branch into `main`, tag `main` with `x.y.z`and push to `main`: - - `git checkout main` - - `git pull origin main` - - `git merge --no-ff release/x.y.z` - - `git tag x.y.z` - - `git push origin main` - - `git push origin x.y.z` - - We merge the release branch back into `develop`: - - `git checkout develop` - - `git merge --no-ff release/x.y.z` - - `git push origin develop` + - Once we're satisfied with the release, we finish the release by running `./scripts/release.sh finish x.y.z` - We archive cards in the done column of the [work in progress](https://github.com/airyhq/airy/projects/1) board - We rename the current draft release to `x.y.z` and publish it - We announce the release! diff --git a/scripts/release.sh b/scripts/release.sh index 922423c4ce..b7869ba262 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -71,18 +71,17 @@ merge_develop() { echo -e "Successfully merged into develop branch\n" } -if [[ -b $1 ]] && [[ -b $2 ]]; -then - case $1 in - "start") - start $2 - ;; - "finish") - finish $2 - esac -else +if [[ -z ${1+x} || -z ${2+x} ]]; then echo -ne "Error executing script\n" echo -ne "Expected syntax: release.sh \n" exit 1 fi +case $1 in + "start") + start $2 + ;; + "finish") + finish $2 +esac + From 96c6af3a27614ff72c3ee45ee0dce717b7141812 Mon Sep 17 00:00:00 2001 From: Bodo Tasche Date: Wed, 13 Jan 2021 14:51:40 +0100 Subject: [PATCH 03/56] [#599] Display avatar + time in messages (#625) --- frontend/demo/README.md | 11 +++- .../components/AvatarImage/index.module.scss | 9 ++++ .../demo/src/components/AvatarImage/index.tsx | 22 ++++++++ .../demo/src/components/Sidebar/index.tsx | 6 +-- .../Inbox/ConversationListItem/index.tsx | 11 ++-- .../Inbox/Messenger/MessageList/Avatar.scss | 9 ---- .../Inbox/Messenger/MessageList/Avatar.tsx | 33 ------------ .../Messenger/MessageList/index.module.scss | 12 +++++ .../Inbox/Messenger/MessageList/index.tsx | 52 +++++++++++++++---- .../MessengerListItem/index.module.scss | 39 +++++++++----- .../Messenger/MessengerListItem/index.tsx | 50 ++++++++---------- .../httpclient/endpoints/listConversations.ts | 2 +- .../httpclient/endpoints/listMessages.ts | 2 +- lib/typescript/httpclient/model/Contact.ts | 11 ++-- .../httpclient/model/Conversation.ts | 9 +--- 15 files changed, 158 insertions(+), 120 deletions(-) create mode 100644 frontend/demo/src/components/AvatarImage/index.module.scss create mode 100644 frontend/demo/src/components/AvatarImage/index.tsx delete mode 100644 frontend/demo/src/pages/Inbox/Messenger/MessageList/Avatar.scss delete mode 100644 frontend/demo/src/pages/Inbox/Messenger/MessageList/Avatar.tsx diff --git a/frontend/demo/README.md b/frontend/demo/README.md index 5fe00df59c..b3236a533b 100644 --- a/frontend/demo/README.md +++ b/frontend/demo/README.md @@ -8,12 +8,12 @@ The Airy Demo UI is a minimal user interactive frontend project that showcases the Airy Core Platform API.It enables users to experience the functionalities of our Airy Core Platform. - - [Prerequities](#prerequities) - [Building Airy Demo UI](#building-airy-demo-ui) - [Installation](#installation) - [Authentication](#authentication) - [Endpoints](#endpoints) +- [Development](#development) ### Prerequisites @@ -49,4 +49,13 @@ In order to communicate with our API endpoints, you need a valid [JWT](https://j Aside from Curl, [PostMan](https://www.postman.com/downloads/) and other API testing tools could also be used to access the endpoints. +### Development + +To start the app in development mode, run these commands: + +``` +yarn +yarn ibazel run //frontend/demo:bundle_server +``` +After it started, open a web browser to [`localhost:8080`](http://localhost:8080). Login with the user you created above. \ No newline at end of file diff --git a/frontend/demo/src/components/AvatarImage/index.module.scss b/frontend/demo/src/components/AvatarImage/index.module.scss new file mode 100644 index 0000000000..49c278ac39 --- /dev/null +++ b/frontend/demo/src/components/AvatarImage/index.module.scss @@ -0,0 +1,9 @@ +.avatar { + display: flex; +} + +.avatarImage { + border-radius: 50%; + width: 100%; + height: 100%; +} diff --git a/frontend/demo/src/components/AvatarImage/index.tsx b/frontend/demo/src/components/AvatarImage/index.tsx new file mode 100644 index 0000000000..5be93c6104 --- /dev/null +++ b/frontend/demo/src/components/AvatarImage/index.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import {Contact} from 'httpclient'; +import styles from './index.module.scss'; + +const fallbackAvatar = 'https://s3.amazonaws.com/assets.airy.co/unknown.png'; + +type AvatarProps = { + contact: Contact; +}; + +const AvatarImage = (props: AvatarProps) => { + const {contact} = props; + + return ( +
+ +
+ ); +}; + +export default AvatarImage; diff --git a/frontend/demo/src/components/Sidebar/index.tsx b/frontend/demo/src/components/Sidebar/index.tsx index 924224c9bd..a3e3935ad5 100644 --- a/frontend/demo/src/components/Sidebar/index.tsx +++ b/frontend/demo/src/components/Sidebar/index.tsx @@ -19,19 +19,19 @@ const Sidebar = (props: RouteProps) => {
- + Inbox
- + Channels
- + Tags
diff --git a/frontend/demo/src/pages/Inbox/ConversationListItem/index.tsx b/frontend/demo/src/pages/Inbox/ConversationListItem/index.tsx index 33df9c34a4..16192814bc 100644 --- a/frontend/demo/src/pages/Inbox/ConversationListItem/index.tsx +++ b/frontend/demo/src/pages/Inbox/ConversationListItem/index.tsx @@ -3,6 +3,7 @@ import {Link} from 'react-router-dom'; import _, {connect, ConnectedProps} from 'react-redux'; import IconChannel from '../../../components/IconChannel'; +import AvatarImage from '../../../components/AvatarImage'; import {formatTimeOfMessage} from '../../../services/format/date'; @@ -39,10 +40,7 @@ const FormattedMessage = ({message}: FormattedMessageProps) => { const ConversationListItem = (props: ConversationListItemProps) => { const {conversation, active, style} = props; - const participant = conversation.contact; - const fallbackAvatar = 'https://s3.amazonaws.com/assets.airy.co/unknown.png'; - const unread = conversation.unreadMessageCount > 0; return ( @@ -52,10 +50,9 @@ const ConversationListItem = (props: ConversationListItemProps) => { className={`${active ? styles.containerListItemActive : styles.containerListItem} ${ unread ? styles.unread : '' }`}> -
+
+ +
diff --git a/frontend/demo/src/pages/Inbox/Messenger/MessageList/Avatar.scss b/frontend/demo/src/pages/Inbox/Messenger/MessageList/Avatar.scss deleted file mode 100644 index 28d396dedd..0000000000 --- a/frontend/demo/src/pages/Inbox/Messenger/MessageList/Avatar.scss +++ /dev/null @@ -1,9 +0,0 @@ -.avatarImage { - height: 40px; - width: 40px; - background-color: green; -} - -.noAvatar { - background-color: red; -} diff --git a/frontend/demo/src/pages/Inbox/Messenger/MessageList/Avatar.tsx b/frontend/demo/src/pages/Inbox/Messenger/MessageList/Avatar.tsx deleted file mode 100644 index 7f73202843..0000000000 --- a/frontend/demo/src/pages/Inbox/Messenger/MessageList/Avatar.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -import './Avatar.scss'; -import airyAvatarImage from '../../../../assets/images/icons/airy_avatar.svg'; - -type AvatarProps = { - avatarUrl?: string; - isLastMessage: boolean; -}; - -const NoAvatar = () => { - return ( -
- -
- ); -}; - -const Avatar = (props: AvatarProps) => { - const {avatarUrl, isLastMessage} = props; - - if (avatarUrl && isLastMessage) { - return ( -
- -
- ); - } - - return ; -}; - -export default Avatar; diff --git a/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.module.scss b/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.module.scss index 56a57d76dc..6329092f1c 100644 --- a/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.module.scss +++ b/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.module.scss @@ -1,3 +1,6 @@ +@import '../../../../assets/scss/colors.scss'; +@import '../../../../assets/scss/fonts.scss'; + .messageList { display: flex; flex-direction: column; @@ -7,3 +10,12 @@ flex-grow: 1; overflow-x: hidden; } + +.dateHeader { + @include font-s; + margin: 8px auto; + padding: 4px 8px; + border-radius: 4px; + background-color: var(--color-background-gray); + color: var(--color-text-gray); +} diff --git a/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.tsx b/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.tsx index 0895dd276f..918b1da09f 100644 --- a/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.tsx +++ b/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.tsx @@ -1,9 +1,9 @@ -import React, {useEffect, createRef} from 'react'; +import React, {useEffect, useState, createRef} from 'react'; import {RouteComponentProps, useParams} from 'react-router-dom'; import _, {connect, ConnectedProps} from 'react-redux'; import _redux from 'redux'; -import {Message} from 'httpclient'; +import {Message, SenderType} from 'httpclient'; import {StateModel} from '../../../../reducers'; import {MessageById} from '../../../../reducers/data/messages'; @@ -14,6 +14,7 @@ import {listMessages} from '../../../../actions/messages'; import {allConversationSelector} from '../../../../selectors/conversations'; import styles from './index.module.scss'; +import {formatDateOfMessage} from '../../../../services/format/date'; type MessageListProps = {conversationId: string} & ConnectedProps & RouteComponentProps<{conversationId: string}>; @@ -43,9 +44,10 @@ const mapDispatchToProps = { const connector = connect(mapStateToProps, mapDispatchToProps); const MessageList = (props: MessageListProps) => { - const {listMessages, messages} = props; + const {conversations, listMessages, messages} = props; const conversationIdParams = useParams(); const currentConversationId = conversationIdParams[Object.keys(conversationIdParams)[0]]; + const [currentConversation, setCurrentConversation] = useState(null); const messageListRef = createRef(); @@ -54,21 +56,49 @@ const MessageList = (props: MessageListProps) => { scrollBottom(); }, [currentConversationId]); + useEffect(() => { + setCurrentConversation(conversations.find(item => item && item.id === currentConversationId)); + }, [currentConversationId, conversations]); + const scrollBottom = () => { messageListRef.current.scrollTop = messageListRef.current.scrollHeight; }; + const isContact = (message: Message) => message.senderType !== SenderType.appUser; + + const hasDateChanged = (prevMessage: Message, message: Message) => { + if (prevMessage == null) { + return true; + } + + return !isSameDay(prevMessage.sentAt, message.sentAt); + }; + + const isSameDay = (firstDate: Date, secondDate: Date) => { + return new Date(firstDate).setHours(0, 0, 0, 0) === new Date(secondDate).setHours(0, 0, 0, 0); + }; + return (
- {messages.map((message: Message) => { + {messages.map((message: Message, index: number) => { + const prevMessage = messages[index - 1]; + const nextMessage = messages[index + 1]; + const prevWasContact = prevMessage ? isContact(prevMessage) : false; + const nextIsSameUser = nextMessage ? isContact(message) == isContact(nextMessage) : false; + return ( - + <> + {hasDateChanged(prevMessage, message) && ( +
{formatDateOfMessage(message)}
+ )} + + ); })}
diff --git a/frontend/demo/src/pages/Inbox/Messenger/MessengerListItem/index.module.scss b/frontend/demo/src/pages/Inbox/Messenger/MessengerListItem/index.module.scss index 45194c1f5d..eac1368a90 100644 --- a/frontend/demo/src/pages/Inbox/Messenger/MessengerListItem/index.module.scss +++ b/frontend/demo/src/pages/Inbox/Messenger/MessengerListItem/index.module.scss @@ -1,4 +1,5 @@ -@import '../../../../assets//scss/colors.scss'; +@import '../../../../assets/scss/colors.scss'; +@import '../../../../assets/scss/fonts.scss'; .messageListItemContainer { display: flex; @@ -8,38 +9,46 @@ .messageListItem { display: flex; align-self: flex-end; - max-width: 100%; + width: 100%; overflow-wrap: break-word; word-break: break-word; } +.messageAvatar { + width: 40px; + height: 40px; + margin: 6px 8px 0 0; +} + .messageListUserContainer { display: flex; flex-direction: row; - - img { - height: 40px; - width: 40px; - margin-top: 5px; - } } .messageListItemUser { - display: inline-flex; align-self: flex-start; + text-align: left; + position: relative; +} + +.messageListItemUserText { + display: inline-flex; padding: 10px; margin-top: 5px; background: var(--color-background-blue); color: #212428; - position: relative; - text-align: left; border-radius: 8px; } .messageListItemMember { margin-top: 5px; + justify-content: flex-end; + width: 100%; + text-align: right; +} + +.messageListItemMemberText { display: inline-flex; - align-self: flex-end; padding: 10px; position: relative; background: var(--color-airy-blue); @@ -48,3 +57,9 @@ text-align: left; border-radius: 8px; } + +.messageTime { + @include font-s; + color: var(--color-text-gray); + margin: 4px 10px; +} diff --git a/frontend/demo/src/pages/Inbox/Messenger/MessengerListItem/index.tsx b/frontend/demo/src/pages/Inbox/Messenger/MessengerListItem/index.tsx index e2f1f649c3..fc270daa9f 100644 --- a/frontend/demo/src/pages/Inbox/Messenger/MessengerListItem/index.tsx +++ b/frontend/demo/src/pages/Inbox/Messenger/MessengerListItem/index.tsx @@ -1,51 +1,43 @@ import React from 'react'; -import _redux from 'redux'; -import _, {connect, ConnectedProps} from 'react-redux'; -import {Message, SenderType} from 'httpclient'; -import {StateModel} from '../../../../reducers'; -import Avatar from '../MessageList/Avatar'; +import _ from 'redux'; +import {Message, Conversation, SenderType} from 'httpclient'; +import AvatarImage from '../../../../components/AvatarImage'; import styles from './index.module.scss'; +import {formatTimeOfMessage} from '../../../../services/format/date'; type MessengerListItemProps = { - messageText: string; - messageSenderType: string; - messageDate: Date; message: Message; -} & ConnectedProps; - -const mapStateToProps = (state: StateModel) => { - return { - lastMessages: state.data.conversations.all.items, - }; + conversation: Conversation; + showAvatar: boolean; + showSentAt: boolean; }; -const connector = connect(mapStateToProps, null); - const MessengerListItem = (props: MessengerListItemProps) => { - const {messageText, messageSenderType, message, lastMessages} = props; - const isUser = messageSenderType !== SenderType.appUser; + const {conversation, showAvatar, showSentAt, message} = props; + const isUser = message.senderType !== SenderType.appUser; - const messageAvatar = (messageId: string) => { - Object.values(lastMessages).forEach(lastMessage => { - return ( - - ); - }); + const messageAvatar = () => { + return conversation && ; }; + const messageText = message.content[0].text; + return (
{!isUser ? (
- {messageText} - {messageAvatar(message.id)} +
{messageText}
+ {showSentAt &&
{formatTimeOfMessage(message)}
}
) : (
- {messageAvatar(message.id)} -
{messageText}
+
{showAvatar && messageAvatar()}
+
+
{messageText}
+ {showSentAt &&
{formatTimeOfMessage(message)}
} +
)}
@@ -53,4 +45,4 @@ const MessengerListItem = (props: MessengerListItemProps) => { ); }; -export default connector(MessengerListItem); +export default MessengerListItem; diff --git a/lib/typescript/httpclient/endpoints/listConversations.ts b/lib/typescript/httpclient/endpoints/listConversations.ts index f82bd4cfeb..52a1b1a194 100644 --- a/lib/typescript/httpclient/endpoints/listConversations.ts +++ b/lib/typescript/httpclient/endpoints/listConversations.ts @@ -11,7 +11,7 @@ const messageMapper = (payload: MessagePayload): Message => { content: payload.content, deliveryState: payload.delivery_state, senderType: payload.sender_type, - sentAt: payload.sent_at, + sentAt: new Date(payload.sent_at), }; return message; }; diff --git a/lib/typescript/httpclient/endpoints/listMessages.ts b/lib/typescript/httpclient/endpoints/listMessages.ts index b71b497ed4..cf1e4bc0ef 100644 --- a/lib/typescript/httpclient/endpoints/listMessages.ts +++ b/lib/typescript/httpclient/endpoints/listMessages.ts @@ -29,7 +29,7 @@ const messageMapperData = (payload: MessagePayloadData): Message[] => { content: messagePayload.content, deliveryState: messagePayload.delivery_state, senderType: messagePayload.sender_type, - sentAt: messagePayload.sent_at, + sentAt: new Date(messagePayload.sent_at), }; return message; }); diff --git a/lib/typescript/httpclient/model/Contact.ts b/lib/typescript/httpclient/model/Contact.ts index 8e0e4b327b..1ceccbff1c 100644 --- a/lib/typescript/httpclient/model/Contact.ts +++ b/lib/typescript/httpclient/model/Contact.ts @@ -2,10 +2,9 @@ import {Tag} from './Tag'; export interface Contact { id: string; - info: Dictionary; - first_name: string; - last_name: string; - display_name: string; - avatar_url: string; - tags: Tag[]; + firstName: string; + lastName: string; + displayName: string; + avatarUrl: string; + tags?: Tag[]; } diff --git a/lib/typescript/httpclient/model/Conversation.ts b/lib/typescript/httpclient/model/Conversation.ts index df10890cfb..927a15e4c1 100644 --- a/lib/typescript/httpclient/model/Conversation.ts +++ b/lib/typescript/httpclient/model/Conversation.ts @@ -1,17 +1,12 @@ import {Channel} from './Channel'; +import {Contact} from './Contact'; import {Message} from './Message'; export interface Conversation { id: string; channel: Channel; createdAt: string; - contact: { - avatarUrl: string; - firstName: string; - lastName: string; - displayName: string; - id: string; - }; + contact: Contact; tags: string[]; lastMessage: Message; unreadMessageCount?: number; From 9c809cd99b06e57d53870765e4023eac5bc5dd04 Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Wed, 13 Jan 2021 18:10:43 +0100 Subject: [PATCH 04/56] [#642] Update typescript content typings (#645) --- .../java/co/airy/ts_generator/Main.java | 2 +- lib/typescript/types/content.ts | 26 ++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/lib/java/mapping/src/ts-generator/java/co/airy/ts_generator/Main.java b/lib/java/mapping/src/ts-generator/java/co/airy/ts_generator/Main.java index a56e9d5f0b..e4b7c8ed51 100644 --- a/lib/java/mapping/src/ts-generator/java/co/airy/ts_generator/Main.java +++ b/lib/java/mapping/src/ts-generator/java/co/airy/ts_generator/Main.java @@ -27,7 +27,7 @@ public static void main(String[] args) { parameters.debug = false; parameters.classNamePatterns = List.of("co.airy.mapping.model.**"); - final File output = new File(System.getenv().get("BUILD_WORKSPACE_DIRECTORY") + "/frontend/types/content.ts"); + final File output = new File(System.getenv().get("BUILD_WORKSPACE_DIRECTORY") + "/lib/typescript/types/content.ts"); settings.validateFileName(output); generator.generateTypeScript(Input.from(parameters), Output.to(output)); diff --git a/lib/typescript/types/content.ts b/lib/typescript/types/content.ts index d3d788072c..068d883a86 100644 --- a/lib/typescript/types/content.ts +++ b/lib/typescript/types/content.ts @@ -1,9 +1,25 @@ /* tslint:disable */ /* eslint-disable */ -// Generated using typescript-generator version 2.26.723 on 2020-12-02 10:41:15. +// Generated using typescript-generator version 2.26.723 on 2021-01-13 15:17:35. + +export interface Audio extends Content, DataUrl { + type: 'audio'; +} export interface Content { - type: 'text'; + type: 'audio' | 'file' | 'image' | 'text' | 'video'; +} + +export interface DataUrl { + url: string; +} + +export interface File extends Content, DataUrl { + type: 'file'; +} + +export interface Image extends Content, DataUrl { + type: 'image'; } export interface Text extends Content { @@ -11,4 +27,8 @@ export interface Text extends Content { text: string; } -export type ContentUnion = Text; +export interface Video extends Content, DataUrl { + type: 'video'; +} + +export type ContentUnion = Text | Audio | File | Image | Video; From f3458892e7005ed5ef3cdabdaec4fbb015bf40c4 Mon Sep 17 00:00:00 2001 From: AudreyKj <38159391+AudreyKj@users.noreply.github.com> Date: Thu, 14 Jan 2021 10:00:13 +0100 Subject: [PATCH 05/56] Feature/598 read unread state of conversations (#617) * added readConversation endpoint, action, reducer * fixed read conversations action * fixed json error for http request * small fixes to action andapi config * small refactor to readconversations endpoint * refactor changes to read conversation endpoint and the lib api config * fixed bazel build * fixed linting * small changes refactor to api read conversation --- frontend/demo/src/actions/conversations/index.ts | 11 +++++++++++ .../pages/Inbox/ConversationListItem/index.tsx | 12 +++++++++--- .../src/reducers/data/conversations/index.ts | 16 ++++++++++++++++ lib/typescript/httpclient/api/airyConfig.ts | 5 ++++- lib/typescript/httpclient/endpoints/index.ts | 1 + .../httpclient/endpoints/readConversations.ts | 5 +++++ lib/typescript/httpclient/index.ts | 2 ++ 7 files changed, 48 insertions(+), 4 deletions(-) create mode 100644 lib/typescript/httpclient/endpoints/readConversations.ts diff --git a/frontend/demo/src/actions/conversations/index.ts b/frontend/demo/src/actions/conversations/index.ts index 8395c8608e..153d05fd65 100644 --- a/frontend/demo/src/actions/conversations/index.ts +++ b/frontend/demo/src/actions/conversations/index.ts @@ -9,6 +9,7 @@ export const CONVERSATIONS_LOADING = '@@conversations/LOADING'; export const CONVERSATIONS_MERGE = '@@conversations/MERGE'; export const CONVERSATION_ADD_ERROR = '@@conversations/ADD_ERROR_TO_CONVERSATION'; export const CONVERSATION_REMOVE_ERROR = '@@conversations/REMOVE_ERROR_FROM_CONVERSATION'; +export const CONVERSATION_READ = '@@conversations/CONVERSATION_READ'; export const loadingConversationAction = createAction(CONVERSATION_LOADING, resolve => (conversationId: string) => resolve(conversationId) @@ -22,6 +23,10 @@ export const mergeConversationsAction = createAction( resolve({conversations, responseMetadata}) ); +export const readConversationsAction = createAction(CONVERSATION_READ, resolve => (conversationId: string) => + resolve({conversationId}) +); + export const addErrorToConversationAction = createAction( CONVERSATION_ADD_ERROR, resolve => (conversationId: string, errorMessage: string) => resolve({conversationId, errorMessage}) @@ -60,3 +65,9 @@ export function listNextConversations() { }); }; } + +export function readConversations(conversationId: string) { + return function(dispatch: Dispatch) { + HttpClient.readConversations(conversationId).then(() => dispatch(readConversationsAction(conversationId))); + }; +} diff --git a/frontend/demo/src/pages/Inbox/ConversationListItem/index.tsx b/frontend/demo/src/pages/Inbox/ConversationListItem/index.tsx index 16192814bc..e6d35dc8eb 100644 --- a/frontend/demo/src/pages/Inbox/ConversationListItem/index.tsx +++ b/frontend/demo/src/pages/Inbox/ConversationListItem/index.tsx @@ -10,6 +10,7 @@ import {formatTimeOfMessage} from '../../../services/format/date'; import {Conversation, Message} from 'httpclient'; import {StateModel} from '../../../reducers'; import {INBOX_CONVERSATIONS_ROUTE} from '../../../routes/routes'; +import {readConversations} from '../../../actions/conversations'; import styles from './index.module.scss'; @@ -29,7 +30,11 @@ const mapStateToProps = (state: StateModel) => { }; }; -const connector = connect(mapStateToProps, null); +const mapDispatchToProps = { + readConversations, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); const FormattedMessage = ({message}: FormattedMessageProps) => { if (message && message.content[0]) { @@ -39,12 +44,13 @@ const FormattedMessage = ({message}: FormattedMessageProps) => { }; const ConversationListItem = (props: ConversationListItemProps) => { - const {conversation, active, style} = props; + const {conversation, active, style, readConversations} = props; + const participant = conversation.contact; const unread = conversation.unreadMessageCount > 0; return ( -
+
readConversations(conversation.id)}>
{ if (response.ok) { - return response.json(); + try { + return await response.json(); + } catch {} } let body = await response.text(); + if (body.length > 0) { body = JSON.parse(body); } diff --git a/lib/typescript/httpclient/endpoints/index.ts b/lib/typescript/httpclient/endpoints/index.ts index 235c75ff29..cbb6ec6a16 100644 --- a/lib/typescript/httpclient/endpoints/index.ts +++ b/lib/typescript/httpclient/endpoints/index.ts @@ -9,3 +9,4 @@ export * from './createTag'; export * from './updateTag'; export * from './deleteTag'; export * from './loginViaEmail'; +export * from './readConversations'; diff --git a/lib/typescript/httpclient/endpoints/readConversations.ts b/lib/typescript/httpclient/endpoints/readConversations.ts new file mode 100644 index 0000000000..21906acc80 --- /dev/null +++ b/lib/typescript/httpclient/endpoints/readConversations.ts @@ -0,0 +1,5 @@ +import {doFetchFromBackend} from '../api'; + +export function readConversations(conversationId: string) { + return doFetchFromBackend('conversations.read', {conversation_id: conversationId}).then(() => Promise.resolve(true)); +} diff --git a/lib/typescript/httpclient/index.ts b/lib/typescript/httpclient/index.ts index 16599b6b1a..5ab208181a 100644 --- a/lib/typescript/httpclient/index.ts +++ b/lib/typescript/httpclient/index.ts @@ -4,6 +4,7 @@ import { connectChannel, disconnectChannel, listConversations, + readConversations, listMessages, listTags, createTag, @@ -19,6 +20,7 @@ export const HttpClient = (function() { connectChannel: connectChannel, disconnectChannel: disconnectChannel, listConversations: listConversations, + readConversations: readConversations, listMessages: listMessages, listTags: listTags, createTag: createTag, From 1e33a10a6723ddbaee58ce8f9b0c08f5add13a37 Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Thu, 14 Jan 2021 11:32:18 +0100 Subject: [PATCH 06/56] [#497] Upload metadata and message source files to a user provided s3 bucket (#602) --- .gitignore | 7 +- .ijwb/.bazelproject | 24 ++ .../.idea/fileTemplates/Stream App.java | 14 +- WORKSPACE | 4 + backend/media/BUILD | 9 +- .../airy/core/media/MessageMediaResolver.java | 111 +++++++ .../co/airy/core/media/MetadataResolver.java | 96 ++++++ .../main/java/co/airy/core/media/Stores.java | 87 +++++ .../co/airy/core/media/config/AwsConfig.java | 31 ++ .../core/media/dto/MessageMediaRequest.java | 17 + .../airy/core/media/services/MediaUpload.java | 60 ++++ .../src/main/resources/application.properties | 8 +- .../java/co/airy/core/media/MessagesTest.java | 150 +++++++++ .../java/co/airy/core/media/MetadataTest.java | 135 ++++++++ .../media/src/test/resources/test.properties | 10 + .../airy/model/message/MessageRepository.java | 4 + .../model/metadata/MetadataRepository.java | 15 +- backend/sources/twilio/events-router/BUILD | 1 + .../sources/twilio/TwilioInfoExtractor.java | 28 +- docs/docs/guides/airy-core-in-production.md | 38 ++- infrastructure/airy.conf.all | 11 +- .../configmap-controller.go | 1 - .../airy-config/templates/user-storage.yaml | 14 + .../apps/charts/airy-config/values.yaml | 2 +- .../apps/charts/media-resolver/Chart.yaml | 5 + .../media-resolver/templates/deployment.yaml | 58 ++++ .../media-resolver/templates/service.yaml | 13 + .../apps/charts/media-resolver/values.yaml | 1 + infrastructure/scripts/trigger/stop.sh | 1 + .../co/airy/mapping/ContentMapperTest.java | 1 - lib/java/url/BUILD | 7 + .../src/main/java/co/airy/url/UrlUtil.java | 29 ++ maven_install.json | 300 ++++++++++++++++-- 33 files changed, 1199 insertions(+), 93 deletions(-) create mode 100644 .ijwb/.bazelproject rename backend/media/src/main/java/co/airy/core/media/Resolver.java => .ijwb/.idea/fileTemplates/Stream App.java (75%) create mode 100644 backend/media/src/main/java/co/airy/core/media/MessageMediaResolver.java create mode 100644 backend/media/src/main/java/co/airy/core/media/MetadataResolver.java create mode 100644 backend/media/src/main/java/co/airy/core/media/Stores.java create mode 100644 backend/media/src/main/java/co/airy/core/media/config/AwsConfig.java create mode 100644 backend/media/src/main/java/co/airy/core/media/dto/MessageMediaRequest.java create mode 100644 backend/media/src/main/java/co/airy/core/media/services/MediaUpload.java create mode 100644 backend/media/src/test/java/co/airy/core/media/MessagesTest.java create mode 100644 backend/media/src/test/java/co/airy/core/media/MetadataTest.java create mode 100644 backend/media/src/test/resources/test.properties create mode 100644 infrastructure/helm-chart/charts/apps/charts/airy-config/templates/user-storage.yaml create mode 100644 infrastructure/helm-chart/charts/apps/charts/media-resolver/Chart.yaml create mode 100644 infrastructure/helm-chart/charts/apps/charts/media-resolver/templates/deployment.yaml create mode 100644 infrastructure/helm-chart/charts/apps/charts/media-resolver/templates/service.yaml create mode 100644 infrastructure/helm-chart/charts/apps/charts/media-resolver/values.yaml create mode 100644 lib/java/url/BUILD create mode 100644 lib/java/url/src/main/java/co/airy/url/UrlUtil.java diff --git a/.gitignore b/.gitignore index 148b9451f0..ef4f7e8db8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,14 @@ # Editors and IDE's + +!.ijwb/.bazelproject +!.ijwb/.idea/fileTemplates/ + .idea/ .code/ .vscode/ .ijwb/ -# Bazel -!.ijwb/.bazelproject + dist/ bazel-* diff --git a/.ijwb/.bazelproject b/.ijwb/.bazelproject new file mode 100644 index 0000000000..0be506c563 --- /dev/null +++ b/.ijwb/.bazelproject @@ -0,0 +1,24 @@ +directories: + # Add the directories you want added as source here + # By default, we've added your entire workspace ('.') + . + +# Automatically includes all relevant targets under the 'directories' above +derive_targets_from_directories: true + +targets: + # If source code isn't resolving, add additional targets that compile it here + +additional_languages: + # Uncomment any additional languages you want supported + # android + # dart + go + javascript + # kotlin + python + # scala + typescript + +ts_config_rules: + //frontend/chat-plugin:widget_tsconfig \ No newline at end of file diff --git a/backend/media/src/main/java/co/airy/core/media/Resolver.java b/.ijwb/.idea/fileTemplates/Stream App.java similarity index 75% rename from backend/media/src/main/java/co/airy/core/media/Resolver.java rename to .ijwb/.idea/fileTemplates/Stream App.java index d9abdc148f..489b70dcd7 100644 --- a/backend/media/src/main/java/co/airy/core/media/Resolver.java +++ b/.ijwb/.idea/fileTemplates/Stream App.java @@ -1,4 +1,4 @@ -package co.airy.core.media; +#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end import co.airy.kafka.streams.KafkaStreamsWrapper; import co.airy.log.AiryLoggerFactory; @@ -11,20 +11,19 @@ import org.springframework.stereotype.Component; @Component -public class Resolver implements ApplicationListener, DisposableBean { +public class ${NAME} implements ApplicationListener, DisposableBean { private final Logger log = AiryLoggerFactory.getLogger(Resolver.class); - - private static final String appId = "media.Resolver"; + private static final String appId = "#[[$AppId$]]#"; private final KafkaStreamsWrapper streams; - public Resolver(KafkaStreamsWrapper streams) { + public ${NAME}(KafkaStreamsWrapper streams) { this.streams = streams; } @Override public void onApplicationEvent(ApplicationStartedEvent event) { final StreamsBuilder builder = new StreamsBuilder(); - + #[[$END$]]# streams.start(builder.build(), appId); } @@ -35,8 +34,7 @@ public void destroy() { } } - - // visible for testing + // Visible for testing KafkaStreams.State getStreamState() { return streams.state(); } diff --git a/WORKSPACE b/WORKSPACE index 83dc45a9d7..ba708a9056 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -25,6 +25,8 @@ load("@rules_jvm_external//:defs.bzl", "maven_install") maven_install( artifacts = airy_jvm_deps + [ + "com.amazonaws:aws-java-sdk-core:1.11.933", + "com.amazonaws:aws-java-sdk-s3:1.11.933", "com.fasterxml.jackson.core:jackson-annotations:2.10.0", "com.fasterxml.jackson.core:jackson-core:2.10.0", "com.fasterxml.jackson.core:jackson-databind:2.10.0", @@ -60,6 +62,7 @@ maven_install( "org.apache.lucene:lucene-queryparser:8.7.0", "org.apache.lucene:lucene-analyzers-common:8.7.0", "org.apache.lucene:lucene-core:8.7.0", + "org.aspectj:aspectjweaver:1.8.10", "org.bouncycastle:bcpkix-jdk15on:1.63", "org.flywaydb:flyway-core:5.2.4", "org.hamcrest:hamcrest-library:2.1", @@ -84,6 +87,7 @@ maven_install( "org.springframework.boot:spring-boot-starter-web:2.3.1.RELEASE", "org.springframework.boot:spring-boot-starter-websocket:2.3.1.RELEASE", "org.springframework.boot:spring-boot-starter-security:2.3.1.RELEASE", + "org.springframework.retry:spring-retry:1.2.5.RELEASE", "org.springframework:spring-aop:4.1.4.RELEASE", "org.springframework:spring-jdbc:4.1.4.RELEASE", "org.springframework:spring-context-support:5.2.0.RELEASE", diff --git a/backend/media/BUILD b/backend/media/BUILD index 0b7ed227b6..d6e8aa0d8a 100644 --- a/backend/media/BUILD +++ b/backend/media/BUILD @@ -6,12 +6,15 @@ app_deps = [ "//backend:base_app", "//backend/model/message", "//backend/model/metadata", - "//lib/java/uuid", "//lib/java/mapping", + "//lib/java/url", "//lib/java/spring/kafka/core:spring-kafka-core", "//lib/java/spring/kafka/streams:spring-kafka-streams", - "@maven//:io_lettuce_lettuce_core", - "@maven//:org_springframework_data_spring_data_redis", + "@maven//:javax_xml_bind_jaxb_api", + "@maven//:org_springframework_retry_spring_retry", + "@maven//:org_aspectj_aspectjweaver", + "@maven//:com_amazonaws_aws_java_sdk_core", + "@maven//:com_amazonaws_aws_java_sdk_s3", ] springboot( diff --git a/backend/media/src/main/java/co/airy/core/media/MessageMediaResolver.java b/backend/media/src/main/java/co/airy/core/media/MessageMediaResolver.java new file mode 100644 index 0000000000..198f7f985e --- /dev/null +++ b/backend/media/src/main/java/co/airy/core/media/MessageMediaResolver.java @@ -0,0 +1,111 @@ +package co.airy.core.media; + +import co.airy.avro.communication.Message; +import co.airy.avro.communication.Metadata; +import co.airy.core.media.dto.MessageMediaRequest; +import co.airy.core.media.services.MediaUpload; +import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; +import co.airy.log.AiryLoggerFactory; +import co.airy.mapping.ContentMapper; +import co.airy.mapping.model.Content; +import co.airy.mapping.model.DataUrl; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.slf4j.Logger; +import org.springframework.stereotype.Component; + +import javax.xml.bind.DatatypeConverter; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static co.airy.model.metadata.MetadataRepository.getId; +import static co.airy.model.metadata.MetadataRepository.newMessageMetadata; + +@Component +public class MessageMediaResolver { + private final Logger log = AiryLoggerFactory.getLogger(MessageMediaResolver.class); + private final String applicationCommunicationMetadata = new ApplicationCommunicationMetadata().name(); + private final KafkaProducer producer; + private final MediaUpload mediaUpload; + private final ContentMapper mapper; + private final ExecutorService executor; + + public MessageMediaResolver(KafkaProducer producer, + MediaUpload mediaUpload, + ContentMapper mapper) { + this.producer = producer; + this.mediaUpload = mediaUpload; + this.mapper = mapper; + this.executor = Executors.newSingleThreadExecutor(); + } + + public void onMessageMediaRequest(String messageId, MessageMediaRequest messageMediaRequest) { + executor.submit(() -> processMessageMediaRequests(messageId, messageMediaRequest)); + } + + private void processMessageMediaRequests(String messageId, MessageMediaRequest messageMediaRequest) { + final Message message = messageMediaRequest.getMessage(); + final Map metadataMap = Optional.ofNullable(messageMediaRequest.getMetadata()).orElse(new HashMap<>()); + + final List contentList = mapper.renderWithDefaultAndLog(message, metadataMap); + + for (Content content : contentList) { + if (!(content instanceof DataUrl)) { + continue; + } + + final String sourceUrl = ((DataUrl) content).getUrl(); + + try { + final URL url = new URL(sourceUrl); + + if (!mediaUpload.isUserStorageUrl(url) && !hasPersistentUrl(metadataMap, sourceUrl)) { + final String persistentUrl = mediaUpload.uploadMedia(url.openStream(), getFileName(sourceUrl)); + + final Metadata metadata = newMessageMetadata(messageId, getMessageKey(sourceUrl), persistentUrl); + storeMetadata(metadata); + } + } catch (ExecutionException | InterruptedException exception) { + throw new RuntimeException(exception); + } catch (MalformedURLException exception) { + // If it's not a URL, this is an error on the source side + log.warn("Source data url field is not a URL", exception); + } catch (Exception exception) { + log.error("Fetching message source content failed {}", messageMediaRequest); + } + } + } + + private void storeMetadata(Metadata metadata) throws ExecutionException, InterruptedException { + final String metadataKey = getId(metadata).toString(); + producer.send(new ProducerRecord<>(applicationCommunicationMetadata, metadataKey, metadata)).get(); + } + + private String getFileName(String sourceUrl) { + try { + final MessageDigest digest = MessageDigest.getInstance("SHA-256"); + final String urlHash = DatatypeConverter.printHexBinary(digest.digest(sourceUrl.getBytes(StandardCharsets.UTF_8))); + return String.format("data_%s", urlHash.toLowerCase()); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + private boolean hasPersistentUrl(Map metadataMap, String sourceUrl) { + return metadataMap.containsKey(getMessageKey(sourceUrl)); + } + + private String getMessageKey(String sourceUrl) { + return String.format("data_%s", sourceUrl); + } +} diff --git a/backend/media/src/main/java/co/airy/core/media/MetadataResolver.java b/backend/media/src/main/java/co/airy/core/media/MetadataResolver.java new file mode 100644 index 0000000000..ffae659dd4 --- /dev/null +++ b/backend/media/src/main/java/co/airy/core/media/MetadataResolver.java @@ -0,0 +1,96 @@ +package co.airy.core.media; + +import co.airy.avro.communication.Metadata; +import co.airy.core.media.services.MediaUpload; +import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; +import co.airy.log.AiryLoggerFactory; +import co.airy.model.metadata.MetadataKeys; +import co.airy.model.metadata.Subject; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.slf4j.Logger; +import org.springframework.stereotype.Component; + +import java.net.URL; +import java.time.Instant; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static co.airy.model.metadata.MetadataRepository.getId; +import static co.airy.model.metadata.MetadataRepository.getSubject; +import static co.airy.model.metadata.MetadataRepository.isConversationMetadata; + +@Component +public class MetadataResolver { + private final Logger log = AiryLoggerFactory.getLogger(MetadataResolver.class); + private final String applicationCommunicationMetadata = new ApplicationCommunicationMetadata().name(); + private final KafkaProducer producer; + private final MediaUpload mediaUpload; + private final ExecutorService executor; + + public MetadataResolver( + KafkaProducer producer, + MediaUpload mediaUpload) { + this.producer = producer; + this.mediaUpload = mediaUpload; + this.executor = Executors.newSingleThreadExecutor(); + } + + public boolean shouldResolve(Metadata metadata) { + if (metadata == null) { + return false; + } + + URL dataUrl; + + try { + dataUrl = new URL(metadata.getValue()); + } catch (Exception ignored) { + return false; + } + + return isConversationMetadata(metadata) + && metadata.getKey().equals(MetadataKeys.Source.Contact.AVATAR_URL) + && !mediaUpload.isUserStorageUrl(dataUrl); + } + + public void onMetadata(Metadata metadata) { + executor.submit(() -> processMetadataMediaRequest(metadata)); + } + + public void processMetadataMediaRequest(Metadata metadata) { + URL dataUrl; + + try { + dataUrl = new URL(metadata.getValue()); + } catch (Exception exception) { + log.error("Metadata value not a valid url despite filtering {}", metadata, exception); + return; + } + + final String resolvedKey = metadata.getKey() + ".resolved"; + final Subject subject = getSubject(metadata); + final String fileName = String.format("%s/%s", subject.getIdentifier(), resolvedKey); + + try { + final String userStorageUrl = mediaUpload.uploadMedia(dataUrl.openStream(), fileName); + + storeMetadata(Metadata.newBuilder() + .setSubject(subject.toString()) + .setKey(resolvedKey) + .setValue(userStorageUrl) + .setTimestamp(Instant.now().toEpochMilli()) + .build()); + } catch (ExecutionException | InterruptedException exception) { + throw new RuntimeException(exception); + } catch (Exception exception) { + log.error("Failed to upload metadata data url {}", metadata, exception); + } + } + + private void storeMetadata(Metadata metadata) throws ExecutionException, InterruptedException { + final String metadataKey = getId(metadata).toString(); + producer.send(new ProducerRecord<>(applicationCommunicationMetadata, metadataKey, metadata)).get(); + } +} diff --git a/backend/media/src/main/java/co/airy/core/media/Stores.java b/backend/media/src/main/java/co/airy/core/media/Stores.java new file mode 100644 index 0000000000..f867ab5def --- /dev/null +++ b/backend/media/src/main/java/co/airy/core/media/Stores.java @@ -0,0 +1,87 @@ +package co.airy.core.media; + +import co.airy.avro.communication.Message; +import co.airy.avro.communication.Metadata; +import co.airy.core.media.dto.MessageMediaRequest; +import co.airy.kafka.schema.application.ApplicationCommunicationMessages; +import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; +import co.airy.kafka.streams.KafkaStreamsWrapper; +import co.airy.log.AiryLoggerFactory; +import org.apache.kafka.streams.KafkaStreams; +import org.apache.kafka.streams.KeyValue; +import org.apache.kafka.streams.StreamsBuilder; +import org.apache.kafka.streams.kstream.KStream; +import org.apache.kafka.streams.kstream.KTable; +import org.slf4j.Logger; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +import static co.airy.model.message.MessageRepository.isNewMessage; +import static co.airy.model.metadata.MetadataRepository.getSubject; +import static co.airy.model.metadata.MetadataRepository.isMessageMetadata; + +@Component +public class Stores implements ApplicationListener, DisposableBean { + private final Logger log = AiryLoggerFactory.getLogger(Stores.class); + + private static final String appId = "media.Resolver"; + private final KafkaStreamsWrapper streams; + private final MetadataResolver metadataResolver; + private final MessageMediaResolver messageResolver; + + public Stores(KafkaStreamsWrapper streams, + MetadataResolver metadataResolver, + MessageMediaResolver messageResolver) { + this.streams = streams; + this.metadataResolver = metadataResolver; + this.messageResolver = messageResolver; + } + + @Override + public void onApplicationEvent(ApplicationStartedEvent event) { + final StreamsBuilder builder = new StreamsBuilder(); + + final KStream metadataTable = builder.stream(new ApplicationCommunicationMetadata().name()); + + final KTable> messageMetadataTable = metadataTable.toTable() + .filter((metadataId, metadata) -> isMessageMetadata(metadata)) + .groupBy((metadataId, metadata) -> KeyValue.pair(getSubject(metadata).getIdentifier(), metadata)) + .aggregate(HashMap::new, (metadataId, metadata, metadataMap) -> { + metadataMap.put(metadata.getKey(), metadata.getValue()); + return metadataMap; + }, (metadataId, metadata, metadataMap) -> { + metadataMap.remove(metadata.getKey()); + return metadataMap; + }); + + metadataTable + .filter((metadataId, metadata) -> metadataResolver.shouldResolve(metadata)) + .foreach((metadataId, metadata) -> metadataResolver.onMetadata(metadata)); + + builder.stream(new ApplicationCommunicationMessages().name()) + // Since the message content is immutable we only have to fetch + // the media for new messages + .filter((messageId, message) -> isNewMessage(message)) + .leftJoin(messageMetadataTable, MessageMediaRequest::new) + .foreach(messageResolver::onMessageMediaRequest); + + streams.start(builder.build(), appId); + } + + @Override + public void destroy() { + if (streams != null) { + streams.close(); + } + } + + // visible for testing + KafkaStreams.State getStreamState() { + return streams.state(); + } +} diff --git a/backend/media/src/main/java/co/airy/core/media/config/AwsConfig.java b/backend/media/src/main/java/co/airy/core/media/config/AwsConfig.java new file mode 100644 index 0000000000..f718d2be19 --- /dev/null +++ b/backend/media/src/main/java/co/airy/core/media/config/AwsConfig.java @@ -0,0 +1,31 @@ +package co.airy.core.media.config; + +import com.amazonaws.auth.AWSCredentials; +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AwsConfig { + + @Bean + public AmazonS3 amazonS3Client(@Value("${storage.s3.key}") final String mediaS3Key, + @Value("${storage.s3.secret}") final String mediaS3Secret, + @Value("${storage.s3.region}") final String region) { + AWSCredentials credentials = new BasicAWSCredentials( + mediaS3Key, + mediaS3Secret + ); + + return AmazonS3ClientBuilder + .standard() + .withCredentials(new AWSStaticCredentialsProvider(credentials)) + .withRegion(region) + .build(); + } +} diff --git a/backend/media/src/main/java/co/airy/core/media/dto/MessageMediaRequest.java b/backend/media/src/main/java/co/airy/core/media/dto/MessageMediaRequest.java new file mode 100644 index 0000000000..2e3a55b4d8 --- /dev/null +++ b/backend/media/src/main/java/co/airy/core/media/dto/MessageMediaRequest.java @@ -0,0 +1,17 @@ +package co.airy.core.media.dto; + +import co.airy.avro.communication.Message; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.Map; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MessageMediaRequest implements Serializable { + private Message message; + private Map metadata; +} diff --git a/backend/media/src/main/java/co/airy/core/media/services/MediaUpload.java b/backend/media/src/main/java/co/airy/core/media/services/MediaUpload.java new file mode 100644 index 0000000000..9ab3fa6c27 --- /dev/null +++ b/backend/media/src/main/java/co/airy/core/media/services/MediaUpload.java @@ -0,0 +1,60 @@ +package co.airy.core.media.services; + +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.CannedAccessControlList; +import com.amazonaws.services.s3.model.ObjectMetadata; +import com.amazonaws.services.s3.model.PutObjectRequest; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.retry.annotation.EnableRetry; +import org.springframework.retry.annotation.Retryable; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; + +@Service +@EnableRetry +public class MediaUpload { + private final AmazonS3 amazonS3Client; + private final String bucket; + private final URL host; + + public MediaUpload(AmazonS3 amazonS3Client, + @Value("${storage.s3.bucket}") String bucket, + @Value("${storage.s3.path}") String path) throws MalformedURLException { + this.amazonS3Client = amazonS3Client; + this.bucket = bucket; + URL bucketHost = new URL(String.format("https://%s.s3.amazonaws.com/", bucket)); + this.host = new URL(bucketHost, path); + } + + public boolean isUserStorageUrl(URL dataUrl) { + return dataUrl.getHost().equals(host.getHost()); + } + + @Retryable + public String uploadMedia(final InputStream is, final String fileName) throws Exception { + final String contentType = resolveContentType(fileName, is); + + final ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setContentType(contentType); + + final PutObjectRequest putObjectRequest = new PutObjectRequest( + bucket, + fileName, + is, + objectMetadata + ).withCannedAcl(CannedAccessControlList.PublicRead); + amazonS3Client.putObject(putObjectRequest); + + return new URL(host, fileName).toString(); + } + + private String resolveContentType(final String fileName, final InputStream is) throws IOException { + final String contentType = URLConnection.guessContentTypeFromStream(is); + return contentType == null ? URLConnection.guessContentTypeFromName(fileName) : contentType; + } +} diff --git a/backend/media/src/main/resources/application.properties b/backend/media/src/main/resources/application.properties index e456a64de1..d7e3c82c0f 100644 --- a/backend/media/src/main/resources/application.properties +++ b/backend/media/src/main/resources/application.properties @@ -1,4 +1,10 @@ kafka.brokers=${KAFKA_BROKERS} kafka.schema-registry-url=${KAFKA_SCHEMA_REGISTRY_URL} -storage.host=${STORAGE_HOST} +kafka.suppress-interval-ms=${KAFKA_SUPPRESS_INTERVAL_MS:3000} + +storage.s3.key=${STORAGE_S3_KEY} +storage.s3.secret=${STORAGE_S3_SECRET} +storage.s3.bucket=${STORAGE_S3_BUCKET} +storage.s3.region=${STORAGE_S3_REGION} +storage.s3.path=${STORAGE_S3_PATH:/} diff --git a/backend/media/src/test/java/co/airy/core/media/MessagesTest.java b/backend/media/src/test/java/co/airy/core/media/MessagesTest.java new file mode 100644 index 0000000000..dbe108ec69 --- /dev/null +++ b/backend/media/src/test/java/co/airy/core/media/MessagesTest.java @@ -0,0 +1,150 @@ +package co.airy.core.media; + +import co.airy.avro.communication.DeliveryState; +import co.airy.avro.communication.Message; +import co.airy.avro.communication.Metadata; +import co.airy.avro.communication.SenderType; +import co.airy.core.media.services.MediaUpload; +import co.airy.kafka.schema.application.ApplicationCommunicationMessages; +import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; +import co.airy.kafka.test.KafkaTestHelper; +import co.airy.kafka.test.junit.SharedKafkaTestResource; +import co.airy.mapping.ContentMapper; +import co.airy.mapping.model.Audio; +import co.airy.spring.core.AirySpringBootApplication; +import com.amazonaws.AmazonServiceException; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.services.s3.model.PutObjectResult; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.hamcrest.core.StringEndsWith; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import static co.airy.test.Timing.retryOnException; +import static org.apache.kafka.streams.KafkaStreams.State.RUNNING; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SpringBootTest(classes = AirySpringBootApplication.class) +@TestPropertySource(value = "classpath:test.properties") +@ExtendWith(SpringExtension.class) +public class MessagesTest { + @RegisterExtension + public static final SharedKafkaTestResource sharedKafkaTestResource = new SharedKafkaTestResource(); + private static KafkaTestHelper kafkaTestHelper; + private static final ApplicationCommunicationMessages applicationCommunicationMessages = new ApplicationCommunicationMessages(); + private static final ApplicationCommunicationMetadata applicationCommunicationMetadata = new ApplicationCommunicationMetadata(); + + @BeforeAll + static void beforeAll() throws Exception { + kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, + applicationCommunicationMetadata, + applicationCommunicationMessages + ); + + kafkaTestHelper.beforeAll(); + } + + @AfterAll + static void afterAll() throws Exception { + kafkaTestHelper.afterAll(); + } + + @Autowired + Stores stores; + + MediaUpload mediaUpload; + + @MockBean + private AmazonS3 amazonS3; + + @MockBean + private ContentMapper mapper; + + @Value("${storage.s3.bucket}") + private String bucket; + + @Value("${storage.s3.path}") + private String path; + + @BeforeEach + void beforeEach() throws Exception { + MockitoAnnotations.initMocks(this); + mediaUpload = new MediaUpload(amazonS3, bucket, path); + retryOnException(() -> assertEquals(stores.getStreamState(), RUNNING), "Failed to reach RUNNING state."); + } + + @Test + void storesMessageUrlsWithRetries() throws Exception { + final String originalUrl = "https://picsum.photos/1/1"; + final String urlHash = "64ff04d5ca0ad5951e64e3669c3dbd9159675e177a2ba237bf334495f4778da5"; + final String messageId = UUID.randomUUID().toString(); + + final String expectedUrl = String.format("https://%s.s3.amazonaws.com%sdata_%s", + bucket, path, urlHash); + + final ArgumentCaptor s3PutCaptor = ArgumentCaptor.forClass(PutObjectRequest.class); + when(mapper.renderWithDefaultAndLog(Mockito.any(), Mockito.any())).thenReturn(List.of(new Audio(originalUrl))); + + // Simulate a failure to trigger one retry + when(amazonS3.putObject(s3PutCaptor.capture())) + .thenThrow(AmazonServiceException.class) + .thenReturn(new PutObjectResult()); + + kafkaTestHelper.produceRecord( + new ProducerRecord<>(applicationCommunicationMessages.name(), messageId, + Message.newBuilder() + .setId(messageId) + .setSource("fakesource") + .setSentAt(Instant.now().toEpochMilli()) + .setUpdatedAt(null) + .setSenderId("sourceConversationId") + .setSenderType(SenderType.SOURCE_CONTACT) + .setDeliveryState(DeliveryState.DELIVERED) + .setConversationId("conversationId") + .setChannelId("channelId") + .setContent("mocked") + .build() + )); + + TimeUnit.SECONDS.sleep(10); + + List> metadataRecords = kafkaTestHelper.consumeRecords(2, applicationCommunicationMetadata.name()); + Metadata metadata = metadataRecords.stream() + .filter((record) -> record.value().getKey().equals(String.format("data_%s", originalUrl))) + .findFirst().get().value(); + + assertThat(metadata.getValue(), equalTo(expectedUrl)); + + verify(amazonS3, times(2)).putObject(Mockito.any(PutObjectRequest.class)); + final PutObjectRequest putObjectRequest = s3PutCaptor.getValue(); + assertThat(putObjectRequest.getBucketName(), equalTo(bucket)); + // The filename we wrote to the metadata has to match the file key we write to S3 + assertThat(metadata.getValue(), StringEndsWith.endsWith(putObjectRequest.getKey())); + } +} diff --git a/backend/media/src/test/java/co/airy/core/media/MetadataTest.java b/backend/media/src/test/java/co/airy/core/media/MetadataTest.java new file mode 100644 index 0000000000..0c0bf154e3 --- /dev/null +++ b/backend/media/src/test/java/co/airy/core/media/MetadataTest.java @@ -0,0 +1,135 @@ +package co.airy.core.media; + +import co.airy.avro.communication.Metadata; +import co.airy.core.media.services.MediaUpload; +import co.airy.kafka.schema.application.ApplicationCommunicationMessages; +import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; +import co.airy.kafka.test.KafkaTestHelper; +import co.airy.kafka.test.junit.SharedKafkaTestResource; +import co.airy.model.metadata.MetadataKeys; +import co.airy.spring.core.AirySpringBootApplication; +import com.amazonaws.AmazonServiceException; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.PutObjectRequest; +import com.amazonaws.services.s3.model.PutObjectResult; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.List; +import java.util.UUID; + +import static co.airy.model.metadata.MetadataRepository.newConversationMetadata; +import static co.airy.test.Timing.retryOnException; +import static org.apache.kafka.streams.KafkaStreams.State.RUNNING; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.isNotNull; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@SpringBootTest(classes = AirySpringBootApplication.class) +@TestPropertySource(value = "classpath:test.properties") +@ExtendWith(SpringExtension.class) +public class MetadataTest { + @RegisterExtension + public static final SharedKafkaTestResource sharedKafkaTestResource = new SharedKafkaTestResource(); + private static KafkaTestHelper kafkaTestHelper; + private static final ApplicationCommunicationMessages applicationCommunicationMessages = new ApplicationCommunicationMessages(); + private static final ApplicationCommunicationMetadata applicationCommunicationMetadata = new ApplicationCommunicationMetadata(); + + @BeforeAll + static void beforeAll() throws Exception { + kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, + applicationCommunicationMetadata, + applicationCommunicationMessages + ); + + kafkaTestHelper.beforeAll(); + } + + @AfterAll + static void afterAll() throws Exception { + kafkaTestHelper.afterAll(); + } + + @Autowired + Stores stores; + + @Autowired + + MediaUpload mediaUpload; + + @MockBean + private AmazonS3 amazonS3; + + @Value("${storage.s3.bucket}") + private String bucket; + + @Value("${storage.s3.path}") + private String path; + + @BeforeEach + void beforeEach() throws Exception { + MockitoAnnotations.initMocks(this); + mediaUpload = new MediaUpload(amazonS3, bucket, path); + retryOnException(() -> assertEquals(stores.getStreamState(), RUNNING), "Failed to reach RUNNING state."); + } + + @Test + void storesMetadataUrlsWithRetries() throws Exception { + final String conversationId = UUID.randomUUID().toString(); + final String originalUrl = "https://picsum.photos/1/1"; + final String metadataId = UUID.randomUUID().toString(); + final String expectedUrl = String.format("https://%s.s3.amazonaws.com%s%s/%s.resolved", + bucket, + path, + conversationId, + MetadataKeys.Source.Contact.AVATAR_URL); + + final ArgumentCaptor s3PutCaptor = ArgumentCaptor.forClass(PutObjectRequest.class); + + // Simulate a failure to trigger one retry + when(amazonS3.putObject(s3PutCaptor.capture())) + .thenThrow(AmazonServiceException.class) + .thenReturn(new PutObjectResult()); + + kafkaTestHelper.produceRecord( + new ProducerRecord<>(applicationCommunicationMetadata.name(), metadataId, + newConversationMetadata(conversationId, + MetadataKeys.Source.Contact.AVATAR_URL, + originalUrl) + )); + + List> metadataRecords = kafkaTestHelper.consumeRecords(2, applicationCommunicationMetadata.name()); + Metadata metadata = metadataRecords.stream() + .filter((record) -> !record.key().equals(metadataId)) + .findFirst().get().value(); + + assertThat(metadata.getValue(), equalTo(expectedUrl)); + + verify(amazonS3, times(2)).putObject(Mockito.any(PutObjectRequest.class)); + final PutObjectRequest putObjectRequest = s3PutCaptor.getValue(); + assertThat(putObjectRequest.getBucketName(), equalTo(bucket)); + assertThat(putObjectRequest.getKey(), + equalTo(String.format("%s/%s", conversationId, MetadataKeys.Source.Contact.AVATAR_URL + ".resolved"))); + } +} diff --git a/backend/media/src/test/resources/test.properties b/backend/media/src/test/resources/test.properties new file mode 100644 index 0000000000..86dde47a14 --- /dev/null +++ b/backend/media/src/test/resources/test.properties @@ -0,0 +1,10 @@ +kafka.cleanup=true +kafka.cache.max.bytes=0 +kafka.commit-interval-ms=100 +kafka.suppress-interval-ms=0 + +storage.s3.key=no +storage.s3.secret=no +storage.s3.bucket=mybucket +storage.s3.region=us-east-1 +storage.s3.path=/core-media/ diff --git a/backend/model/message/src/main/java/co/airy/model/message/MessageRepository.java b/backend/model/message/src/main/java/co/airy/model/message/MessageRepository.java index fa9cabe23d..6166a7ad4f 100644 --- a/backend/model/message/src/main/java/co/airy/model/message/MessageRepository.java +++ b/backend/model/message/src/main/java/co/airy/model/message/MessageRepository.java @@ -11,4 +11,8 @@ public static Message updateDeliveryState(Message message, DeliveryState state) message.setUpdatedAt(Instant.now().toEpochMilli()); return message; } + + public static boolean isNewMessage(Message message) { + return message.getUpdatedAt() == null; + } } diff --git a/backend/model/metadata/src/main/java/co/airy/model/metadata/MetadataRepository.java b/backend/model/metadata/src/main/java/co/airy/model/metadata/MetadataRepository.java index f11599dedc..48517d752e 100644 --- a/backend/model/metadata/src/main/java/co/airy/model/metadata/MetadataRepository.java +++ b/backend/model/metadata/src/main/java/co/airy/model/metadata/MetadataRepository.java @@ -21,7 +21,16 @@ public static Map filterPrefix(Map metadataMap, public static Metadata newConversationMetadata(String conversationId, String key, String value) { return Metadata.newBuilder() - .setSubject(new Subject("conversation",conversationId).toString()) + .setSubject(new Subject("conversation", conversationId).toString()) + .setKey(key) + .setValue(value) + .setTimestamp(Instant.now().toEpochMilli()) + .build(); + } + + public static Metadata newMessageMetadata(String messageId, String key, String value) { + return Metadata.newBuilder() + .setSubject(new Subject("message", messageId).toString()) .setKey(key) .setValue(value) .setTimestamp(Instant.now().toEpochMilli()) @@ -32,6 +41,10 @@ public static boolean isConversationMetadata(Metadata metadata) { return metadata.getSubject().startsWith("conversation:"); } + public static boolean isMessageMetadata(Metadata metadata) { + return metadata.getSubject().startsWith("message:"); + } + public static Map getConversationInfo(Map metadataMap) { return filterPrefix(metadataMap, PUBLIC); } diff --git a/backend/sources/twilio/events-router/BUILD b/backend/sources/twilio/events-router/BUILD index a1b92003fa..63e98d23b1 100644 --- a/backend/sources/twilio/events-router/BUILD +++ b/backend/sources/twilio/events-router/BUILD @@ -8,6 +8,7 @@ app_deps = [ "//backend/model/message", "//lib/java/uuid", "//lib/java/log", + "//lib/java/url", "//lib/java/kafka/schema:source-twilio-events", "//lib/java/spring/kafka/core:spring-kafka-core", "//lib/java/spring/kafka/streams:spring-kafka-streams", diff --git a/backend/sources/twilio/events-router/src/main/java/co/airy/core/sources/twilio/TwilioInfoExtractor.java b/backend/sources/twilio/events-router/src/main/java/co/airy/core/sources/twilio/TwilioInfoExtractor.java index fbedc96a9b..c90e748a8c 100644 --- a/backend/sources/twilio/events-router/src/main/java/co/airy/core/sources/twilio/TwilioInfoExtractor.java +++ b/backend/sources/twilio/events-router/src/main/java/co/airy/core/sources/twilio/TwilioInfoExtractor.java @@ -1,14 +1,8 @@ package co.airy.core.sources.twilio; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.List; import java.util.Map; -import java.util.Objects; - -import static java.util.stream.Collectors.toMap; +import static co.airy.url.UrlUtil.parseUrlEncoded; public class TwilioInfoExtractor { @@ -21,24 +15,4 @@ static TwilioEventInfo extract(String payload) { .payload(payload) .build(); } - - static Map parseUrlEncoded(String payload) { - List kvPairs = Arrays.asList(payload.split("&")); - - return kvPairs.stream() - .map((kvPair) -> { - String[] fields = kvPair.split("="); - - if (fields.length != 2) { - return null; - } - - String name = URLDecoder.decode(fields[0], StandardCharsets.UTF_8); - String value = URLDecoder.decode(fields[1], StandardCharsets.UTF_8); - - return List.of(name, value); - }) - .filter(Objects::nonNull) - .collect(toMap((tuple) -> tuple.get(0), (tuple) -> tuple.get(1))); - } } diff --git a/docs/docs/guides/airy-core-in-production.md b/docs/docs/guides/airy-core-in-production.md index 0e0d38680f..86ea95fd66 100644 --- a/docs/docs/guides/airy-core-in-production.md +++ b/docs/docs/guides/airy-core-in-production.md @@ -158,21 +158,37 @@ cd infrastructure cp airy.conf.all airy.conf ``` -Edit the file to configure connections to the base services. Make sure that the -following sections are configured correctly, so that the `Airy apps` to start -properly: +Edit the file to configure connections to the base services. Make sure to configure the +following sections correctly, so that the `Airy apps` start properly: -``` +```yaml apps: - kafka: - ... - redis: - ... - postgresql: - ... + kafka: ... + redis: ... + postgresql: ... ``` -We recommend to create a new database if you are reusing a PostgreSQL server to avoid name collisions. +We recommend that you create a new database if you are reusing a PostgreSQL server to avoid name collisions. + +## Source media storage + +Most message sources allow users to send rich data such as images, videos and audio files. For some sources +the Urls that host this data expire which is why after some time you may find that conversations have inaccessible +content. + +The Airy Core Platform allows you to persist this data to a storage of your choice. To take advantage of this +you must provide access credentials to your storage. The platform currently supports [s3](https://aws.amazon.com/s3/): + +```yaml +apps: + storage: + s3: + key: + secret: + bucket: + region: + path: <(optional) defaults to the bucket root> +``` ### Deployment diff --git a/infrastructure/airy.conf.all b/infrastructure/airy.conf.all index a9db0e8404..d8095ca83b 100644 --- a/infrastructure/airy.conf.all +++ b/infrastructure/airy.conf.all @@ -3,9 +3,9 @@ global: appImageTag: latest containerRegistry: ghcr.io/airyhq namespace: default -# Configuration for the Kafka cluster core: apps: + # Configuration for the Kafka cluster kafka: brokers: "kafka-headless:9092" schemaRegistryUrl: "http://schema-registry:8081" @@ -18,7 +18,7 @@ core: postgresql: endpoint: "postgres:5432" dbName: "admin" - username: "postgresadmin" + username: "postgresadmin" password: "long-random-generated-password" # Specific configurations for sources sources: @@ -42,3 +42,10 @@ core: mailPassword: "changeme" jwtSecret: "long-random-generated-jwt-secret" allowedOrigins: "*" + storage: + s3: + key: "changeme" + secret: "changeme" + bucket: "changeme" + region: "changeme" + path: "path" diff --git a/infrastructure/controller/pkg/configmap-controller/configmap-controller.go b/infrastructure/controller/pkg/configmap-controller/configmap-controller.go index edfdda7b99..4e9789bfe4 100644 --- a/infrastructure/controller/pkg/configmap-controller/configmap-controller.go +++ b/infrastructure/controller/pkg/configmap-controller/configmap-controller.go @@ -69,7 +69,6 @@ func (c *Controller) Handle(key string) error { for _, affectedDeployment := range affectedDeployments { klog.Infof("Scheduling reload for deployment: %s", affectedDeployment) handler.ReloadDeployment(c.clientset, "default", affectedDeployment) - } } return nil diff --git a/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/user-storage.yaml b/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/user-storage.yaml new file mode 100644 index 0000000000..e1197a5d02 --- /dev/null +++ b/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/user-storage.yaml @@ -0,0 +1,14 @@ +{{ if .Values.storage }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: user-storage +data: + {{- if .Values.storage.s3 -}} + STORAGE_S3_KEY: {{ .Values.storage.s3.key }} + STORAGE_S3_SECRET: "{{ .Values.storage.s3.secret }}" + STORAGE_S3_BUCKET: {{ .Values.storage.s3.bucket }} + STORAGE_S3_REGION: {{ .Values.storage.s3.region }} + STORAGE_S3_PATH: {{ .Values.storage.s3.path }} + {{- end -}} +{{ end }} diff --git a/infrastructure/helm-chart/charts/apps/charts/airy-config/values.yaml b/infrastructure/helm-chart/charts/apps/charts/airy-config/values.yaml index 12d4d27b07..39707d6b13 100644 --- a/infrastructure/helm-chart/charts/apps/charts/airy-config/values.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/airy-config/values.yaml @@ -7,7 +7,7 @@ redis: port: 6379 postgresql: endpoint: "postgres:5432" - username: "postgresadmin" + username: "postgresadmin" password: "changeme" sources: facebook: diff --git a/infrastructure/helm-chart/charts/apps/charts/media-resolver/Chart.yaml b/infrastructure/helm-chart/charts/apps/charts/media-resolver/Chart.yaml new file mode 100644 index 0000000000..8e8e7a0e37 --- /dev/null +++ b/infrastructure/helm-chart/charts/apps/charts/media-resolver/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for the Media Resolver app +name: media-resolver +version: 0.1.0 diff --git a/infrastructure/helm-chart/charts/apps/charts/media-resolver/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/media-resolver/templates/deployment.yaml new file mode 100644 index 0000000000..2379d5f75b --- /dev/null +++ b/infrastructure/helm-chart/charts/apps/charts/media-resolver/templates/deployment.yaml @@ -0,0 +1,58 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: media-resolver + namespace: default + labels: + app: media-resolver + type: media +spec: + replicas: 0 + selector: + matchLabels: + app: media-resolver + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: media-resolver + spec: + containers: + - name: app + image: "{{ .Values.global.containerRegistry}}/{{ .Values.image }}:{{ .Values.global.appImageTag }}" + imagePullPolicy: Always + envFrom: + - configMapRef: + name: user-storage + env: + - name: KAFKA_BROKERS + valueFrom: + configMapKeyRef: + name: kafka-config + key: KAFKA_BROKERS + - name: KAFKA_SCHEMA_REGISTRY_URL + valueFrom: + configMapKeyRef: + name: kafka-config + key: KAFKA_SCHEMA_REGISTRY_URL + - name: KAFKA_COMMIT_INTERVAL_MS + valueFrom: + configMapKeyRef: + name: kafka-config + key: KAFKA_COMMIT_INTERVAL_MS + - name: SERVICE_NAME + value: media-resolver + livenessProbe: + httpGet: + path: /actuator/health + port: 8080 + httpHeaders: + - name: Health-Check + value: health-check + initialDelaySeconds: 60 + periodSeconds: 10 + failureThreshold: 3 diff --git a/infrastructure/helm-chart/charts/apps/charts/media-resolver/templates/service.yaml b/infrastructure/helm-chart/charts/apps/charts/media-resolver/templates/service.yaml new file mode 100644 index 0000000000..8cd982a016 --- /dev/null +++ b/infrastructure/helm-chart/charts/apps/charts/media-resolver/templates/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: media-resolver + namespace: default +spec: + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + type: NodePort + selector: + app: media-resolver diff --git a/infrastructure/helm-chart/charts/apps/charts/media-resolver/values.yaml b/infrastructure/helm-chart/charts/apps/charts/media-resolver/values.yaml new file mode 100644 index 0000000000..6a2b9ac149 --- /dev/null +++ b/infrastructure/helm-chart/charts/apps/charts/media-resolver/values.yaml @@ -0,0 +1 @@ +image: media/resolver diff --git a/infrastructure/scripts/trigger/stop.sh b/infrastructure/scripts/trigger/stop.sh index cbe03d2f9c..08bea932bb 100755 --- a/infrastructure/scripts/trigger/stop.sh +++ b/infrastructure/scripts/trigger/stop.sh @@ -9,6 +9,7 @@ kubectl scale deployment -l type=sources-google --replicas=0 kubectl scale deployment -l type=sources-facebook --replicas=0 kubectl scale deployment -l type=sources-chatplugin --replicas=0 kubectl scale deployment -l type=webhook --replicas=0 +kubectl scale deployment -l type=media --replicas=0 kubectl scale deployment -l type=sources-chatplugin --replicas=0 kubectl scale deployment -l type=api --replicas=0 kubectl scale deployment schema-registry --replicas=0 diff --git a/lib/java/mapping/src/test/java/co/airy/mapping/ContentMapperTest.java b/lib/java/mapping/src/test/java/co/airy/mapping/ContentMapperTest.java index c2bc10c2d6..839a9c02f1 100644 --- a/lib/java/mapping/src/test/java/co/airy/mapping/ContentMapperTest.java +++ b/lib/java/mapping/src/test/java/co/airy/mapping/ContentMapperTest.java @@ -102,7 +102,6 @@ public List render(String payload) { final String persistentUrl = "http://storage.org/path/data"; final Map messageMetadata = Map.of("data_" + originalUrl, persistentUrl); - // No replacement without metadata audioMessage = (Audio) mapper.render(message, messageMetadata).get(0); assertThat(audioMessage.getUrl(), equalTo(persistentUrl)); } diff --git a/lib/java/url/BUILD b/lib/java/url/BUILD new file mode 100644 index 0000000000..e16ada072e --- /dev/null +++ b/lib/java/url/BUILD @@ -0,0 +1,7 @@ +load("//tools/build:java_library.bzl", "custom_java_library") + +custom_java_library( + name = "url", + srcs = glob(["src/main/java/co/airy/url/**/*.java"]), + visibility = ["//visibility:public"], +) diff --git a/lib/java/url/src/main/java/co/airy/url/UrlUtil.java b/lib/java/url/src/main/java/co/airy/url/UrlUtil.java new file mode 100644 index 0000000000..cfdfaadacf --- /dev/null +++ b/lib/java/url/src/main/java/co/airy/url/UrlUtil.java @@ -0,0 +1,29 @@ +package co.airy.url; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static java.util.stream.Collectors.toMap; + +public class UrlUtil { + public static Map parseUrlEncoded(String payload) { + List kvPairs = Arrays.asList(payload.split("&")); + + return kvPairs.stream() + .map((kvPair) -> { + String[] fields = kvPair.split("="); + + String name = URLDecoder.decode(fields[0], StandardCharsets.UTF_8); + String value = ""; + if (fields.length > 1) { + value = URLDecoder.decode(fields[1], StandardCharsets.UTF_8); + } + + return List.of(name, value); + }) + .collect(toMap((tuple) -> tuple.get(0), (tuple) -> tuple.get(1))); + } +} diff --git a/maven_install.json b/maven_install.json index edd7cea957..6d9d1796fa 100644 --- a/maven_install.json +++ b/maven_install.json @@ -1,6 +1,6 @@ { "dependency_tree": { - "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": -1613073853, + "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": 1000798703, "conflict_resolution": { "com.fasterxml.jackson.core:jackson-annotations:2.10.0": "com.fasterxml.jackson.core:jackson-annotations:2.11.2", "com.fasterxml.jackson.core:jackson-core:2.10.0": "com.fasterxml.jackson.core:jackson-core:2.11.2", @@ -68,6 +68,146 @@ "sha256": "72e05e5031508115cafa6092cd53af306c5584957a34012511a20aac5e6c45e5", "url": "https://repo1.maven.org/maven2/com/101tec/zkclient/0.11/zkclient-0.11.jar" }, + { + "coord": "com.amazonaws:aws-java-sdk-core:1.11.933", + "dependencies": [ + "com.fasterxml.jackson.core:jackson-core:2.11.2", + "commons-logging:commons-logging:1.2", + "software.amazon.ion:ion-java:1.0.2", + "commons-codec:commons-codec:1.11", + "org.apache.httpcomponents:httpcore:4.4.13", + "joda-time:joda-time:2.10.2", + "com.fasterxml.jackson.core:jackson-annotations:2.11.2", + "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:jar:2.6.7", + "org.apache.httpcomponents:httpclient:4.5.13", + "com.fasterxml.jackson.core:jackson-databind:jar:2.11.2" + ], + "directDependencies": [ + "commons-logging:commons-logging:1.2", + "software.amazon.ion:ion-java:1.0.2", + "joda-time:joda-time:2.10.2", + "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:jar:2.6.7", + "org.apache.httpcomponents:httpclient:4.5.13", + "com.fasterxml.jackson.core:jackson-databind:jar:2.11.2" + ], + "exclusions": [ + "ch.qos.logback:logback-classic", + "org.springframework.boot:spring-boot-starter-tomcat", + "org.springframework.boot:spring-boot-starter-logging", + "org.slf4j:slf4j-log4j12" + ], + "file": "v1/https/repo1.maven.org/maven2/com/amazonaws/aws-java-sdk-core/1.11.933/aws-java-sdk-core-1.11.933.jar", + "mirror_urls": [ + "https://packages.confluent.io/maven/com/amazonaws/aws-java-sdk-core/1.11.933/aws-java-sdk-core-1.11.933.jar", + "https://oss.sonatype.org/content/repositories/snapshots/com/amazonaws/aws-java-sdk-core/1.11.933/aws-java-sdk-core-1.11.933.jar", + "https://repo1.maven.org/maven2/com/amazonaws/aws-java-sdk-core/1.11.933/aws-java-sdk-core-1.11.933.jar", + "https://jitpack.io/com/amazonaws/aws-java-sdk-core/1.11.933/aws-java-sdk-core-1.11.933.jar" + ], + "sha256": "3ec5d0fc6a6a605f74f5ac736bd3a96d189c113ee7fd84117b8b1281223c224c", + "url": "https://repo1.maven.org/maven2/com/amazonaws/aws-java-sdk-core/1.11.933/aws-java-sdk-core-1.11.933.jar" + }, + { + "coord": "com.amazonaws:aws-java-sdk-kms:1.11.933", + "dependencies": [ + "com.fasterxml.jackson.core:jackson-core:2.11.2", + "com.amazonaws:jmespath-java:1.11.933", + "com.amazonaws:aws-java-sdk-core:1.11.933", + "commons-logging:commons-logging:1.2", + "software.amazon.ion:ion-java:1.0.2", + "commons-codec:commons-codec:1.11", + "org.apache.httpcomponents:httpcore:4.4.13", + "joda-time:joda-time:2.10.2", + "com.fasterxml.jackson.core:jackson-annotations:2.11.2", + "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:jar:2.6.7", + "org.apache.httpcomponents:httpclient:4.5.13", + "com.fasterxml.jackson.core:jackson-databind:2.11.2", + "com.fasterxml.jackson.core:jackson-databind:jar:2.11.2" + ], + "directDependencies": [ + "com.amazonaws:aws-java-sdk-core:1.11.933", + "com.amazonaws:jmespath-java:1.11.933" + ], + "exclusions": [ + "ch.qos.logback:logback-classic", + "org.springframework.boot:spring-boot-starter-tomcat", + "org.springframework.boot:spring-boot-starter-logging", + "org.slf4j:slf4j-log4j12" + ], + "file": "v1/https/repo1.maven.org/maven2/com/amazonaws/aws-java-sdk-kms/1.11.933/aws-java-sdk-kms-1.11.933.jar", + "mirror_urls": [ + "https://packages.confluent.io/maven/com/amazonaws/aws-java-sdk-kms/1.11.933/aws-java-sdk-kms-1.11.933.jar", + "https://oss.sonatype.org/content/repositories/snapshots/com/amazonaws/aws-java-sdk-kms/1.11.933/aws-java-sdk-kms-1.11.933.jar", + "https://repo1.maven.org/maven2/com/amazonaws/aws-java-sdk-kms/1.11.933/aws-java-sdk-kms-1.11.933.jar", + "https://jitpack.io/com/amazonaws/aws-java-sdk-kms/1.11.933/aws-java-sdk-kms-1.11.933.jar" + ], + "sha256": "12311f2824c5fd1a8d8f6aef7fbbf450192c406507003c3aa22aa1e660612035", + "url": "https://repo1.maven.org/maven2/com/amazonaws/aws-java-sdk-kms/1.11.933/aws-java-sdk-kms-1.11.933.jar" + }, + { + "coord": "com.amazonaws:aws-java-sdk-s3:1.11.933", + "dependencies": [ + "com.fasterxml.jackson.core:jackson-core:2.11.2", + "com.amazonaws:jmespath-java:1.11.933", + "com.amazonaws:aws-java-sdk-core:1.11.933", + "commons-logging:commons-logging:1.2", + "com.amazonaws:aws-java-sdk-kms:1.11.933", + "software.amazon.ion:ion-java:1.0.2", + "commons-codec:commons-codec:1.11", + "org.apache.httpcomponents:httpcore:4.4.13", + "joda-time:joda-time:2.10.2", + "com.fasterxml.jackson.core:jackson-annotations:2.11.2", + "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:jar:2.6.7", + "org.apache.httpcomponents:httpclient:4.5.13", + "com.fasterxml.jackson.core:jackson-databind:2.11.2", + "com.fasterxml.jackson.core:jackson-databind:jar:2.11.2" + ], + "directDependencies": [ + "com.amazonaws:aws-java-sdk-core:1.11.933", + "com.amazonaws:aws-java-sdk-kms:1.11.933", + "com.amazonaws:jmespath-java:1.11.933" + ], + "exclusions": [ + "ch.qos.logback:logback-classic", + "org.springframework.boot:spring-boot-starter-tomcat", + "org.springframework.boot:spring-boot-starter-logging", + "org.slf4j:slf4j-log4j12" + ], + "file": "v1/https/repo1.maven.org/maven2/com/amazonaws/aws-java-sdk-s3/1.11.933/aws-java-sdk-s3-1.11.933.jar", + "mirror_urls": [ + "https://packages.confluent.io/maven/com/amazonaws/aws-java-sdk-s3/1.11.933/aws-java-sdk-s3-1.11.933.jar", + "https://oss.sonatype.org/content/repositories/snapshots/com/amazonaws/aws-java-sdk-s3/1.11.933/aws-java-sdk-s3-1.11.933.jar", + "https://repo1.maven.org/maven2/com/amazonaws/aws-java-sdk-s3/1.11.933/aws-java-sdk-s3-1.11.933.jar", + "https://jitpack.io/com/amazonaws/aws-java-sdk-s3/1.11.933/aws-java-sdk-s3-1.11.933.jar" + ], + "sha256": "8201d0e4db03e80050bf6e57009e3a71d47c31f4fe9e5bba3faa19ae5099a035", + "url": "https://repo1.maven.org/maven2/com/amazonaws/aws-java-sdk-s3/1.11.933/aws-java-sdk-s3-1.11.933.jar" + }, + { + "coord": "com.amazonaws:jmespath-java:1.11.933", + "dependencies": [ + "com.fasterxml.jackson.core:jackson-databind:2.11.2", + "com.fasterxml.jackson.core:jackson-core:2.11.2", + "com.fasterxml.jackson.core:jackson-annotations:2.11.2" + ], + "directDependencies": [ + "com.fasterxml.jackson.core:jackson-databind:2.11.2" + ], + "exclusions": [ + "ch.qos.logback:logback-classic", + "org.springframework.boot:spring-boot-starter-tomcat", + "org.springframework.boot:spring-boot-starter-logging", + "org.slf4j:slf4j-log4j12" + ], + "file": "v1/https/repo1.maven.org/maven2/com/amazonaws/jmespath-java/1.11.933/jmespath-java-1.11.933.jar", + "mirror_urls": [ + "https://packages.confluent.io/maven/com/amazonaws/jmespath-java/1.11.933/jmespath-java-1.11.933.jar", + "https://oss.sonatype.org/content/repositories/snapshots/com/amazonaws/jmespath-java/1.11.933/jmespath-java-1.11.933.jar", + "https://repo1.maven.org/maven2/com/amazonaws/jmespath-java/1.11.933/jmespath-java-1.11.933.jar", + "https://jitpack.io/com/amazonaws/jmespath-java/1.11.933/jmespath-java-1.11.933.jar" + ], + "sha256": "e8752d6d6f857f86c886957bbc20160b2fd22750d10f28f2bbac783cc8351ff7", + "url": "https://repo1.maven.org/maven2/com/amazonaws/jmespath-java/1.11.933/jmespath-java-1.11.933.jar" + }, { "coord": "com.cedarsoftware:java-util:1.34.0", "dependencies": [], @@ -179,6 +319,30 @@ "sha256": "cb890b4aad8ed21a7b57e3c8f7924dbdca1aeff9ddd27cb0ff37243037ae1342", "url": "https://repo1.maven.org/maven2/com/fasterxml/jackson/core/jackson-databind/2.11.2/jackson-databind-2.11.2.jar" }, + { + "coord": "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:jar:2.6.7", + "dependencies": [ + "com.fasterxml.jackson.core:jackson-core:2.11.2" + ], + "directDependencies": [ + "com.fasterxml.jackson.core:jackson-core:2.11.2" + ], + "exclusions": [ + "ch.qos.logback:logback-classic", + "org.springframework.boot:spring-boot-starter-tomcat", + "org.springframework.boot:spring-boot-starter-logging", + "org.slf4j:slf4j-log4j12" + ], + "file": "v1/https/repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-cbor/2.6.7/jackson-dataformat-cbor-2.6.7.jar", + "mirror_urls": [ + "https://packages.confluent.io/maven/com/fasterxml/jackson/dataformat/jackson-dataformat-cbor/2.6.7/jackson-dataformat-cbor-2.6.7.jar", + "https://oss.sonatype.org/content/repositories/snapshots/com/fasterxml/jackson/dataformat/jackson-dataformat-cbor/2.6.7/jackson-dataformat-cbor-2.6.7.jar", + "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-cbor/2.6.7/jackson-dataformat-cbor-2.6.7.jar", + "https://jitpack.io/com/fasterxml/jackson/dataformat/jackson-dataformat-cbor/2.6.7/jackson-dataformat-cbor-2.6.7.jar" + ], + "sha256": "956a0fb9186a796b8a6548909da1ee55004279647e261c7f540e5d49d4f199bf", + "url": "https://repo1.maven.org/maven2/com/fasterxml/jackson/dataformat/jackson-dataformat-cbor/2.6.7/jackson-dataformat-cbor-2.6.7.jar" + }, { "coord": "com.fasterxml.jackson.dataformat:jackson-dataformat-csv:2.10.2", "dependencies": [ @@ -702,7 +866,6 @@ "dependencies": [ "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava", "com.fasterxml.jackson.core:jackson-core:2.11.2", - "org.apache.httpcomponents:httpclient:4.5.10", "com.google.j2objc:j2objc-annotations:1.3", "commons-logging:commons-logging:1.2", "io.opencensus:opencensus-contrib-http-util:0.24.0", @@ -712,12 +875,13 @@ "commons-codec:commons-codec:1.11", "io.opencensus:opencensus-api:0.24.0", "io.grpc:grpc-context:1.22.1", + "org.apache.httpcomponents:httpcore:4.4.13", "com.google.http-client:google-http-client-jackson2:1.34.0", "com.google.errorprone:error_prone_annotations:2.3.4", "com.google.http-client:google-http-client:1.34.0", "com.google.guava:failureaccess:1.0.1", "com.google.guava:guava:29.0-jre", - "org.apache.httpcomponents:httpcore:4.4.12", + "org.apache.httpcomponents:httpclient:4.5.13", "org.checkerframework:checker-qual:2.11.1" ], "directDependencies": [ @@ -903,7 +1067,6 @@ "dependencies": [ "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava", "com.fasterxml.jackson.core:jackson-core:2.11.2", - "org.apache.httpcomponents:httpclient:4.5.10", "com.google.j2objc:j2objc-annotations:1.3", "commons-logging:commons-logging:1.2", "io.opencensus:opencensus-contrib-http-util:0.24.0", @@ -911,11 +1074,12 @@ "commons-codec:commons-codec:1.11", "io.opencensus:opencensus-api:0.24.0", "io.grpc:grpc-context:1.22.1", + "org.apache.httpcomponents:httpcore:4.4.13", "com.google.errorprone:error_prone_annotations:2.3.4", "com.google.http-client:google-http-client:1.34.0", "com.google.guava:failureaccess:1.0.1", "com.google.guava:guava:29.0-jre", - "org.apache.httpcomponents:httpcore:4.4.12", + "org.apache.httpcomponents:httpclient:4.5.13", "org.checkerframework:checker-qual:2.11.1" ], "directDependencies": [ @@ -942,7 +1106,6 @@ "coord": "com.google.http-client:google-http-client:1.34.0", "dependencies": [ "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava", - "org.apache.httpcomponents:httpclient:4.5.10", "com.google.j2objc:j2objc-annotations:1.3", "commons-logging:commons-logging:1.2", "io.opencensus:opencensus-contrib-http-util:0.24.0", @@ -950,20 +1113,21 @@ "commons-codec:commons-codec:1.11", "io.opencensus:opencensus-api:0.24.0", "io.grpc:grpc-context:1.22.1", + "org.apache.httpcomponents:httpcore:4.4.13", "com.google.errorprone:error_prone_annotations:2.3.4", "com.google.guava:failureaccess:1.0.1", "com.google.guava:guava:29.0-jre", - "org.apache.httpcomponents:httpcore:4.4.12", + "org.apache.httpcomponents:httpclient:4.5.13", "org.checkerframework:checker-qual:2.11.1" ], "directDependencies": [ - "org.apache.httpcomponents:httpclient:4.5.10", "com.google.j2objc:j2objc-annotations:1.3", "io.opencensus:opencensus-contrib-http-util:0.24.0", "com.google.code.findbugs:jsr305:3.0.2", "io.opencensus:opencensus-api:0.24.0", + "org.apache.httpcomponents:httpcore:4.4.13", "com.google.guava:guava:29.0-jre", - "org.apache.httpcomponents:httpcore:4.4.12" + "org.apache.httpcomponents:httpclient:4.5.13" ], "exclusions": [ "ch.qos.logback:logback-classic", @@ -1447,7 +1611,6 @@ "dependencies": [ "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava", "com.fasterxml.jackson.core:jackson-core:2.11.2", - "org.apache.httpcomponents:httpclient:4.5.10", "javax.xml.bind:jaxb-api:2.3.1", "com.google.j2objc:j2objc-annotations:1.3", "commons-logging:commons-logging:1.2", @@ -1455,27 +1618,28 @@ "com.google.code.findbugs:jsr305:3.0.2", "javax.activation:javax.activation-api:1.2.0", "io.jsonwebtoken:jjwt-jackson:0.10.7", + "org.apache.httpcomponents:httpcore:4.4.13", "com.google.errorprone:error_prone_annotations:2.3.4", "io.jsonwebtoken:jjwt-api:0.10.7", "joda-time:joda-time:2.10.2", "com.fasterxml.jackson.core:jackson-annotations:2.11.2", "com.google.guava:failureaccess:1.0.1", "com.google.guava:guava:29.0-jre", - "org.apache.httpcomponents:httpcore:4.4.12", + "org.apache.httpcomponents:httpclient:4.5.13", "com.fasterxml.jackson.core:jackson-databind:2.11.2", "org.checkerframework:checker-qual:2.11.1" ], "directDependencies": [ "com.fasterxml.jackson.core:jackson-core:2.11.2", - "org.apache.httpcomponents:httpclient:4.5.10", "javax.xml.bind:jaxb-api:2.3.1", "io.jsonwebtoken:jjwt-impl:0.10.7", "io.jsonwebtoken:jjwt-jackson:0.10.7", + "org.apache.httpcomponents:httpcore:4.4.13", "io.jsonwebtoken:jjwt-api:0.10.7", "joda-time:joda-time:2.10.2", "com.fasterxml.jackson.core:jackson-annotations:2.11.2", "com.google.guava:guava:29.0-jre", - "org.apache.httpcomponents:httpcore:4.4.12", + "org.apache.httpcomponents:httpclient:4.5.13", "com.fasterxml.jackson.core:jackson-databind:2.11.2" ], "exclusions": [ @@ -4560,34 +4724,35 @@ "url": "https://repo1.maven.org/maven2/org/apache/curator/curator-test/4.2.0/curator-test-4.2.0.jar" }, { - "coord": "org.apache.httpcomponents:httpclient:4.5.10", + "coord": "org.apache.httpcomponents:httpclient:4.5.13", "dependencies": [ + "org.apache.httpcomponents:httpcore:4.4.13", "commons-logging:commons-logging:1.2", - "org.apache.httpcomponents:httpcore:4.4.12" + "commons-codec:commons-codec:1.11" ], "directDependencies": [ + "commons-codec:commons-codec:1.11", "commons-logging:commons-logging:1.2", - "org.apache.httpcomponents:httpcore:4.4.12" + "org.apache.httpcomponents:httpcore:4.4.13" ], "exclusions": [ - "org.slf4j:slf4j-log4j12", - "commons-codec:commons-codec", - "org.springframework.boot:spring-boot-starter-tomcat", "ch.qos.logback:logback-classic", - "org.springframework.boot:spring-boot-starter-logging" + "org.springframework.boot:spring-boot-starter-tomcat", + "org.springframework.boot:spring-boot-starter-logging", + "org.slf4j:slf4j-log4j12" ], - "file": "v1/https/repo1.maven.org/maven2/org/apache/httpcomponents/httpclient/4.5.10/httpclient-4.5.10.jar", + "file": "v1/https/repo1.maven.org/maven2/org/apache/httpcomponents/httpclient/4.5.13/httpclient-4.5.13.jar", "mirror_urls": [ - "https://packages.confluent.io/maven/org/apache/httpcomponents/httpclient/4.5.10/httpclient-4.5.10.jar", - "https://oss.sonatype.org/content/repositories/snapshots/org/apache/httpcomponents/httpclient/4.5.10/httpclient-4.5.10.jar", - "https://repo1.maven.org/maven2/org/apache/httpcomponents/httpclient/4.5.10/httpclient-4.5.10.jar", - "https://jitpack.io/org/apache/httpcomponents/httpclient/4.5.10/httpclient-4.5.10.jar" + "https://packages.confluent.io/maven/org/apache/httpcomponents/httpclient/4.5.13/httpclient-4.5.13.jar", + "https://oss.sonatype.org/content/repositories/snapshots/org/apache/httpcomponents/httpclient/4.5.13/httpclient-4.5.13.jar", + "https://repo1.maven.org/maven2/org/apache/httpcomponents/httpclient/4.5.13/httpclient-4.5.13.jar", + "https://jitpack.io/org/apache/httpcomponents/httpclient/4.5.13/httpclient-4.5.13.jar" ], - "sha256": "38b9f16f504928e4db736a433b9cd10968d9ec8d6f5d0e61a64889a689172134", - "url": "https://repo1.maven.org/maven2/org/apache/httpcomponents/httpclient/4.5.10/httpclient-4.5.10.jar" + "sha256": "6fe9026a566c6a5001608cf3fc32196641f6c1e5e1986d1037ccdbd5f31ef743", + "url": "https://repo1.maven.org/maven2/org/apache/httpcomponents/httpclient/4.5.13/httpclient-4.5.13.jar" }, { - "coord": "org.apache.httpcomponents:httpcore:4.4.12", + "coord": "org.apache.httpcomponents:httpcore:4.4.13", "dependencies": [], "directDependencies": [], "exclusions": [ @@ -4596,15 +4761,15 @@ "org.springframework.boot:spring-boot-starter-logging", "org.slf4j:slf4j-log4j12" ], - "file": "v1/https/repo1.maven.org/maven2/org/apache/httpcomponents/httpcore/4.4.12/httpcore-4.4.12.jar", + "file": "v1/https/repo1.maven.org/maven2/org/apache/httpcomponents/httpcore/4.4.13/httpcore-4.4.13.jar", "mirror_urls": [ - "https://packages.confluent.io/maven/org/apache/httpcomponents/httpcore/4.4.12/httpcore-4.4.12.jar", - "https://oss.sonatype.org/content/repositories/snapshots/org/apache/httpcomponents/httpcore/4.4.12/httpcore-4.4.12.jar", - "https://repo1.maven.org/maven2/org/apache/httpcomponents/httpcore/4.4.12/httpcore-4.4.12.jar", - "https://jitpack.io/org/apache/httpcomponents/httpcore/4.4.12/httpcore-4.4.12.jar" + "https://packages.confluent.io/maven/org/apache/httpcomponents/httpcore/4.4.13/httpcore-4.4.13.jar", + "https://oss.sonatype.org/content/repositories/snapshots/org/apache/httpcomponents/httpcore/4.4.13/httpcore-4.4.13.jar", + "https://repo1.maven.org/maven2/org/apache/httpcomponents/httpcore/4.4.13/httpcore-4.4.13.jar", + "https://jitpack.io/org/apache/httpcomponents/httpcore/4.4.13/httpcore-4.4.13.jar" ], - "sha256": "ab765334beabf0ea024484a5e90a7c40e8160b145f22d199e11e27f68d57da08", - "url": "https://repo1.maven.org/maven2/org/apache/httpcomponents/httpcore/4.4.12/httpcore-4.4.12.jar" + "sha256": "e06e89d40943245fcfa39ec537cdbfce3762aecde8f9c597780d2b00c2b43424", + "url": "https://repo1.maven.org/maven2/org/apache/httpcomponents/httpcore/4.4.13/httpcore-4.4.13.jar" }, { "coord": "org.apache.kafka:connect-api:2.5.1", @@ -5207,6 +5372,26 @@ "sha256": "a9aae9ff8ae3e17a2a18f79175e82b16267c246fbbd3ca9dfbbb290b08dcfdd4", "url": "https://repo1.maven.org/maven2/org/apiguardian/apiguardian-api/1.1.0/apiguardian-api-1.1.0.jar" }, + { + "coord": "org.aspectj:aspectjweaver:1.8.10", + "dependencies": [], + "directDependencies": [], + "exclusions": [ + "ch.qos.logback:logback-classic", + "org.springframework.boot:spring-boot-starter-tomcat", + "org.springframework.boot:spring-boot-starter-logging", + "org.slf4j:slf4j-log4j12" + ], + "file": "v1/https/repo1.maven.org/maven2/org/aspectj/aspectjweaver/1.8.10/aspectjweaver-1.8.10.jar", + "mirror_urls": [ + "https://packages.confluent.io/maven/org/aspectj/aspectjweaver/1.8.10/aspectjweaver-1.8.10.jar", + "https://oss.sonatype.org/content/repositories/snapshots/org/aspectj/aspectjweaver/1.8.10/aspectjweaver-1.8.10.jar", + "https://repo1.maven.org/maven2/org/aspectj/aspectjweaver/1.8.10/aspectjweaver-1.8.10.jar", + "https://jitpack.io/org/aspectj/aspectjweaver/1.8.10/aspectjweaver-1.8.10.jar" + ], + "sha256": "9687a76555ae2fc334ed6434343c62b17e04e1be86ca473149b6c9469405ecf7", + "url": "https://repo1.maven.org/maven2/org/aspectj/aspectjweaver/1.8.10/aspectjweaver-1.8.10.jar" + }, { "coord": "org.assertj:assertj-core:3.16.1", "dependencies": [], @@ -9100,6 +9285,31 @@ "sha256": "6c02c06ee4c9f989d48d30d3b17cf90fe07b19ed39596e2ede69b1c674acaa97", "url": "https://repo1.maven.org/maven2/org/springframework/data/spring-data-relational/2.0.1.RELEASE/spring-data-relational-2.0.1.RELEASE.jar" }, + { + "coord": "org.springframework.retry:spring-retry:1.2.5.RELEASE", + "dependencies": [ + "org.springframework:spring-jcl:5.2.8.RELEASE", + "org.springframework:spring-core:5.2.8.RELEASE" + ], + "directDependencies": [ + "org.springframework:spring-core:5.2.8.RELEASE" + ], + "exclusions": [ + "ch.qos.logback:logback-classic", + "org.springframework.boot:spring-boot-starter-tomcat", + "org.springframework.boot:spring-boot-starter-logging", + "org.slf4j:slf4j-log4j12" + ], + "file": "v1/https/repo1.maven.org/maven2/org/springframework/retry/spring-retry/1.2.5.RELEASE/spring-retry-1.2.5.RELEASE.jar", + "mirror_urls": [ + "https://packages.confluent.io/maven/org/springframework/retry/spring-retry/1.2.5.RELEASE/spring-retry-1.2.5.RELEASE.jar", + "https://oss.sonatype.org/content/repositories/snapshots/org/springframework/retry/spring-retry/1.2.5.RELEASE/spring-retry-1.2.5.RELEASE.jar", + "https://repo1.maven.org/maven2/org/springframework/retry/spring-retry/1.2.5.RELEASE/spring-retry-1.2.5.RELEASE.jar", + "https://jitpack.io/org/springframework/retry/spring-retry/1.2.5.RELEASE/spring-retry-1.2.5.RELEASE.jar" + ], + "sha256": "71e7cb0d33e3f595011d3e98b14f41ca165a435760ecd4d68cb935e8afa8a3d2", + "url": "https://repo1.maven.org/maven2/org/springframework/retry/spring-retry/1.2.5.RELEASE/spring-retry-1.2.5.RELEASE.jar" + }, { "coord": "org.springframework.security:spring-security-config:5.3.3.RELEASE", "dependencies": [ @@ -9716,6 +9926,26 @@ ], "sha256": "d87d607e500885356c03c1cae61e8c2e05d697df8787d5aba13484c2eb76a844", "url": "https://repo1.maven.org/maven2/org/yaml/snakeyaml/1.26/snakeyaml-1.26.jar" + }, + { + "coord": "software.amazon.ion:ion-java:1.0.2", + "dependencies": [], + "directDependencies": [], + "exclusions": [ + "ch.qos.logback:logback-classic", + "org.springframework.boot:spring-boot-starter-tomcat", + "org.springframework.boot:spring-boot-starter-logging", + "org.slf4j:slf4j-log4j12" + ], + "file": "v1/https/repo1.maven.org/maven2/software/amazon/ion/ion-java/1.0.2/ion-java-1.0.2.jar", + "mirror_urls": [ + "https://packages.confluent.io/maven/software/amazon/ion/ion-java/1.0.2/ion-java-1.0.2.jar", + "https://oss.sonatype.org/content/repositories/snapshots/software/amazon/ion/ion-java/1.0.2/ion-java-1.0.2.jar", + "https://repo1.maven.org/maven2/software/amazon/ion/ion-java/1.0.2/ion-java-1.0.2.jar", + "https://jitpack.io/software/amazon/ion/ion-java/1.0.2/ion-java-1.0.2.jar" + ], + "sha256": "0d127b205a1fce0abc2a3757a041748651bc66c15cf4c059bac5833b27d471a5", + "url": "https://repo1.maven.org/maven2/software/amazon/ion/ion-java/1.0.2/ion-java-1.0.2.jar" } ], "version": "0.1.0" From ba0d0cd9e4f38d1fe4ccd617936b5feaab5e42cb Mon Sep 17 00:00:00 2001 From: Kazeem Adetunji Date: Thu, 14 Jan 2021 16:33:08 +0100 Subject: [PATCH 07/56] [#501] Resume conversation in chat plugin (#603) * [#501] Resume conversation in chat plugin * Type declaration added * Run prettier * Add small edit * Type declaration name changed * Name of type declaration changed * Implemented review suggestions * Local storage and suggested changes added * Refactored and suggested changes added * Issues with local storage fixed * API calls moved to different file * Small improvement --- .../chat-plugin/src/components/api/index.tsx | 55 +++++++++++++++++++ .../chat-plugin/src/components/chat/index.tsx | 15 ++++- .../src/components/websocket/index.ts | 49 ++++++----------- 3 files changed, 84 insertions(+), 35 deletions(-) create mode 100644 frontend/chat-plugin/src/components/api/index.tsx diff --git a/frontend/chat-plugin/src/components/api/index.tsx b/frontend/chat-plugin/src/components/api/index.tsx new file mode 100644 index 0000000000..566a19daa4 --- /dev/null +++ b/frontend/chat-plugin/src/components/api/index.tsx @@ -0,0 +1,55 @@ +declare const window: { + airy: { + h: string; + cid: string; + no_tls: boolean; + }; +}; + +const API_HOST = window.airy ? window.airy.h : 'chatplugin.airy'; +const TLS_PREFIX = window.airy ? (window.airy.no_tls === true ? '' : 's') : ''; + +export const sendMessage = (message: string, token: string) => { + return fetch(`http${TLS_PREFIX}://${API_HOST}/chatplugin.send`, { + method: 'POST', + body: message, + headers: { + 'Content-Type': 'application/json', + Authorization: token, + }, + }); +}; + +export const getResumeToken = async (token: string) => { + const resumeChat = await fetch(`http${TLS_PREFIX}://${API_HOST}/chatplugin.resumeToken`, { + method: 'POST', + body: JSON.stringify({}), + headers: { + 'Content-Type': 'application/json', + Authorization: token, + }, + }); + const jsonResumeToken = await resumeChat.json(); + localStorage.setItem('resume_token', jsonResumeToken.resume_token); +}; + +export const start = async (channel_id: string, resume_token: string) => { + try { + const response = await fetch(`http${TLS_PREFIX}://${API_HOST}/chatplugin.authenticate`, { + method: 'POST', + body: JSON.stringify({ + channel_id: channel_id, + ...(resume_token && { + resume_token, + }), + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + + return await response.json(); + } catch (e) { + return Promise.reject(new Error('Widget authorization failed. Please check your installation.')); + } +}; diff --git a/frontend/chat-plugin/src/components/chat/index.tsx b/frontend/chat-plugin/src/components/chat/index.tsx index 8137d929c4..34ee9aaec1 100644 --- a/frontend/chat-plugin/src/components/chat/index.tsx +++ b/frontend/chat-plugin/src/components/chat/index.tsx @@ -2,7 +2,7 @@ import {h} from 'preact'; import {useState, useEffect} from 'preact/hooks'; import {IMessage} from '@stomp/stompjs'; -import Websocket from '../../components/websocket'; +import WebSocket from '../../components/websocket'; import MessageProp from '../../components/message'; import InputBarProp from '../../components/inputBar'; import AiryInputBar from '../../airyRenderProps/AiryInputBar'; @@ -16,7 +16,7 @@ import {RoutableProps} from 'preact-router'; import BubbleProp from '../bubble'; import AiryBubble from '../../airyRenderProps/AiryBubble'; -let ws: Websocket; +let ws: WebSocket; const welcomeMessage = { id: '19527d24-9b47-4e18-9f79-fd1998b95059', @@ -40,8 +40,17 @@ const Chat = (props: Props) => { const [isChatHidden, setIsChatHidden] = useState(true); const [messages, setMessages] = useState([welcomeMessage]); + const getResumeToken = () => { + const queryParams = new URLSearchParams(window.location.search); + if (queryParams.has('resume_token')) { + localStorage.setItem('resume_token', queryParams.get('resume_token')); + } + + return queryParams.get('resume_token') || localStorage.getItem('resume_token'); + }; + useEffect(() => { - ws = new Websocket(props.channel_id, onReceive); + ws = new WebSocket(props.channel_id, onReceive, getResumeToken()); ws.start().catch(error => { console.error(error); setInstallError(error.message); diff --git a/frontend/chat-plugin/src/components/websocket/index.ts b/frontend/chat-plugin/src/components/websocket/index.ts index 8c2b9991d1..a3e73a0cf4 100644 --- a/frontend/chat-plugin/src/components/websocket/index.ts +++ b/frontend/chat-plugin/src/components/websocket/index.ts @@ -1,5 +1,6 @@ import {Client, messageCallbackType, IFrame} from '@stomp/stompjs'; import 'regenerator-runtime/runtime'; +import {start, getResumeToken, sendMessage} from '../api'; declare const window: { airy: { @@ -12,15 +13,17 @@ declare const window: { const API_HOST = window.airy ? window.airy.h : 'chatplugin.airy'; const TLS_PREFIX = window.airy ? (window.airy.no_tls === true ? '' : 's') : ''; -class Websocket { +class WebSocket { client: Client; channel_id: string; token: string; + resume_token: string; onReceive: messageCallbackType; - constructor(channel_id: string, onReceive: messageCallbackType) { + constructor(channel_id: string, onReceive: messageCallbackType, resume_token?: string) { this.channel_id = channel_id; this.onReceive = onReceive; + this.resume_token = resume_token; } connect = (token: string) => { @@ -49,39 +52,21 @@ class Websocket { this.client.activate(); }; - onConnect = () => { - this.client.subscribe('/user/queue/message', this.onReceive); - }; - onSend = (message: string) => { - return fetch(`http${TLS_PREFIX}://${API_HOST}/chatplugin.send`, { - method: 'POST', - body: message, - headers: { - 'Content-Type': 'application/json', - Authorization: this.token, - }, - }); + sendMessage(message, this.token); }; - async start() { - try { - const response = await fetch(`http${TLS_PREFIX}://${API_HOST}/chatplugin.authenticate`, { - method: 'POST', - body: JSON.stringify({ - channel_id: this.channel_id, - }), - headers: { - 'Content-Type': 'application/json', - }, - }); - - const jsonResponse = await response.json(); - this.connect(jsonResponse.token); - } catch (e) { - return Promise.reject(new Error('Widget authorization failed. Please check your installation.')); + start = async () => { + this.token = (await start(this.channel_id, this.resume_token)).token; + this.connect(this.token); + if (!this.resume_token) { + await getResumeToken(this.token); } - } + }; + + onConnect = () => { + this.client.subscribe('/user/queue/message', this.onReceive); + }; } -export default Websocket; +export default WebSocket; From bb76fa7c339b02eecf4f48b7069d8ece055d75da Mon Sep 17 00:00:00 2001 From: lucapette Date: Thu, 14 Jan 2021 16:36:31 +0100 Subject: [PATCH 08/56] [#306] Introduce cli config (#649) Fixes #306 --- BUILD | 1 + infrastructure/cli/BUILD | 2 - infrastructure/cli/cmd/BUILD | 15 +- infrastructure/cli/cmd/auth/BUILD | 2 +- infrastructure/cli/cmd/auth/auth.go | 6 +- infrastructure/cli/cmd/bootstrap/BUILD | 12 - infrastructure/cli/cmd/bootstrap/bootstrap.go | 31 -- infrastructure/cli/cmd/config/BUILD | 1 - infrastructure/cli/cmd/config/config.go | 7 - infrastructure/cli/cmd/root.go | 103 ++++++- infrastructure/cli/cmd/{demo => ui}/BUILD | 7 +- .../cli/cmd/{demo/demo.go => ui/ui.go} | 17 +- infrastructure/cli/cmd/version.go | 26 -- infrastructure/cli/go.mod | 11 +- infrastructure/cli/go.sum | 287 +++++++++++++++++- infrastructure/cli/main.go | 1 - infrastructure/cli/main_test.go | 10 +- infrastructure/cli/pkg/tests/BUILD | 1 - infrastructure/cli/pkg/tests/golden/BUILD | 1 + .../cli/pkg/tests/golden/airycli.yaml | 1 + .../cli/pkg/tests/golden/cli.no-args.golden | 12 +- infrastructure/cli/pkg/tests/mockserver.go | 1 - infrastructure/controller/BUILD | 5 +- .../controller/pkg/configmap-controller/BUILD | 2 - infrastructure/lib/go/k8s/handler/BUILD | 3 +- infrastructure/lib/go/k8s/util/BUILD | 3 +- lib/go/httpclient/BUILD | 4 +- lib/go/httpclient/payloads/BUILD | 1 + 28 files changed, 418 insertions(+), 155 deletions(-) delete mode 100644 infrastructure/cli/cmd/bootstrap/BUILD delete mode 100644 infrastructure/cli/cmd/bootstrap/bootstrap.go rename infrastructure/cli/cmd/{demo => ui}/BUILD (57%) rename infrastructure/cli/cmd/{demo/demo.go => ui/ui.go} (68%) delete mode 100644 infrastructure/cli/cmd/version.go create mode 100644 infrastructure/cli/pkg/tests/golden/airycli.yaml diff --git a/BUILD b/BUILD index 097d329b75..10e30e5293 100644 --- a/BUILD +++ b/BUILD @@ -168,6 +168,7 @@ exports_files( ], ) +# gazelle:proto disable_global # gazelle:build_file_name BUILD # gazelle:prefix gazelle(name = "gazelle") diff --git a/infrastructure/cli/BUILD b/infrastructure/cli/BUILD index ee37f6ecec..63bf621f7e 100644 --- a/infrastructure/cli/BUILD +++ b/infrastructure/cli/BUILD @@ -1,11 +1,9 @@ # gazelle:prefix cli -# gazelle:importmap_prefix infrastructure load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") go_library( name = "cli_lib", srcs = ["main.go"], - importmap = "infrastructure", importpath = "cli", visibility = ["//visibility:private"], deps = ["//infrastructure/cli/cmd"], diff --git a/infrastructure/cli/cmd/BUILD b/infrastructure/cli/cmd/BUILD index 77f0424c35..f506ed40eb 100644 --- a/infrastructure/cli/cmd/BUILD +++ b/infrastructure/cli/cmd/BUILD @@ -2,22 +2,19 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "cmd", - srcs = [ - "root.go", - "version.go", - ], - importmap = "infrastructure/cmd", + srcs = ["root.go"], importpath = "cli/cmd", visibility = ["//visibility:public"], x_defs = { - "CLIVersion": "{STABLE_VERSION}", - "GitCommit": "{STABLE_GIT_COMMIT}", + "Version": "{STABLE_VERSION}", + "CommitSHA1": "{STABLE_GIT_COMMIT}", }, deps = [ "//infrastructure/cli/cmd/auth", - "//infrastructure/cli/cmd/bootstrap", "//infrastructure/cli/cmd/config", - "//infrastructure/cli/cmd/demo", + "//infrastructure/cli/cmd/ui", + "@com_github_mitchellh_go_homedir//:go-homedir", "@com_github_spf13_cobra//:cobra", + "@com_github_spf13_viper//:viper", ], ) diff --git a/infrastructure/cli/cmd/auth/BUILD b/infrastructure/cli/cmd/auth/BUILD index cc679d53f4..32624f8ea0 100644 --- a/infrastructure/cli/cmd/auth/BUILD +++ b/infrastructure/cli/cmd/auth/BUILD @@ -3,12 +3,12 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "auth", srcs = ["auth.go"], - importmap = "infrastructure/cmd/auth", importpath = "cli/cmd/auth", visibility = ["//visibility:public"], deps = [ "//lib/go/httpclient", "//lib/go/httpclient/payloads", "@com_github_spf13_cobra//:cobra", + "@com_github_spf13_viper//:viper", ], ) diff --git a/infrastructure/cli/cmd/auth/auth.go b/infrastructure/cli/cmd/auth/auth.go index 629bad9d39..34f47db2a7 100644 --- a/infrastructure/cli/cmd/auth/auth.go +++ b/infrastructure/cli/cmd/auth/auth.go @@ -8,6 +8,7 @@ import ( "github.com/airyhq/airy/lib/go/httpclient/payloads" "github.com/spf13/cobra" + "github.com/spf13/viper" ) // AuthCmd subcommand for Airy Core @@ -20,7 +21,7 @@ var AuthCmd = &cobra.Command{ } func auth(cmd *cobra.Command, args []string) { - url, _ := cmd.Flags().GetString("url") + url := viper.GetString("apihost") email, _ := cmd.Flags().GetString("email") password, _ := cmd.Flags().GetString("password") c := httpclient.NewClient() @@ -42,8 +43,7 @@ func auth(cmd *cobra.Command, args []string) { } func init() { - var url, email, password string - AuthCmd.Flags().StringVarP(&url, "url", "u", "http://api.airy", "The url of the Airy API") + var email, password string AuthCmd.Flags().StringVarP(&email, "email", "e", "grace@hopper.com", "Email to use for the authentication") AuthCmd.Flags().StringVarP(&password, "password", "p", "the_answer_is_42", "Password to use for the authentication") } diff --git a/infrastructure/cli/cmd/bootstrap/BUILD b/infrastructure/cli/cmd/bootstrap/BUILD deleted file mode 100644 index d016d3af30..0000000000 --- a/infrastructure/cli/cmd/bootstrap/BUILD +++ /dev/null @@ -1,12 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") - -go_library( - name = "bootstrap", - srcs = ["bootstrap.go"], - importmap = "infrastructure/cmd/bootstrap", - importpath = "cli/cmd/bootstrap", - visibility = ["//visibility:public"], - deps = [ - "@com_github_spf13_cobra//:cobra", - ], -) diff --git a/infrastructure/cli/cmd/bootstrap/bootstrap.go b/infrastructure/cli/cmd/bootstrap/bootstrap.go deleted file mode 100644 index 66301513d6..0000000000 --- a/infrastructure/cli/cmd/bootstrap/bootstrap.go +++ /dev/null @@ -1,31 +0,0 @@ -package bootstrap - -import ( - "log" - - "github.com/spf13/cobra" -) - -// ResponsePayload for receiving the request - -// BootstrapCmd subcommand for Airy Core -var BootstrapCmd = &cobra.Command{ - Use: "bootstrap", - TraverseChildren: true, - Short: "Bootstrap Airy Core Platform locally", - Long: `This will install the Airy Core Platform in the current directory unless you choose a different one. - It will also try to install Vagrant and VirtualBox.`, - Run: bootstrap, -} - -func bootstrap(cmd *cobra.Command, args []string) { - // Initialize the api request - - log.Println("BootstrapCmd called") - -} - -func init() { - var imageTag string - BootstrapCmd.Flags().StringVarP(&imageTag, "image-tag", "i", "", "The docker image tag that the Airy apps will use.") -} diff --git a/infrastructure/cli/cmd/config/BUILD b/infrastructure/cli/cmd/config/BUILD index 102a335cff..caccc11385 100644 --- a/infrastructure/cli/cmd/config/BUILD +++ b/infrastructure/cli/cmd/config/BUILD @@ -3,7 +3,6 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "config", srcs = ["config.go"], - importmap = "infrastructure/cmd/config", importpath = "cli/cmd/config", visibility = ["//visibility:public"], deps = ["@com_github_spf13_cobra//:cobra"], diff --git a/infrastructure/cli/cmd/config/config.go b/infrastructure/cli/cmd/config/config.go index 68fa0c78f6..eba053c773 100644 --- a/infrastructure/cli/cmd/config/config.go +++ b/infrastructure/cli/cmd/config/config.go @@ -6,8 +6,6 @@ import ( "github.com/spf13/cobra" ) -// ResponsePayload for receiving the request - // ConfigCmd subcommand for Airy Core var ConfigCmd = &cobra.Command{ Use: "config", @@ -18,11 +16,6 @@ var ConfigCmd = &cobra.Command{ } func config(cmd *cobra.Command, args []string) { - // Initialize the api request - fmt.Println("ConfigCmd called") } - -func init() { -} diff --git a/infrastructure/cli/cmd/root.go b/infrastructure/cli/cmd/root.go index d7268ec9fe..86c1688321 100644 --- a/infrastructure/cli/cmd/root.go +++ b/infrastructure/cli/cmd/root.go @@ -3,37 +3,114 @@ package cmd import ( "fmt" "os" + "path" "cli/cmd/auth" - "cli/cmd/bootstrap" "cli/cmd/config" - "cli/cmd/demo" + "cli/cmd/ui" + homedir "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" + "github.com/spf13/viper" ) -// rootCmd represents the base command when called without any subcommands -var RootCmd = &cobra.Command{ +const configFileName = ".airycli" + +var configFile string +var Version string +var CommitSHA1 string + +// RootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ Use: "airy", - Short: "Airy CLI", + Short: "airy controls your Airy Core Platform instance", Long: ``, TraverseChildren: true, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + if cmd.Name() != "init" { + initConfig() + } + }, } -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { +// Version command +var versionCmd = &cobra.Command{ + Use: "version", + Short: "Prints version information", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("Version: %s, GitCommit: %s", Version, CommitSHA1) + }, +} + +// Version command +var initCmd = &cobra.Command{ + Use: "init", + Short: "Inits your Airy CLI configuration", + Long: ``, + Run: func(cmd *cobra.Command, args []string) { + home, err := homedir.Dir() + if err != nil { + fmt.Println(err) + os.Exit(1) + } - if err := RootCmd.Execute(); err != nil { + //TODO let users choose a different name + viper.AddConfigPath(home) + viper.SetConfigName(configFileName) + + viper.WriteConfigAs(path.Join(home, configFileName)) + }, +} + +// Execute adds all child commands to the root command and sets flags +// appropriately. This is called by main.main(). It only needs to happen once to +// the rootCmd. +func Execute() { + if err := rootCmd.Execute(); err != nil { fmt.Println(err) os.Exit(1) } +} + +func initConfig() { + if configFile != "" { + viper.SetConfigFile(configFile) + } else { + home, err := homedir.Dir() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + viper.AddConfigPath(home) + viper.SetConfigName(configFileName) + } + + viper.AutomaticEnv() + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + fmt.Println(err) + fmt.Println("please run airy init") + } else { + fmt.Println("invalid configuration: ", err) + } + + os.Exit(1) + } } func init() { - RootCmd.AddCommand(bootstrap.BootstrapCmd) - RootCmd.AddCommand(auth.AuthCmd) - RootCmd.AddCommand(config.ConfigCmd) - RootCmd.AddCommand(demo.DemoCmd) + apiHost := "" + rootCmd.PersistentFlags().StringVar(&apiHost, "apihost", "http://api.airy", "Airy Core Platform HTTP API host") + viper.BindPFlag("apihost", rootCmd.PersistentFlags().Lookup("apihost")) + viper.SetDefault("apihost", "http://api.airy") + + rootCmd.PersistentFlags().StringVar(&configFile, "config", "", "config file (default is $HOME/.airycli.yaml)") + rootCmd.AddCommand(auth.AuthCmd) + rootCmd.AddCommand(config.ConfigCmd) + rootCmd.AddCommand(ui.UICmd) + rootCmd.AddCommand(versionCmd) + rootCmd.AddCommand(initCmd) } diff --git a/infrastructure/cli/cmd/demo/BUILD b/infrastructure/cli/cmd/ui/BUILD similarity index 57% rename from infrastructure/cli/cmd/demo/BUILD rename to infrastructure/cli/cmd/ui/BUILD index 7bf9222d39..52a704a950 100644 --- a/infrastructure/cli/cmd/demo/BUILD +++ b/infrastructure/cli/cmd/ui/BUILD @@ -1,10 +1,9 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( - name = "demo", - srcs = ["demo.go"], - importmap = "infrastructure/cmd/demo", - importpath = "cli/cmd/demo", + name = "ui", + srcs = ["ui.go"], + importpath = "cli/cmd/ui", visibility = ["//visibility:public"], deps = ["@com_github_spf13_cobra//:cobra"], ) diff --git a/infrastructure/cli/cmd/demo/demo.go b/infrastructure/cli/cmd/ui/ui.go similarity index 68% rename from infrastructure/cli/cmd/demo/demo.go rename to infrastructure/cli/cmd/ui/ui.go index 7bb8e0132c..25c7e91f35 100644 --- a/infrastructure/cli/cmd/demo/demo.go +++ b/infrastructure/cli/cmd/ui/ui.go @@ -1,4 +1,4 @@ -package demo +package ui import ( "fmt" @@ -9,13 +9,11 @@ import ( "github.com/spf13/cobra" ) -// ResponsePayload for receiving the request - -// DemoCmd subcommand for Airy Core -var DemoCmd = &cobra.Command{ - Use: "demo", +// UICmd opens the Airy Core Platform UI +var UICmd = &cobra.Command{ + Use: "ui", TraverseChildren: true, - Short: "Opens the demo page in the browser", + Short: "Opens the Airy Core Platform UI in your local browser", Long: ``, Run: demo, } @@ -23,7 +21,7 @@ var DemoCmd = &cobra.Command{ func demo(cmd *cobra.Command, args []string) { // Initialize the api request - url := "http://chatplugin.airy/example.html" + url := "http://ui.airy/" var err error @@ -42,6 +40,3 @@ func demo(cmd *cobra.Command, args []string) { } } - -func init() { -} diff --git a/infrastructure/cli/cmd/version.go b/infrastructure/cli/cmd/version.go deleted file mode 100644 index 5a5eeefb49..0000000000 --- a/infrastructure/cli/cmd/version.go +++ /dev/null @@ -1,26 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/spf13/cobra" -) - -var CLIVersion string -var GitCommit string - -// StatusCmd cli kafka version -var versionCmd = &cobra.Command{ - Use: "version", - Short: "Return current version", - Long: ``, - Run: version, -} - -func version(cmd *cobra.Command, args []string) { - fmt.Printf("Version: %s, GitCommit: %s", CLIVersion, GitCommit) -} - -func init() { - RootCmd.AddCommand(versionCmd) -} diff --git a/infrastructure/cli/go.mod b/infrastructure/cli/go.mod index d113097457..7d7459829f 100644 --- a/infrastructure/cli/go.mod +++ b/infrastructure/cli/go.mod @@ -3,10 +3,17 @@ module cli go 1.12 require ( + github.com/BurntSushi/toml v0.3.1 // indirect github.com/airyhq/airy/lib/go/httpclient v0.0.0 + github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 // indirect + github.com/coreos/go-etcd v2.0.0+incompatible // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/kr/pretty v0.2.1 - github.com/spf13/cobra v0.0.3 - github.com/spf13/viper v1.3.1 + github.com/mitchellh/go-homedir v1.1.0 + github.com/spf13/cobra v1.1.1 + github.com/spf13/viper v1.7.1 + github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 // indirect + github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 // indirect goji.io v2.0.2+incompatible ) diff --git a/infrastructure/cli/go.sum b/infrastructure/cli/go.sum index a53a9ff9a8..3ad81e4526 100644 --- a/infrastructure/cli/go.sum +++ b/infrastructure/cli/go.sum @@ -1,18 +1,113 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/aws/aws-sdk-go v1.28.3 h1:FnkDp+fz4JHWUW3Ust2Wh89RpdGif077Wjis/sMrGKM= -github.com/aws/aws-sdk-go v1.28.3/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -20,44 +115,222 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.1 h1:5+8j8FTpnFV4nEImW/ofkzEt8VoOiLXxdYIDsB73T38= github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tcnksm/go-input v0.0.0-20180404061846-548a7d7a8ee8 h1:RB0v+/pc8oMzPsN97aZYEwNuJ6ouRJ2uhjxemJ9zvrY= -github.com/tcnksm/go-input v0.0.0-20180404061846-548a7d7a8ee8/go.mod h1:IlWNj9v/13q7xFbaK4mbyzMNwrZLaWSHx/aibKIZuIg= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= goji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c= goji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +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-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +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/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 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 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/infrastructure/cli/main.go b/infrastructure/cli/main.go index abf40a3da2..6b3302a5ac 100644 --- a/infrastructure/cli/main.go +++ b/infrastructure/cli/main.go @@ -5,6 +5,5 @@ import ( ) func main() { - cmd.Execute() } diff --git a/infrastructure/cli/main_test.go b/infrastructure/cli/main_test.go index 7a82256b7a..02690ef28c 100644 --- a/infrastructure/cli/main_test.go +++ b/infrastructure/cli/main_test.go @@ -19,12 +19,10 @@ func TestCli(t *testing.T) { wantErr bool }{ {"no args", []string{}, "cli.no-args.golden", false}, - {"auth", []string{"auth", "--url", "http://localhost:3001"}, "cli.auth.golden", false}, - {"auth", []string{"auth", "--url", "http://localhost:3001", "--email", "example@email.com"}, "cli.auth.golden", false}, - {"auth", []string{"auth", "--url", "http://localhost:3001", "--email", "example@email.com", "--password", "examplepassword"}, "cli.auth.golden", false}, - // {"bootstrap", []string{"bootstrap"}, "cli.bootstrap.golden", false}, - // {"config", []string{"config"}, "cli.config.no-args.golden", true}, - {"version", []string{"version"}, "cli.version.golden", false}, + {"auth", []string{"auth", "--config", "pkg/tests/golden/airycli.yaml"}, "cli.auth.golden", false}, + {"auth", []string{"auth", "--config", "pkg/tests/golden/airycli.yaml", "--email", "grace@example.com"}, "cli.auth.golden", false}, + {"auth", []string{"auth", "--config", "pkg/tests/golden/airycli.yaml", "--email", "grace@example.com", "--password", "examplepassword"}, "cli.auth.golden", false}, + {"version", []string{"version", "--config", "pkg/tests/golden/airycli.yaml"}, "cli.version.golden", false}, } go func() { diff --git a/infrastructure/cli/pkg/tests/BUILD b/infrastructure/cli/pkg/tests/BUILD index 21f5fede37..98517f41f0 100644 --- a/infrastructure/cli/pkg/tests/BUILD +++ b/infrastructure/cli/pkg/tests/BUILD @@ -8,7 +8,6 @@ go_library( "golden.go", "mockserver.go", ], - importmap = "infrastructure/pkg/tests", importpath = "cli/pkg/tests", deps = [ "@com_github_kr_pretty//:pretty", diff --git a/infrastructure/cli/pkg/tests/golden/BUILD b/infrastructure/cli/pkg/tests/golden/BUILD index 61fb98b9de..895c692957 100644 --- a/infrastructure/cli/pkg/tests/golden/BUILD +++ b/infrastructure/cli/pkg/tests/golden/BUILD @@ -3,6 +3,7 @@ package(default_visibility = ["//visibility:public"]) filegroup( name = "golden_files", srcs = [ + "airycli.yaml", "api.signup.golden", "cli.auth.golden", "cli.no-args.golden", diff --git a/infrastructure/cli/pkg/tests/golden/airycli.yaml b/infrastructure/cli/pkg/tests/golden/airycli.yaml new file mode 100644 index 0000000000..8e86406679 --- /dev/null +++ b/infrastructure/cli/pkg/tests/golden/airycli.yaml @@ -0,0 +1 @@ +apihost: http://localhost:3001 diff --git a/infrastructure/cli/pkg/tests/golden/cli.no-args.golden b/infrastructure/cli/pkg/tests/golden/cli.no-args.golden index 52769b5c49..6a3bee7a32 100644 --- a/infrastructure/cli/pkg/tests/golden/cli.no-args.golden +++ b/infrastructure/cli/pkg/tests/golden/cli.no-args.golden @@ -1,17 +1,19 @@ -Airy CLI +airy controls your Airy Core Platform instance Usage: airy [command] Available Commands: auth Create a default user and return a JWT token - bootstrap Bootstrap Airy Core Platform locally config Reloads configuration based on airy.conf - demo Opens the demo page in the browser help Help about any command - version Return current version + init Inits your Airy CLI configuration + ui Opens the Airy Core Platform UI in your local browser + version Prints version information Flags: - -h, --help help for airy + --apihost string Airy Core Platform HTTP API host (default "http://api.airy") + --config string config file (default is $HOME/.airycli.yaml) + -h, --help help for airy Use "airy [command] --help" for more information about a command. diff --git a/infrastructure/cli/pkg/tests/mockserver.go b/infrastructure/cli/pkg/tests/mockserver.go index 65c1783d88..1103a5f99e 100644 --- a/infrastructure/cli/pkg/tests/mockserver.go +++ b/infrastructure/cli/pkg/tests/mockserver.go @@ -17,7 +17,6 @@ func MockServer() { mux.HandleFunc(pat.Post("/users.signup"), mockUserSignupHandler) mux.HandleFunc(pat.Post("/users.login"), mockUserLoginHandler) - log.Println("starting mock server on port localhost:3001") s := &http.Server{ Addr: ":3001", Handler: mux, diff --git a/infrastructure/controller/BUILD b/infrastructure/controller/BUILD index 8b68346131..a10567ee0d 100644 --- a/infrastructure/controller/BUILD +++ b/infrastructure/controller/BUILD @@ -1,5 +1,4 @@ -# gazelle:prefix controller -# gazelle:prefix k8s.io/kubernetes +# gazelle:prefix github.com/airyhq/airy/infrastructure/controller load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") load("@io_bazel_rules_docker//go:image.bzl", "go_image") load("//tools/build:container_push.bzl", "container_push") @@ -11,8 +10,6 @@ go_library( visibility = ["//visibility:private"], deps = [ "//infrastructure/controller/pkg/configmap-controller", - "//infrastructure/lib/go/k8s/handler", - "//infrastructure/lib/go/k8s/util", "@io_k8s_client_go//kubernetes", "@io_k8s_client_go//tools/clientcmd", "@io_k8s_klog//:klog", diff --git a/infrastructure/controller/pkg/configmap-controller/BUILD b/infrastructure/controller/pkg/configmap-controller/BUILD index ef6399fb37..91019d74ed 100644 --- a/infrastructure/controller/pkg/configmap-controller/BUILD +++ b/infrastructure/controller/pkg/configmap-controller/BUILD @@ -1,5 +1,3 @@ -# gazelle:prefix configmap-controller -# gazelle:prefix k8s.io/kubernetes load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( diff --git a/infrastructure/lib/go/k8s/handler/BUILD b/infrastructure/lib/go/k8s/handler/BUILD index 14f813ff08..ba971cf2d2 100644 --- a/infrastructure/lib/go/k8s/handler/BUILD +++ b/infrastructure/lib/go/k8s/handler/BUILD @@ -1,5 +1,4 @@ -# gazelle:prefix handler -# gazelle:prefix k8s.io/kubernetes +# gazelle:prefix github.com/airyhq/airy/infrastructure/lib/go/k8s/handler load("@io_bazel_rules_go//go:def.bzl", "go_library") diff --git a/infrastructure/lib/go/k8s/util/BUILD b/infrastructure/lib/go/k8s/util/BUILD index c6387d371e..f7106742fc 100644 --- a/infrastructure/lib/go/k8s/util/BUILD +++ b/infrastructure/lib/go/k8s/util/BUILD @@ -1,5 +1,4 @@ -# gazelle:prefix util -# gazelle:prefix k8s.io/kubernetes +# gazelle:prefix github.com/airyhq/airy/infrastructure/lib/go/k8s/util load("@io_bazel_rules_go//go:def.bzl", "go_library") diff --git a/lib/go/httpclient/BUILD b/lib/go/httpclient/BUILD index 01b9934a9b..dae39ebd2f 100644 --- a/lib/go/httpclient/BUILD +++ b/lib/go/httpclient/BUILD @@ -1,7 +1,7 @@ -# gazelle:prefix httpclient -# gazelle:importmap_prefix lib/go load("@io_bazel_rules_go//go:def.bzl", "go_library") +# gazelle:prefix github.com/airyhq/airy/lib/go/httpclient + go_library( name = "httpclient", srcs = [ diff --git a/lib/go/httpclient/payloads/BUILD b/lib/go/httpclient/payloads/BUILD index e313270567..a1c95042f5 100644 --- a/lib/go/httpclient/payloads/BUILD +++ b/lib/go/httpclient/payloads/BUILD @@ -1,4 +1,5 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") +# gazelle:prefix github.com/airyhq/airy/lib/go/httpclient/payloads go_library( name = "payloads", From ed50b3d306a99ec26491c05e106ece5b00bbce4f Mon Sep 17 00:00:00 2001 From: AudreyKj <38159391+AudreyKj@users.noreply.github.com> Date: Thu, 14 Jan 2021 17:56:12 +0100 Subject: [PATCH 09/56] add mappers to ts http client lib (#657) * added mappers to ts http lib * refactored mappers --- .../httpclient/endpoints/connectChannel.ts | 25 ++----------- .../httpclient/endpoints/disconnectChannel.ts | 22 ++--------- .../httpclient/endpoints/exploreChannels.ts | 13 +------ .../httpclient/endpoints/listChannels.ts | 13 +------ .../httpclient/endpoints/listConversations.ts | 37 +------------------ .../httpclient/endpoints/listMessages.ts | 15 +------- .../httpclient/endpoints/listTags.ts | 13 +------ .../httpclient/endpoints/loginViaEmail.ts | 12 +----- .../httpclient/mappers/channelMapper.ts | 10 +++++ .../httpclient/mappers/channelsMapper.ts | 11 ++++++ .../mappers/connectChannelApiMapper.ts | 9 +++++ .../httpclient/mappers/conversationMapper.ts | 19 ++++++++++ .../httpclient/mappers/conversationsMapper.ts | 7 ++++ .../mappers/disconnectChannelApiMapper.ts | 7 ++++ .../httpclient/mappers/messageMapper.ts | 10 +++++ .../httpclient/mappers/messageMapperData.ts | 7 ++++ .../httpclient/mappers/tagsMapper.ts | 12 ++++++ .../httpclient/mappers/userMapper.ts | 10 +++++ 18 files changed, 114 insertions(+), 138 deletions(-) create mode 100644 lib/typescript/httpclient/mappers/channelMapper.ts create mode 100644 lib/typescript/httpclient/mappers/channelsMapper.ts create mode 100644 lib/typescript/httpclient/mappers/connectChannelApiMapper.ts create mode 100644 lib/typescript/httpclient/mappers/conversationMapper.ts create mode 100644 lib/typescript/httpclient/mappers/conversationsMapper.ts create mode 100644 lib/typescript/httpclient/mappers/disconnectChannelApiMapper.ts create mode 100644 lib/typescript/httpclient/mappers/messageMapper.ts create mode 100644 lib/typescript/httpclient/mappers/messageMapperData.ts create mode 100644 lib/typescript/httpclient/mappers/tagsMapper.ts create mode 100644 lib/typescript/httpclient/mappers/userMapper.ts diff --git a/lib/typescript/httpclient/endpoints/connectChannel.ts b/lib/typescript/httpclient/endpoints/connectChannel.ts index 768f182985..abf59ac2b2 100644 --- a/lib/typescript/httpclient/endpoints/connectChannel.ts +++ b/lib/typescript/httpclient/endpoints/connectChannel.ts @@ -1,27 +1,8 @@ import {doFetchFromBackend} from '../api'; -import {ConnectChannelRequestPayload, ConnectChannelRequestApiPayload} from '../payload'; -import {Channel} from '../model'; +import {ConnectChannelRequestPayload} from '../payload'; import {ChannelApiPayload} from '../payload/ChannelApiPayload'; - -const connectChannelApiMapper = (payload: ConnectChannelRequestPayload): ConnectChannelRequestApiPayload => { - return { - source: payload.source, - source_channel_id: payload.sourceChannelId, - token: payload.token, - name: payload.name, - image_url: payload.imageUrl, - }; -}; - -const channelMapper = (payload: ChannelApiPayload): Channel => { - return { - name: payload.name, - source: payload.source, - sourceChannelId: payload.source_channel_id, - imageUrl: payload.image_url, - connected: true, - }; -}; +import {connectChannelApiMapper} from '../mappers/connectChannelApiMapper'; +import {channelMapper} from '../mappers/channelMapper'; export function connectChannel(requestPayload: ConnectChannelRequestPayload) { return doFetchFromBackend('channels.connect', connectChannelApiMapper(requestPayload)) diff --git a/lib/typescript/httpclient/endpoints/disconnectChannel.ts b/lib/typescript/httpclient/endpoints/disconnectChannel.ts index c63d5ece66..95403312be 100644 --- a/lib/typescript/httpclient/endpoints/disconnectChannel.ts +++ b/lib/typescript/httpclient/endpoints/disconnectChannel.ts @@ -1,24 +1,8 @@ import {doFetchFromBackend} from '../api'; -import {DisconnectChannelRequestPayload, DisconnectChannelRequestApiPayload} from '../payload'; -import {Channel} from '../model'; +import {DisconnectChannelRequestPayload} from '../payload'; import {ChannelsPayload} from '../payload/ChannelsPayload'; - -const channelsMapper = (payload: ChannelsPayload, source?: string): Channel[] => { - return payload.data.map( - (entry: Channel): Channel => { - return { - source, - ...entry, - }; - } - ); -}; - -const disconnectChannelApiMapper = (payload: DisconnectChannelRequestPayload): DisconnectChannelRequestApiPayload => { - return { - channel_id: payload.channelId, - }; -}; +import {channelsMapper} from '../mappers/channelsMapper'; +import {disconnectChannelApiMapper} from '../mappers/disconnectChannelApiMapper'; export function disconnectChannel(requestPayload: DisconnectChannelRequestPayload) { return doFetchFromBackend('channels.disconnect', disconnectChannelApiMapper(requestPayload)) diff --git a/lib/typescript/httpclient/endpoints/exploreChannels.ts b/lib/typescript/httpclient/endpoints/exploreChannels.ts index 6de5235e61..e111e5686c 100644 --- a/lib/typescript/httpclient/endpoints/exploreChannels.ts +++ b/lib/typescript/httpclient/endpoints/exploreChannels.ts @@ -1,18 +1,7 @@ import {doFetchFromBackend} from '../api'; import {ExploreChannelRequestPayload} from '../payload'; -import {Channel} from '../model'; import {ChannelsPayload} from '../payload/ChannelsPayload'; - -const channelsMapper = (payload: ChannelsPayload, source?: string): Channel[] => { - return payload.data.map( - (entry: Channel): Channel => { - return { - source, - ...entry, - }; - } - ); -}; +import {channelsMapper} from '../mappers/channelsMapper'; export function exploreChannels(requestPayload: ExploreChannelRequestPayload) { return doFetchFromBackend('channels.explore', requestPayload) diff --git a/lib/typescript/httpclient/endpoints/listChannels.ts b/lib/typescript/httpclient/endpoints/listChannels.ts index bc1e47642e..90f3a0563a 100644 --- a/lib/typescript/httpclient/endpoints/listChannels.ts +++ b/lib/typescript/httpclient/endpoints/listChannels.ts @@ -1,17 +1,6 @@ import {doFetchFromBackend} from '../api'; -import {Channel} from '../model'; import {ChannelsPayload} from '../payload/ChannelsPayload'; - -const channelsMapper = (payload: ChannelsPayload, source?: string): Channel[] => { - return payload.data.map( - (entry: Channel): Channel => { - return { - source, - ...entry, - }; - } - ); -}; +import {channelsMapper} from '../mappers/channelsMapper'; export function listChannels() { return doFetchFromBackend('channels.list') diff --git a/lib/typescript/httpclient/endpoints/listConversations.ts b/lib/typescript/httpclient/endpoints/listConversations.ts index 52a1b1a194..ca829e6708 100644 --- a/lib/typescript/httpclient/endpoints/listConversations.ts +++ b/lib/typescript/httpclient/endpoints/listConversations.ts @@ -1,43 +1,8 @@ import {doFetchFromBackend} from '../api'; import {ListConversationsRequestPayload} from '../payload'; -import {Conversation, Message} from '../model'; import {ConversationPayload} from '../payload/ConversationPayload'; -import {MessagePayload} from '../payload/MessagePayload'; import {PaginatedPayload} from '../payload/PaginatedPayload'; - -const messageMapper = (payload: MessagePayload): Message => { - const message: Message = { - id: payload.id, - content: payload.content, - deliveryState: payload.delivery_state, - senderType: payload.sender_type, - sentAt: new Date(payload.sent_at), - }; - return message; -}; - -const conversationMapper = (payload: ConversationPayload): Conversation => { - const conversation: Conversation = { - id: payload.id, - channel: payload.channel, - createdAt: payload.created_at, - contact: { - avatarUrl: payload.contact.avatar_url, - firstName: payload.contact.first_name, - lastName: payload.contact.last_name, - displayName: payload.contact.first_name + ' ' + payload.contact.last_name, - id: payload.contact.id, - }, - tags: payload.tags, - lastMessage: messageMapper(payload.last_message), - unreadMessageCount: payload.unread_message_count, - }; - return conversation; -}; - -const conversationsMapper = (payloadArray: ConversationPayload[]): Conversation[] => { - return (payloadArray || []).map(conversation => conversationMapper(conversation)); -}; +import {conversationsMapper} from '../mappers/conversationsMapper'; export function listConversations(conversationListRequest: ListConversationsRequestPayload) { conversationListRequest.page_size = conversationListRequest.page_size ?? 10; diff --git a/lib/typescript/httpclient/endpoints/listMessages.ts b/lib/typescript/httpclient/endpoints/listMessages.ts index cf1e4bc0ef..162eafc809 100644 --- a/lib/typescript/httpclient/endpoints/listMessages.ts +++ b/lib/typescript/httpclient/endpoints/listMessages.ts @@ -1,8 +1,8 @@ import {doFetchFromBackend} from '../api'; -import {Message, MessagePayloadData} from '../model'; import {ListMessagesRequestPayload} from '../payload/ListMessagesRequestPayload'; import {MessagePayload} from '../payload/MessagePayload'; import {PaginatedPayload} from '../payload/PaginatedPayload'; +import {messageMapperData} from '../mappers/messageMapperData'; export function listMessages(conversationListRequest: ListMessagesRequestPayload) { conversationListRequest.pageSize = conversationListRequest.pageSize ?? 10; @@ -21,16 +21,3 @@ export function listMessages(conversationListRequest: ListMessagesRequestPayload return error; }); } - -const messageMapperData = (payload: MessagePayloadData): Message[] => { - return payload.data.map((messagePayload: MessagePayload) => { - const message: Message = { - id: messagePayload.id, - content: messagePayload.content, - deliveryState: messagePayload.delivery_state, - senderType: messagePayload.sender_type, - sentAt: new Date(messagePayload.sent_at), - }; - return message; - }); -}; diff --git a/lib/typescript/httpclient/endpoints/listTags.ts b/lib/typescript/httpclient/endpoints/listTags.ts index 27fe74381b..1178a49e9f 100644 --- a/lib/typescript/httpclient/endpoints/listTags.ts +++ b/lib/typescript/httpclient/endpoints/listTags.ts @@ -1,17 +1,6 @@ import {doFetchFromBackend} from '../api'; -import {Tag} from '../model'; import {ListTagsResponsePayload} from '../payload'; - -const tagMapper = { - BLUE: 'tag-blue', - RED: 'tag-red', - GREEN: 'tag-green', - PURPLE: 'tag-purple', -}; - -const tagsMapper = (serverTags: Tag[]): Tag[] => { - return serverTags.map(t => ({id: t.id, name: t.name, color: tagMapper[t.color] || 'tag-blue'})); -}; +import {tagsMapper} from '../mappers/tagsMapper'; export function listTags() { return doFetchFromBackend('tags.list') diff --git a/lib/typescript/httpclient/endpoints/loginViaEmail.ts b/lib/typescript/httpclient/endpoints/loginViaEmail.ts index e64db2f026..9d99522e35 100644 --- a/lib/typescript/httpclient/endpoints/loginViaEmail.ts +++ b/lib/typescript/httpclient/endpoints/loginViaEmail.ts @@ -1,17 +1,7 @@ import {doFetchFromBackend} from '../api'; -import {User} from '../model'; import {LoginViaEmailRequestPayload} from '../payload'; import {UserPayload} from '../payload/UserPayload'; - -const userMapper = (payload: UserPayload): User => { - return { - id: payload.id, - firstName: payload.first_name, - lastName: payload.last_name, - displayName: payload.first_name + ' ' + payload.last_name, - token: payload.token, - }; -}; +import {userMapper} from '../mappers/userMapper'; export function loginViaEmail(requestPayload: LoginViaEmailRequestPayload) { return doFetchFromBackend('users.login', requestPayload) diff --git a/lib/typescript/httpclient/mappers/channelMapper.ts b/lib/typescript/httpclient/mappers/channelMapper.ts new file mode 100644 index 0000000000..b8bf0b19f9 --- /dev/null +++ b/lib/typescript/httpclient/mappers/channelMapper.ts @@ -0,0 +1,10 @@ +import {Channel} from '../model'; +import {ChannelApiPayload} from '../payload/ChannelApiPayload'; + +export const channelMapper = (payload: ChannelApiPayload): Channel => ({ + name: payload.name, + source: payload.source, + sourceChannelId: payload.source_channel_id, + imageUrl: payload.image_url, + connected: true, +}); diff --git a/lib/typescript/httpclient/mappers/channelsMapper.ts b/lib/typescript/httpclient/mappers/channelsMapper.ts new file mode 100644 index 0000000000..2980892bb3 --- /dev/null +++ b/lib/typescript/httpclient/mappers/channelsMapper.ts @@ -0,0 +1,11 @@ +import {ChannelsPayload} from '../payload/ChannelsPayload'; +import {Channel} from '../model'; + +export const channelsMapper = (payload: ChannelsPayload, source?: string): Channel[] => { + return payload.data.map( + (entry: Channel): Channel => ({ + source, + ...entry, + }) + ); +}; diff --git a/lib/typescript/httpclient/mappers/connectChannelApiMapper.ts b/lib/typescript/httpclient/mappers/connectChannelApiMapper.ts new file mode 100644 index 0000000000..65e3f20b12 --- /dev/null +++ b/lib/typescript/httpclient/mappers/connectChannelApiMapper.ts @@ -0,0 +1,9 @@ +import {ConnectChannelRequestPayload, ConnectChannelRequestApiPayload} from '../payload'; + +export const connectChannelApiMapper = (payload: ConnectChannelRequestPayload): ConnectChannelRequestApiPayload => ({ + source: payload.source, + source_channel_id: payload.sourceChannelId, + token: payload.token, + name: payload.name, + image_url: payload.imageUrl, +}); diff --git a/lib/typescript/httpclient/mappers/conversationMapper.ts b/lib/typescript/httpclient/mappers/conversationMapper.ts new file mode 100644 index 0000000000..b875149b50 --- /dev/null +++ b/lib/typescript/httpclient/mappers/conversationMapper.ts @@ -0,0 +1,19 @@ +import {Conversation} from '../model'; +import {ConversationPayload} from '../payload/ConversationPayload'; +import {messageMapper} from './messageMapper'; + +export const conversationMapper = (payload: ConversationPayload): Conversation => ({ + id: payload.id, + channel: payload.channel, + createdAt: payload.created_at, + contact: { + avatarUrl: payload.contact.avatar_url, + firstName: payload.contact.first_name, + lastName: payload.contact.last_name, + displayName: payload.contact.first_name + ' ' + payload.contact.last_name, + id: payload.contact.id, + }, + tags: payload.tags, + lastMessage: messageMapper(payload.last_message), + unreadMessageCount: payload.unread_message_count, +}); diff --git a/lib/typescript/httpclient/mappers/conversationsMapper.ts b/lib/typescript/httpclient/mappers/conversationsMapper.ts new file mode 100644 index 0000000000..b17a56d3ec --- /dev/null +++ b/lib/typescript/httpclient/mappers/conversationsMapper.ts @@ -0,0 +1,7 @@ +import {Conversation} from '../model'; +import {ConversationPayload} from '../payload/ConversationPayload'; +import {conversationMapper} from './conversationMapper'; + +export const conversationsMapper = (payloadArray: ConversationPayload[]): Conversation[] => { + return (payloadArray || []).map(conversation => conversationMapper(conversation)); +}; diff --git a/lib/typescript/httpclient/mappers/disconnectChannelApiMapper.ts b/lib/typescript/httpclient/mappers/disconnectChannelApiMapper.ts new file mode 100644 index 0000000000..0916c16c6b --- /dev/null +++ b/lib/typescript/httpclient/mappers/disconnectChannelApiMapper.ts @@ -0,0 +1,7 @@ +import {DisconnectChannelRequestPayload, DisconnectChannelRequestApiPayload} from '../payload'; + +export const disconnectChannelApiMapper = ( + payload: DisconnectChannelRequestPayload +): DisconnectChannelRequestApiPayload => ({ + channel_id: payload.channelId, +}); diff --git a/lib/typescript/httpclient/mappers/messageMapper.ts b/lib/typescript/httpclient/mappers/messageMapper.ts new file mode 100644 index 0000000000..59fba866f5 --- /dev/null +++ b/lib/typescript/httpclient/mappers/messageMapper.ts @@ -0,0 +1,10 @@ +import {MessagePayload} from '../payload/MessagePayload'; +import {Message} from '../model'; + +export const messageMapper = (payload: MessagePayload): Message => ({ + id: payload.id, + content: payload.content, + deliveryState: payload.delivery_state, + senderType: payload.sender_type, + sentAt: new Date(payload.sent_at), +}); diff --git a/lib/typescript/httpclient/mappers/messageMapperData.ts b/lib/typescript/httpclient/mappers/messageMapperData.ts new file mode 100644 index 0000000000..fc480dda91 --- /dev/null +++ b/lib/typescript/httpclient/mappers/messageMapperData.ts @@ -0,0 +1,7 @@ +import {MessagePayload} from '../payload/MessagePayload'; +import {Message, MessagePayloadData} from '../model'; +import {messageMapper} from './messageMapper'; + +export const messageMapperData = (payload: MessagePayloadData): Message[] => { + return payload.data.map((messagePayload: MessagePayload) => messageMapper(messagePayload)); +}; diff --git a/lib/typescript/httpclient/mappers/tagsMapper.ts b/lib/typescript/httpclient/mappers/tagsMapper.ts new file mode 100644 index 0000000000..02d6dec3f3 --- /dev/null +++ b/lib/typescript/httpclient/mappers/tagsMapper.ts @@ -0,0 +1,12 @@ +import {Tag} from '../model'; + +const tagMapper = { + BLUE: 'tag-blue', + RED: 'tag-red', + GREEN: 'tag-green', + PURPLE: 'tag-purple', +}; + +export const tagsMapper = (serverTags: Tag[]): Tag[] => { + return serverTags.map(t => ({id: t.id, name: t.name, color: tagMapper[t.color] || 'tag-blue'})); +}; diff --git a/lib/typescript/httpclient/mappers/userMapper.ts b/lib/typescript/httpclient/mappers/userMapper.ts new file mode 100644 index 0000000000..a79115b0bb --- /dev/null +++ b/lib/typescript/httpclient/mappers/userMapper.ts @@ -0,0 +1,10 @@ +import {UserPayload} from '../payload/UserPayload'; +import {User} from '../model'; + +export const userMapper = (payload: UserPayload): User => ({ + id: payload.id, + firstName: payload.first_name, + lastName: payload.last_name, + displayName: payload.first_name + ' ' + payload.last_name, + token: payload.token, +}); From dd80c38a1809c9eae618ab15c054127a9a41255d Mon Sep 17 00:00:00 2001 From: Pascal Holy <54705263+pascal-airy@users.noreply.github.com> Date: Thu, 14 Jan 2021 18:03:16 +0100 Subject: [PATCH 10/56] [#546] Revise image tags (#658) Fixes #546 --- .bazelrc | 2 +- README.md | 15 +++++----- docs/docs/guides/airy-core-in-production.md | 26 ++++++---------- docs/docs/guides/airy-core-in-test-env.md | 11 ++++--- docs/docs/overview/release-process.md | 2 +- docs/docs/sources/google.md | 2 +- docs/docs/sources/twilio-source.mdx | 2 +- frontend/demo/README.md | 8 +++-- infrastructure/.gitignore | 2 +- .../{airy.conf.all => airy.tpl.yaml} | 6 ++-- infrastructure/cli/cmd/config/config.go | 2 +- .../cli/pkg/tests/golden/cli.no-args.golden | 2 +- .../apps/charts/airy-config/values.yaml | 2 +- .../charts/kafka/charts/kafka/values.yaml | 2 +- .../schema-registry/templates/deployment.yaml | 2 +- .../kafka/charts/schema-registry/values.yaml | 1 + .../charts/kafka/charts/zookeeper/values.yaml | 2 +- infrastructure/images/kafka/Makefile | 8 ++--- .../images/schema-registry/Makefile | 9 ++---- infrastructure/images/tasks/pull-images.sh | 8 ----- infrastructure/images/vagrant.json | 8 +---- infrastructure/scripts/conf.sh | 20 ++++--------- infrastructure/scripts/provision/core.sh | 11 +++++-- .../scripts/provision/prerequisites.sh | 4 ++- scripts/bootstrap.sh | 11 +++---- scripts/push-images.sh | 10 ++----- scripts/release.sh | 4 +-- tools/build/{status.sh => bazel_status.sh} | 0 tools/build/container_push.bzl | 30 +++++++++++-------- 29 files changed, 90 insertions(+), 122 deletions(-) rename infrastructure/{airy.conf.all => airy.tpl.yaml} (93%) delete mode 100755 infrastructure/images/tasks/pull-images.sh rename tools/build/{status.sh => bazel_status.sh} (100%) diff --git a/.bazelrc b/.bazelrc index 8a0914b74d..b1b2478ad2 100644 --- a/.bazelrc +++ b/.bazelrc @@ -3,7 +3,7 @@ build --strategy=TypeScriptCompile=worker # Use java 11 -build --java_toolchain=@bazel_tools//tools/jdk:toolchain_java11 --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_java11 +build --java_toolchain=@bazel_tools//tools/jdk:toolchain_java11 --host_java_toolchain=@bazel_tools//tools/jdk:toolchain_java11 --workspace_status_command=tools/build/bazel_status.sh # Output test errors by default test --test_output=errors diff --git a/README.md b/README.md index 8b13d10a73..4f5bf70d0a 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,13 @@ messaging platform to process conversational data from a variety of sources (like Facebook Messenger, Google Business Messages, Website Live Chat, and more). -- [Getting Started](#getting-started) -- [Components](#components) -- [Organization of this Repo](#organization-of-the-repository) -- [Design Principles](#design-principles) -- [How to contribute](#how-to-contribute) -- [Code of Conduct](#code-of-conduct) +- [Airy Core Platform](#airy-core-platform) + - [Getting started](#getting-started) + - [Components](#components) + - [Organization of the Repository](#organization-of-the-repository) + - [Design Principles](#design-principles) + - [How to contribute](#how-to-contribute) + - [Code of Conduct](#code-of-conduct) ## Getting started @@ -19,7 +20,7 @@ You can run the Airy Core Platform locally by running the following commands: ```sh $ git clone https://github.com/airyhq/airy $ cd airy -$ AIRY_VERSION=beta ./scripts/bootstrap.sh +$ ./scripts/bootstrap.sh ``` The bootstrap installation requires diff --git a/docs/docs/guides/airy-core-in-production.md b/docs/docs/guides/airy-core-in-production.md index 86ea95fd66..4b7f3b95a8 100644 --- a/docs/docs/guides/airy-core-in-production.md +++ b/docs/docs/guides/airy-core-in-production.md @@ -149,13 +149,13 @@ Kafka cluster, PostgreSQL and Redis can be done by creating a configuration file, prior to deploying the apps. Make sure that the `Airy apps` also have network connectivity to the required services. -The file `infrastructure/airy.conf.all` contains an example of all possible -configuration parameters. This file should be copied to `airy.conf` and edited +The file `infrastructure/airy.tpl.yaml` contains an example of all possible +configuration parameters. This file should be copied to `airy.yaml` and edited according to your environment: ```sh cd infrastructure -cp airy.conf.all airy.conf +cp airy.tpl.yaml airy.yaml ``` Edit the file to configure connections to the base services. Make sure to configure the @@ -193,10 +193,10 @@ apps: ### Deployment We provided a Helm chart to deploy the `Airy apps`. Before you can run helm, you -must configure the system via the `airy.conf` file, then you can proceed: +must configure the system via the `airy.yaml` file, then you can proceed: ```sh -cp airy.conf ./helm-chart/charts/apps/values.yaml +cp airy.yaml ./helm-chart/charts/apps/values.yaml helm install core ./helm-chart/charts/apps/ --timeout 1000s ``` @@ -214,19 +214,11 @@ kubectl scale deployment -l type=sources-twilio --replicas=1 At this point you should have a running `Airy Core Platform` in your environment 🎉. -To deploy with a different `image tag` (for example `beta` from the `develop` -branch), you can run: - -```sh -export AIRY_VERSION=beta -helm install core ./helm-chart/charts/apps/ --set global.appImageTag=${AIRY_VERSION} --timeout 1000s -``` - If afterwards you need to modify or add other config parameters in the -`airy.conf` file, after editing the file run: +`airy.yaml` file, after editing the file run: ```sh -cp airy.conf ./helm-chart/charts/apps/values.yaml +cp airy.yaml ./helm-chart/charts/apps/values.yaml helm upgrade core ./helm-chart/charts/apps/ --timeout 1000s ``` @@ -234,8 +226,8 @@ If you deploy the Airy Core Platform with a specific version tag, you must export the `AIRY_VERSION` variable before running `helm upgrade`: ```sh -cp airy.conf ./helm-chart/charts/apps/values.yaml -export AIRY_VERSION=beta +cp airy.yaml ./helm-chart/charts/apps/values.yaml +export AIRY_VERSION=develop helm upgrade core ./helm-chart/charts/apps/ --set global.appImageTag=${AIRY_VERSION} --timeout 1000s ``` diff --git a/docs/docs/guides/airy-core-in-test-env.md b/docs/docs/guides/airy-core-in-test-env.md index 96b1873287..4f5a377955 100644 --- a/docs/docs/guides/airy-core-in-test-env.md +++ b/docs/docs/guides/airy-core-in-test-env.md @@ -139,8 +139,8 @@ document The bootstrap process creates a random URL which is then provisioned inside the Helm chart. To configure these URLs, you can specify them in the `infrastructure/helm-chart/charts/apps/charts/airy-co)fig/values.yaml` document. -Alternatively you can edit the `airy.conf` file by setting the following -parameter (see `airy.conf.all` for more examples): +Alternatively you can edit the `airy.yaml` file by setting the following +parameter (see `airy.tpl.yaml` for more examples): ``` sources: @@ -154,16 +154,15 @@ After preparing the configuration, run the following commands to apply the chang cd infrastructure vagrant ssh sudo -i -cp /vagrant/airy.conf ~/airy-core/helm-chart/charts/apps/values.yaml -helm upgrade core ~/airy-core/helm-chart/charts/apps/ --timeout 1000s +helm upgrade core ~/airy-core/helm-chart/charts/apps/ --values /vagrant/airy.yaml --timeout 1000s ``` ## Connect sources Integrating sources into the `Airy Core Platform` often requires specific configuration settings, refer to the source specific docs for details. You must -provide the settings in `infrastructure/airy.conf` configuration file. An -example of the configuration can be found in `airy.conf.all`. +provide the settings in `infrastructure/airy.yaml` configuration file. An +example of the configuration can be found in `airy.tpl.yaml`. After setting the configuration run: diff --git a/docs/docs/overview/release-process.md b/docs/docs/overview/release-process.md index 10d70fa83d..3040c16b46 100644 --- a/docs/docs/overview/release-process.md +++ b/docs/docs/overview/release-process.md @@ -13,7 +13,7 @@ Here's an outline of the process: - We need a `GITHUB_TOKEN` environment variable with write permission to the org - We run `./scripts/release.sh start x.y.z` - Once release days comes, we execute the following steps: - - We test our release (`AIRY_VERSION=release ./scripts/bootstrap.sh`) and any + - We test our release (`./scripts/bootstrap.sh`) and any additional hot-fix is committed directly to the release branch - Once we're satisfied with the release, we finish the release by running `./scripts/release.sh finish x.y.z` - We archive cards in the done column of the [work in progress](https://github.com/airyhq/airy/projects/1) board diff --git a/docs/docs/sources/google.md b/docs/docs/sources/google.md index bb29a5715b..fc23eeb091 100644 --- a/docs/docs/sources/google.md +++ b/docs/docs/sources/google.md @@ -16,7 +16,7 @@ Business Location and your running instance of the Airy Core Platform. ## Configuration The first step is to copy the Google Service Account file provided by Google to -`infrastructure/airy.conf` as a one line string +`infrastructure/airy.yaml` as a one line string ``` GOOGLE_SA_FILE= diff --git a/docs/docs/sources/twilio-source.mdx b/docs/docs/sources/twilio-source.mdx index addc708a0a..c6bf5389f1 100644 --- a/docs/docs/sources/twilio-source.mdx +++ b/docs/docs/sources/twilio-source.mdx @@ -1,6 +1,6 @@ You must create a [Twilio auth token](https://support.twilio.com/hc/en-us/articles/223136027-Auth-Tokens-and-How-to-Change-Them) -and add it to `infrastructure/airy.conf` together with your account SID: +and add it to `infrastructure/airy.yaml` together with your account SID: ``` authToken= diff --git a/frontend/demo/README.md b/frontend/demo/README.md index b3236a533b..4233270460 100644 --- a/frontend/demo/README.md +++ b/frontend/demo/README.md @@ -8,7 +8,9 @@ The Airy Demo UI is a minimal user interactive frontend project that showcases the Airy Core Platform API.It enables users to experience the functionalities of our Airy Core Platform. -- [Prerequities](#prerequities) + +- [Airy Demo UI](#airy-demo-ui) +- [Prerequisites](#prerequisites) - [Building Airy Demo UI](#building-airy-demo-ui) - [Installation](#installation) - [Authentication](#authentication) @@ -29,14 +31,14 @@ You can run the Airy Demo UI locally by running the following commands: ``` $ git clone https://github.com/airyhq/airy $ cd airy -$ AIRY_VERSION=beta ./scripts/bootstrap.sh (Takes a few minutes) +$ ./scripts/bootstrap.sh (Takes a few minutes) ``` When the bootstrap process finishes, open another terminal and run ``` $ ibazel run //frontend/demo:bundle_server ``` Then open `http://localhost:8080/` in a web browser to access the Airy Demo UI ### Installation The bootstrap installation requires [Vagrant](https://www.vagrantup.com/downloads) and [VirtualBox](https://www.virtualbox.org/wiki/Downloads). If they are not -found, the script ```$ AIRY_VERSION=beta ./scripts/bootstrap.sh``` will attempt to install them for you. Check out our [test deployment guide](/docs/docs/guides/airy-core-in-test-env.md) for detailed information. +found, the script ```$ ./scripts/bootstrap.sh``` will attempt to install them for you. Check out our [test deployment guide](/docs/docs/guides/airy-core-in-test-env.md) for detailed information. ### Authentication diff --git a/infrastructure/.gitignore b/infrastructure/.gitignore index 64b7b113e9..f0e21c63a5 100644 --- a/infrastructure/.gitignore +++ b/infrastructure/.gitignore @@ -2,4 +2,4 @@ .vagrant-out/ images/.vagrant/ images/.vagrant-out/ -airy.conf +airy.yaml diff --git a/infrastructure/airy.conf.all b/infrastructure/airy.tpl.yaml similarity index 93% rename from infrastructure/airy.conf.all rename to infrastructure/airy.tpl.yaml index d8095ca83b..06e0b5446f 100644 --- a/infrastructure/airy.conf.all +++ b/infrastructure/airy.tpl.yaml @@ -1,6 +1,6 @@ # Global configuration global: - appImageTag: latest + appImageTag: develop containerRegistry: ghcr.io/airyhq namespace: default core: @@ -18,8 +18,8 @@ core: postgresql: endpoint: "postgres:5432" dbName: "admin" - username: "postgresadmin" - password: "long-random-generated-password" + username: "postgresadmin" + password: "changeme" # Specific configurations for sources sources: facebook: diff --git a/infrastructure/cli/cmd/config/config.go b/infrastructure/cli/cmd/config/config.go index eba053c773..bfed789265 100644 --- a/infrastructure/cli/cmd/config/config.go +++ b/infrastructure/cli/cmd/config/config.go @@ -10,7 +10,7 @@ import ( var ConfigCmd = &cobra.Command{ Use: "config", TraverseChildren: true, - Short: "Reloads configuration based on airy.conf", + Short: "Reloads configuration based on airy.yaml", Long: ``, Run: config, } diff --git a/infrastructure/cli/pkg/tests/golden/cli.no-args.golden b/infrastructure/cli/pkg/tests/golden/cli.no-args.golden index 6a3bee7a32..d14a1b5cde 100644 --- a/infrastructure/cli/pkg/tests/golden/cli.no-args.golden +++ b/infrastructure/cli/pkg/tests/golden/cli.no-args.golden @@ -5,7 +5,7 @@ Usage: Available Commands: auth Create a default user and return a JWT token - config Reloads configuration based on airy.conf + config Reloads configuration based on airy.yaml help Help about any command init Inits your Airy CLI configuration ui Opens the Airy Core Platform UI in your local browser diff --git a/infrastructure/helm-chart/charts/apps/charts/airy-config/values.yaml b/infrastructure/helm-chart/charts/apps/charts/airy-config/values.yaml index 39707d6b13..12d4d27b07 100644 --- a/infrastructure/helm-chart/charts/apps/charts/airy-config/values.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/airy-config/values.yaml @@ -7,7 +7,7 @@ redis: port: 6379 postgresql: endpoint: "postgres:5432" - username: "postgresadmin" + username: "postgresadmin" password: "changeme" sources: facebook: diff --git a/infrastructure/helm-chart/charts/kafka/charts/kafka/values.yaml b/infrastructure/helm-chart/charts/kafka/charts/kafka/values.yaml index 9cd848c8af..5a0fc937ab 100644 --- a/infrastructure/helm-chart/charts/kafka/charts/kafka/values.yaml +++ b/infrastructure/helm-chart/charts/kafka/charts/kafka/values.yaml @@ -1,6 +1,6 @@ brokers: 1 image: ghcr.io/airyhq/infrastructure/kafka -imageTag: release +imageTag: 2.5.1 imagePullPolicy: IfNotPresent imagePullSecrets: podManagementPolicy: OrderedReady diff --git a/infrastructure/helm-chart/charts/kafka/charts/schema-registry/templates/deployment.yaml b/infrastructure/helm-chart/charts/kafka/charts/schema-registry/templates/deployment.yaml index c4763f40a5..a8dd878e4f 100644 --- a/infrastructure/helm-chart/charts/kafka/charts/schema-registry/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/kafka/charts/schema-registry/templates/deployment.yaml @@ -24,7 +24,7 @@ spec: spec: containers: - name: schema-registry-server - image: "{{ .Values.global.containerRegistry}}/{{ .Values.image }}:{{ .Values.global.appImageTag }}" + image: "{{ .Values.global.containerRegistry}}/{{ .Values.image }}:{{ .Values.imageTag }}" imagePullPolicy: "{{ .Values.imagePullPolicy }}" ports: - name: schema-registry diff --git a/infrastructure/helm-chart/charts/kafka/charts/schema-registry/values.yaml b/infrastructure/helm-chart/charts/kafka/charts/schema-registry/values.yaml index 1ed01b5c09..b76d2b791e 100644 --- a/infrastructure/helm-chart/charts/kafka/charts/schema-registry/values.yaml +++ b/infrastructure/helm-chart/charts/kafka/charts/schema-registry/values.yaml @@ -1,5 +1,6 @@ replicaCount: 0 image: infrastructure/schema-registry +imageTag: 2.0.1 imagePullPolicy: Always servicePort: 8081 kafka: diff --git a/infrastructure/helm-chart/charts/kafka/charts/zookeeper/values.yaml b/infrastructure/helm-chart/charts/kafka/charts/zookeeper/values.yaml index 97a2ff75fc..e93c0527a1 100644 --- a/infrastructure/helm-chart/charts/kafka/charts/zookeeper/values.yaml +++ b/infrastructure/helm-chart/charts/kafka/charts/zookeeper/values.yaml @@ -1,6 +1,6 @@ servers: 1 image: ghcr.io/airyhq/infrastructure/kafka -imageTag: release +imageTag: 2.5.1 imagePullPolicy: IfNotPresent imagePullSecrets: podManagementPolicy: OrderedReady diff --git a/infrastructure/images/kafka/Makefile b/infrastructure/images/kafka/Makefile index 716c33ca55..10d19433a8 100644 --- a/infrastructure/images/kafka/Makefile +++ b/infrastructure/images/kafka/Makefile @@ -1,10 +1,6 @@ build: docker build -t airy-kafka . -release-beta: build - docker tag airy-kafka ghcr.io/airyhq/infrastructure/kafka:beta - docker push ghcr.io/airyhq/infrastructure/kafka:beta - release: build - docker tag airy-kafka ghcr.io/airyhq/infrastructure/kafka:release - docker push ghcr.io/airyhq/infrastructure/kafka:release + docker tag airy-kafka ghcr.io/airyhq/infrastructure/kafka:2.5.1 + docker push ghcr.io/airyhq/infrastructure/kafka:2.5.1 diff --git a/infrastructure/images/schema-registry/Makefile b/infrastructure/images/schema-registry/Makefile index d1aa464411..529ac7e112 100644 --- a/infrastructure/images/schema-registry/Makefile +++ b/infrastructure/images/schema-registry/Makefile @@ -1,10 +1,7 @@ build: docker build -t schema-registry . --build-arg SCHEMA_REGISTRY_VERSION=${SCHEMA_REGISTRY_VERSION} -release-beta: build - docker tag schema-registry ghcr.io/airyhq/infrastructure/schema-registry:beta - docker push ghcr.io/airyhq/infrastructure/schema-registry:beta - release: build - docker tag schema-registry ghcr.io/airyhq/infrastructure/schema-registry:release - docker push ghcr.io/airyhq/infrastructure/schema-registry:release + docker tag schema-registry ghcr.io/airyhq/infrastructure/schema-registry:2.0.1 + docker push ghcr.io/airyhq/infrastructure/schema-registry:2.0.1 + diff --git a/infrastructure/images/tasks/pull-images.sh b/infrastructure/images/tasks/pull-images.sh deleted file mode 100755 index a0bb68b5ab..0000000000 --- a/infrastructure/images/tasks/pull-images.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -euo pipefail -IFS=$'\n\t' - -docker pull confluentinc/cp-kafka:5.5.0 -docker pull confluentinc/cp-zookeeper:5.5.0 -docker pull postgres:12.4-alpine -docker pull redis:5.0.1-alpine diff --git a/infrastructure/images/vagrant.json b/infrastructure/images/vagrant.json index da2cdf66b2..17f54e1fb4 100644 --- a/infrastructure/images/vagrant.json +++ b/infrastructure/images/vagrant.json @@ -16,12 +16,6 @@ } ], "provisioners": [ - { - "scripts": [ - "tasks/pull-images.sh" - ], - "type": "shell" - }, { "scripts": [ "../scripts/provision.sh" @@ -30,7 +24,7 @@ }, { "scripts": [ - "../scripts/airy-conf.sh" + "../scripts/airy.yaml.sh" ], "type": "shell" } diff --git a/infrastructure/scripts/conf.sh b/infrastructure/scripts/conf.sh index 59b00b0f68..56895db89a 100755 --- a/infrastructure/scripts/conf.sh +++ b/infrastructure/scripts/conf.sh @@ -5,30 +5,20 @@ IFS=$'\n\t' SCRIPT_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P) INFRASTRUCTURE_PATH=$(cd ${SCRIPT_PATH}/../; pwd -P) -if [[ ! -f ${INFRASTRUCTURE_PATH}/airy.conf ]]; then - echo "No airy.conf config file found" +if [[ ! -f ${INFRASTRUCTURE_PATH}/airy.yaml ]]; then + echo "No airy.yaml config file found" exit 0 fi -source ${INFRASTRUCTURE_PATH}/scripts/lib/k8s.sh +source /vagrant/scripts/lib/k8s.sh -DEPLOYED_AIRY_VERSION=`kubectl get configmap core-config -o jsonpath='{.data.APP_IMAGE_TAG}'` -if $(grep -q " appImageTag" ${INFRASTRUCTURE_PATH}/airy.conf); then - CONFIGURED_AIRY_VERSION=$(grep " appImageTag" ${INFRASTRUCTURE_PATH}/airy.conf | head -n 1 | awk '{ print $2}') -else - CONFIGURED_AIRY_VERSION="" -fi +source ${INFRASTRUCTURE_PATH}/scripts/lib/k8s.sh -if [ -z ${CONFIGURED_AIRY_VERSION} ]; then - AIRY_VERSION=${DEPLOYED_AIRY_VERSION} -else - AIRY_VERSION=${CONFIGURED_AIRY_VERSION} -fi kubectl delete pod startup-helper --force 2>/dev/null || true kubectl run startup-helper --image busybox --command -- /bin/sh -c "tail -f /dev/null" -helm upgrade core ${INFRASTRUCTURE_PATH}/helm-chart/ --values ${INFRASTRUCTURE_PATH}/airy.conf --set global.appImageTag=${AIRY_VERSION} --timeout 1000s > /dev/null 2>&1 +helm upgrade core ${INFRASTRUCTURE_PATH}/helm-chart/ --values ${INFRASTRUCTURE_PATH}/airy.yaml --timeout 1000s > /dev/null 2>&1 kubectl scale deployment schema-registry --replicas=1 diff --git a/infrastructure/scripts/provision/core.sh b/infrastructure/scripts/provision/core.sh index 3e99c62ee6..eebd857336 100755 --- a/infrastructure/scripts/provision/core.sh +++ b/infrastructure/scripts/provision/core.sh @@ -6,14 +6,19 @@ SCRIPT_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P) INFRASTRUCTURE_PATH=$(cd ${SCRIPT_PATH}/../../; pwd -P) source ${INFRASTRUCTURE_PATH}/scripts/lib/k8s.sh -APP_IMAGE_TAG="${AIRY_VERSION:-latest}" -echo "Deploying the Airy Core Platform with the ${APP_IMAGE_TAG} image tag" cd ${INFRASTRUCTURE_PATH}/scripts/ wait-for-service-account -helm install core ${INFRASTRUCTURE_PATH}/helm-chart/ --set global.appImageTag=${APP_IMAGE_TAG} --version 0.5.0 --timeout 1000s > /dev/null 2>&1 + +echo "Deploying the Airy Core Platform with the ${AIRY_VERSION} image tag" + +if [[ -f ${INFRASTRUCTURE_PATH}/airy.yaml ]]; then + yq w -i ${INFRASTRUCTURE_PATH}/airy.yaml global.appImageTag ${AIRY_VERSION} +fi + +helm install core ${INFRASTRUCTURE_PATH}/helm-chart/ --set global.appImageTag=${AIRY_VERSION} --version 0.5.0 --timeout 1000s > /dev/null 2>&1 kubectl run startup-helper --image busybox --command -- /bin/sh -c "tail -f /dev/null" diff --git a/infrastructure/scripts/provision/prerequisites.sh b/infrastructure/scripts/provision/prerequisites.sh index 16d16d04d2..544b31dc17 100755 --- a/infrastructure/scripts/provision/prerequisites.sh +++ b/infrastructure/scripts/provision/prerequisites.sh @@ -2,7 +2,9 @@ set -euo pipefail IFS=$'\n\t' -apk add --no-cache wget unzip jq bash-completion +echo "http://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories +apk update +apk add --no-cache wget unzip jq yq bash-completion curl -sfL https://get.k3s.io | sh - diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index 9cbff712af..e856536e6b 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -59,7 +59,7 @@ read -p "Do you want to add the vagrant box to the host file so you can access i echo if [[ $REPLY =~ ^[Yy]$ ]]; then - vagrant plugin install vagrant-hostsupdater --plugin-clean-sources --plugin-source https://gems.ruby-china.com + vagrant plugin install vagrant-hostsupdater || vagrant plugin install vagrant-hostsupdater --plugin-clean-sources --plugin-source https://gems.ruby-china.com fi if ! command -v VBoxManage &> /dev/null @@ -130,14 +130,11 @@ if [ -z ${AIRY_VERSION+x} ]; then branch_name=${branch_name##refs/heads/} case "$branch_name" in - develop ) - AIRY_VERSION=beta - ;; - release* ) - AIRY_VERSION=release + main|release* ) + AIRY_VERSION=(`cat ../VERSION`) ;; * ) - AIRY_VERSION=latest + AIRY_VERSION=develop ;; esac fi diff --git a/scripts/push-images.sh b/scripts/push-images.sh index 878c725e40..d236688b06 100755 --- a/scripts/push-images.sh +++ b/scripts/push-images.sh @@ -9,15 +9,11 @@ echo "Branch target: ${BRANCH_TARGET}" case ${BRANCH_TARGET} in develop) - tag="beta" + tag="develop" ;; - main) - tag="latest" - ;; - - release) - tag="release" + main|release) + tag=`(cat ../VERSION)` ;; esac diff --git a/scripts/release.sh b/scripts/release.sh index b7869ba262..c729483914 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -8,6 +8,8 @@ start() { echo -e "Starting release ${release_number}\n" create_issue create_release_branch + increase_version + commit_version } create_issue() { @@ -31,8 +33,6 @@ create_release_branch() { finish() { release_number=$1 echo -e "Finishing release ${release_number}\n" - increase_version - commit_version merge_main merge_develop echo -e "Release ${release_number} is finished\n" diff --git a/tools/build/status.sh b/tools/build/bazel_status.sh similarity index 100% rename from tools/build/status.sh rename to tools/build/bazel_status.sh diff --git a/tools/build/container_push.bzl b/tools/build/container_push.bzl index 460cc9426f..a8c90a42a3 100644 --- a/tools/build/container_push.bzl +++ b/tools/build/container_push.bzl @@ -1,19 +1,14 @@ load("@io_bazel_rules_docker//container:container.bzl", lib_push = "container_push") -tags_to_push = ["release", "latest", "beta"] - def container_push(registry, repository): - [ - lib_push( - name = tag, - format = "Docker", - image = ":image", - registry = registry, - repository = repository, - tag = tag, - ) - for tag in tags_to_push - ] + lib_push( + name = "develop", + format = "Docker", + image = ":image", + registry = registry, + repository = repository, + tag = "develop", + ) lib_push( name = "local", @@ -23,3 +18,12 @@ def container_push(registry, repository): repository = repository, tag = "{BUILD_USER}", ) + + lib_push( + name = "release", + format = "Docker", + image = ":image", + registry = registry, + repository = repository, + tag = "{STABLE_VERSION}", + ) From 6332e1b7900b4ff2f5e982eb1a0e27608e0ceda1 Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Fri, 15 Jan 2021 09:45:28 +0100 Subject: [PATCH 11/56] [#651] Use GCS as a Bazel cache (#652) --- .bazelrc | 3 +-- .github/workflows/main.yml | 12 ++++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/.bazelrc b/.bazelrc index b1b2478ad2..4175338085 100644 --- a/.bazelrc +++ b/.bazelrc @@ -21,7 +21,6 @@ build:ci --test_env=ROOT_LOG_LEVEL=ERROR build:ci --noshow_progress build:ci --verbose_failures -build:ci --disk_cache=~/.cache/bazel -build:ci --repository_cache==~/.cache/bazel_external +build:ci --remote_cache=https://storage.googleapis.com/airy-ci-cache test:ci --flaky_test_attempts=2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 397893c171..2fd6e8bf81 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -15,14 +15,6 @@ jobs: java-version: '11' architecture: 'x64' - - name: Mount bazel cache - uses: actions/cache@v2 - with: - path: | - /home/runner/.cache/bazel - /home/runner/.cache/bazel_external - key: bazel - - name: Install bazelisk run: | curl -LO "https://github.com/bazelbuild/bazelisk/releases/download/v1.1.0/bazelisk-linux-amd64" @@ -32,9 +24,13 @@ jobs: - name: Enable CI settings run: | + echo "$GCS_SA_KEY" > key.json cat <>.bazelrc common --config=ci + build:ci --google_credentials=key.json EOF + env: + GCS_SA_KEY: ${{secrets.GCS_SA_KEY}} - name: Lint run: | From aeb7360575026e3b1fcc4dc56ea0ed2953e261a9 Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Fri, 15 Jan 2021 14:52:43 +0100 Subject: [PATCH 12/56] Disable go plugin by default since it only works for ultimate users (#663) --- .ijwb/.bazelproject | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ijwb/.bazelproject b/.ijwb/.bazelproject index 0be506c563..3d7333f420 100644 --- a/.ijwb/.bazelproject +++ b/.ijwb/.bazelproject @@ -13,7 +13,7 @@ additional_languages: # Uncomment any additional languages you want supported # android # dart - go + # go javascript # kotlin python @@ -21,4 +21,4 @@ additional_languages: typescript ts_config_rules: - //frontend/chat-plugin:widget_tsconfig \ No newline at end of file + //frontend/chat-plugin:widget_tsconfig From ff551fdacff23869393378124c90342afbb4b6fd Mon Sep 17 00:00:00 2001 From: AudreyKj <38159391+AudreyKj@users.noreply.github.com> Date: Fri, 15 Jan 2021 16:47:20 +0100 Subject: [PATCH 13/56] refactored lib with class that can be instantiated (#664) --- frontend/demo/src/AiryConfig.ts | 4 + frontend/demo/src/InitializeAiryApi.ts | 6 + frontend/demo/src/actions/channel/index.ts | 10 +- .../demo/src/actions/conversations/index.ts | 9 +- frontend/demo/src/actions/messages/index.ts | 5 +- frontend/demo/src/actions/tags/index.tsx | 11 +- frontend/demo/src/actions/user/index.ts | 5 +- .../demo/src/cookies}/cookie.ts | 0 .../demo/src/cookies}/index.ts | 1 - .../demo/src/cookies}/webStore.ts | 2 +- frontend/demo/src/pages/Channels/index.tsx | 3 +- frontend/demo/src/reducers/data/user/index.ts | 3 +- frontend/demo/src/reducers/index.ts | 2 +- lib/typescript/httpclient/README.md | 17 +- lib/typescript/httpclient/api/airyConfig.ts | 58 ----- .../httpclient/endpoints/connectChannel.ts | 16 -- .../httpclient/endpoints/createTag.ts | 19 -- .../httpclient/endpoints/deleteTag.ts | 9 - .../httpclient/endpoints/disconnectChannel.ts | 16 -- .../httpclient/endpoints/exploreChannels.ts | 15 -- lib/typescript/httpclient/endpoints/index.ts | 12 - .../httpclient/endpoints/listChannels.ts | 14 - .../httpclient/endpoints/listConversations.ts | 19 -- .../httpclient/endpoints/listMessages.ts | 23 -- .../httpclient/endpoints/listTags.ts | 13 - .../httpclient/endpoints/loginViaEmail.ts | 14 - .../httpclient/endpoints/readConversations.ts | 5 - .../httpclient/endpoints/updateTag.ts | 8 - lib/typescript/httpclient/index.ts | 241 +++++++++++++++--- .../httpclient/payload/PaginatedPayload.ts | 2 +- 30 files changed, 259 insertions(+), 303 deletions(-) create mode 100644 frontend/demo/src/AiryConfig.ts create mode 100644 frontend/demo/src/InitializeAiryApi.ts rename {lib/typescript/httpclient/api => frontend/demo/src/cookies}/cookie.ts (100%) rename {lib/typescript/httpclient/api => frontend/demo/src/cookies}/index.ts (64%) rename {lib/typescript/httpclient/api => frontend/demo/src/cookies}/webStore.ts (97%) delete mode 100644 lib/typescript/httpclient/api/airyConfig.ts delete mode 100644 lib/typescript/httpclient/endpoints/connectChannel.ts delete mode 100644 lib/typescript/httpclient/endpoints/createTag.ts delete mode 100644 lib/typescript/httpclient/endpoints/deleteTag.ts delete mode 100644 lib/typescript/httpclient/endpoints/disconnectChannel.ts delete mode 100644 lib/typescript/httpclient/endpoints/exploreChannels.ts delete mode 100644 lib/typescript/httpclient/endpoints/index.ts delete mode 100644 lib/typescript/httpclient/endpoints/listChannels.ts delete mode 100644 lib/typescript/httpclient/endpoints/listConversations.ts delete mode 100644 lib/typescript/httpclient/endpoints/listMessages.ts delete mode 100644 lib/typescript/httpclient/endpoints/listTags.ts delete mode 100644 lib/typescript/httpclient/endpoints/loginViaEmail.ts delete mode 100644 lib/typescript/httpclient/endpoints/readConversations.ts delete mode 100644 lib/typescript/httpclient/endpoints/updateTag.ts diff --git a/frontend/demo/src/AiryConfig.ts b/frontend/demo/src/AiryConfig.ts new file mode 100644 index 0000000000..7abde61ad7 --- /dev/null +++ b/frontend/demo/src/AiryConfig.ts @@ -0,0 +1,4 @@ +export class AiryConfig { + static NODE_ENV = process.env.NODE_ENV; + static FACEBOOK_APP_ID = 'CHANGE_ME'; +} diff --git a/frontend/demo/src/InitializeAiryApi.ts b/frontend/demo/src/InitializeAiryApi.ts new file mode 100644 index 0000000000..19edf6d76d --- /dev/null +++ b/frontend/demo/src/InitializeAiryApi.ts @@ -0,0 +1,6 @@ +import {HttpClient} from 'httpclient'; +import {getAuthToken} from './cookies'; + +const authToken = getAuthToken(); + +export const HttpClientInstance = new HttpClient(authToken); diff --git a/frontend/demo/src/actions/channel/index.ts b/frontend/demo/src/actions/channel/index.ts index 89d0482f64..ff3109a4e1 100644 --- a/frontend/demo/src/actions/channel/index.ts +++ b/frontend/demo/src/actions/channel/index.ts @@ -6,8 +6,8 @@ import { ConnectChannelRequestPayload, ExploreChannelRequestPayload, DisconnectChannelRequestPayload, - HttpClient, } from 'httpclient'; +import {HttpClientInstance} from '../../InitializeAiryApi'; const SET_CURRENT_CHANNELS = '@@channel/SET_CHANNELS'; const ADD_CHANNELS = '@@channel/ADD_CHANNELS'; @@ -20,7 +20,7 @@ export const addChannelsAction = createAction(ADD_CHANNELS, resolve => (channels export function listChannels() { return async (dispatch: Dispatch) => { - return HttpClient.listChannels() + return HttpClientInstance.listChannels() .then((response: Channel[]) => { dispatch(setCurrentChannelsAction(response)); return Promise.resolve(response); @@ -33,7 +33,7 @@ export function listChannels() { export function exploreChannels(requestPayload: ExploreChannelRequestPayload) { return async (dispatch: Dispatch) => { - return HttpClient.exploreChannels(requestPayload) + return HttpClientInstance.exploreChannels(requestPayload) .then((response: Channel[]) => { dispatch(addChannelsAction(response)); return Promise.resolve(response); @@ -46,7 +46,7 @@ export function exploreChannels(requestPayload: ExploreChannelRequestPayload) { export function connectChannel(requestPayload: ConnectChannelRequestPayload) { return async (dispatch: Dispatch) => { - return HttpClient.connectChannel(requestPayload) + return HttpClientInstance.connectChannel(requestPayload) .then((response: Channel) => { dispatch(addChannelsAction([response])); return Promise.resolve(response); @@ -59,7 +59,7 @@ export function connectChannel(requestPayload: ConnectChannelRequestPayload) { export function disconnectChannel(requestPayload: DisconnectChannelRequestPayload) { return async (dispatch: Dispatch) => { - return HttpClient.disconnectChannel(requestPayload) + return HttpClientInstance.disconnectChannel(requestPayload) .then((response: Channel[]) => { dispatch(setCurrentChannelsAction(response)); return Promise.resolve(response); diff --git a/frontend/demo/src/actions/conversations/index.ts b/frontend/demo/src/actions/conversations/index.ts index 153d05fd65..8a90651a37 100644 --- a/frontend/demo/src/actions/conversations/index.ts +++ b/frontend/demo/src/actions/conversations/index.ts @@ -1,7 +1,8 @@ import {Dispatch} from 'redux'; import {createAction} from 'typesafe-actions'; -import {HttpClient, Conversation} from 'httpclient'; +import {Conversation} from 'httpclient'; import {ResponseMetadataPayload} from 'httpclient/payload/ResponseMetadataPayload'; +import {HttpClientInstance} from '../../InitializeAiryApi'; import {StateModel} from '../../reducers'; export const CONVERSATION_LOADING = '@@conversation/LOADING'; @@ -40,7 +41,7 @@ export const removeErrorFromConversationAction = createAction( export function listConversations() { return async (dispatch: Dispatch) => { dispatch(loadingConversationsAction()); - return HttpClient.listConversations({page_size: 10}) + return HttpClientInstance.listConversations({page_size: 10}) .then((response: {data: Conversation[]; metadata: ResponseMetadataPayload}) => { dispatch(mergeConversationsAction(response.data, response.metadata)); return Promise.resolve(true); @@ -55,7 +56,7 @@ export function listNextConversations() { return async (dispatch: Dispatch, state: StateModel) => { const cursor = state.data.conversations.all.metadata.nextCursor; dispatch(loadingConversationsAction()); - return HttpClient.listConversations({cursor: cursor}) + return HttpClientInstance.listConversations({cursor: cursor}) .then((response: {data: Conversation[]; metadata: ResponseMetadataPayload}) => { dispatch(mergeConversationsAction(response.data, response.metadata)); return Promise.resolve(true); @@ -68,6 +69,6 @@ export function listNextConversations() { export function readConversations(conversationId: string) { return function(dispatch: Dispatch) { - HttpClient.readConversations(conversationId).then(() => dispatch(readConversationsAction(conversationId))); + HttpClientInstance.readConversations(conversationId).then(() => dispatch(readConversationsAction(conversationId))); }; } diff --git a/frontend/demo/src/actions/messages/index.ts b/frontend/demo/src/actions/messages/index.ts index 1ddd704f36..5ad971f9b6 100644 --- a/frontend/demo/src/actions/messages/index.ts +++ b/frontend/demo/src/actions/messages/index.ts @@ -1,6 +1,7 @@ import {Dispatch} from 'redux'; import {createAction} from 'typesafe-actions'; -import {HttpClient, Message, ResponseMetadataPayload} from 'httpclient'; +import {Message, ResponseMetadataPayload} from 'httpclient'; +import {HttpClientInstance} from '../../InitializeAiryApi'; export const MESSAGES_LOADING = '@@messages/LOADING'; @@ -11,7 +12,7 @@ export const loadingMessagesAction = createAction( export function listMessages(conversationId: string) { return async (dispatch: Dispatch) => { - return HttpClient.listMessages({ + return HttpClientInstance.listMessages({ conversationId, pageSize: 10, }) diff --git a/frontend/demo/src/actions/tags/index.tsx b/frontend/demo/src/actions/tags/index.tsx index da4b57a8c6..c9c23def20 100644 --- a/frontend/demo/src/actions/tags/index.tsx +++ b/frontend/demo/src/actions/tags/index.tsx @@ -1,7 +1,8 @@ import _, {Dispatch} from 'redux'; import {createAction} from 'typesafe-actions'; -import {HttpClient, Tag, CreateTagRequestPayload} from 'httpclient'; +import {Tag, CreateTagRequestPayload} from 'httpclient'; +import {HttpClientInstance} from '../../InitializeAiryApi'; const UPSERT_TAG = 'UPSERT_TAG'; const DELETE_TAG = 'DELETE_TAG'; @@ -19,7 +20,7 @@ export const errorTagAction = createAction(ERROR_TAG, resolve => (status: string export function listTags() { return function(dispatch: Dispatch) { - return HttpClient.listTags().then((response: Tag[]) => { + return HttpClientInstance.listTags().then((response: Tag[]) => { dispatch(fetchTagAction(response)); }); }; @@ -27,7 +28,7 @@ export function listTags() { export function createTag(requestPayload: CreateTagRequestPayload) { return async (dispatch: Dispatch) => { - return HttpClient.createTag(requestPayload) + return HttpClientInstance.createTag(requestPayload) .then((response: Tag) => { dispatch(addTagAction(response)); return Promise.resolve(true); @@ -41,13 +42,13 @@ export function createTag(requestPayload: CreateTagRequestPayload) { export function updateTag(tag: Tag) { return function(dispatch: Dispatch) { - HttpClient.updateTag(tag).then(() => dispatch(editTagAction(tag))); + HttpClientInstance.updateTag(tag).then(() => dispatch(editTagAction(tag))); }; } export function deleteTag(id: string) { return function(dispatch: Dispatch) { - HttpClient.deleteTag(id).then(() => { + HttpClientInstance.deleteTag(id).then(() => { dispatch(deleteTagAction(id)); }); }; diff --git a/frontend/demo/src/actions/user/index.ts b/frontend/demo/src/actions/user/index.ts index 614d0d21ba..bc36ebe78a 100644 --- a/frontend/demo/src/actions/user/index.ts +++ b/frontend/demo/src/actions/user/index.ts @@ -1,7 +1,8 @@ import {createAction} from 'typesafe-actions'; import _, {Dispatch} from 'redux'; -import {User, HttpClient, LoginViaEmailRequestPayload} from 'httpclient'; +import {User, LoginViaEmailRequestPayload} from 'httpclient'; +import {HttpClientInstance} from '../../InitializeAiryApi'; const SET_CURRENT_USER = '@@auth/SET_CURRENT_USER'; const USER_AUTH_ERROR = '@@auth/ERROR'; @@ -13,7 +14,7 @@ export const logoutUserAction = createAction(USER_LOGOUT); export function loginViaEmail(requestPayload: LoginViaEmailRequestPayload) { return async (dispatch: Dispatch) => { - return HttpClient.loginViaEmail(requestPayload) + return HttpClientInstance.loginViaEmail(requestPayload) .then((response: User) => { dispatch(setCurrentUserAction(response)); return Promise.resolve(true); diff --git a/lib/typescript/httpclient/api/cookie.ts b/frontend/demo/src/cookies/cookie.ts similarity index 100% rename from lib/typescript/httpclient/api/cookie.ts rename to frontend/demo/src/cookies/cookie.ts diff --git a/lib/typescript/httpclient/api/index.ts b/frontend/demo/src/cookies/index.ts similarity index 64% rename from lib/typescript/httpclient/api/index.ts rename to frontend/demo/src/cookies/index.ts index a5b1f40baf..edadca8f5e 100644 --- a/lib/typescript/httpclient/api/index.ts +++ b/frontend/demo/src/cookies/index.ts @@ -1,3 +1,2 @@ -export * from './airyConfig'; export * from './cookie'; export * from './webStore'; diff --git a/lib/typescript/httpclient/api/webStore.ts b/frontend/demo/src/cookies/webStore.ts similarity index 97% rename from lib/typescript/httpclient/api/webStore.ts rename to frontend/demo/src/cookies/webStore.ts index 24e5b10f43..012f7d6e59 100644 --- a/lib/typescript/httpclient/api/webStore.ts +++ b/frontend/demo/src/cookies/webStore.ts @@ -1,5 +1,5 @@ import {getCookie, setCookie} from './cookie'; -import {User} from '../model/User'; +import {User} from 'httpclient'; export const storeDomainCookie = (key: string) => (token: string) => { setCookie(key, token, document.domain); diff --git a/frontend/demo/src/pages/Channels/index.tsx b/frontend/demo/src/pages/Channels/index.tsx index f345a5310e..cc6f62b26f 100644 --- a/frontend/demo/src/pages/Channels/index.tsx +++ b/frontend/demo/src/pages/Channels/index.tsx @@ -5,7 +5,8 @@ import {RouteComponentProps} from 'react-router-dom'; import FacebookLogin from 'react-facebook-login'; import {Button} from '@airyhq/components'; -import {AiryConfig, Channel} from 'httpclient'; +import {Channel} from 'httpclient'; +import {AiryConfig} from '../../AiryConfig'; import {listChannels, exploreChannels, connectChannel, disconnectChannel} from '../../actions/channel'; import {StateModel} from '../../reducers/index'; diff --git a/frontend/demo/src/reducers/data/user/index.ts b/frontend/demo/src/reducers/data/user/index.ts index 1db881fbab..8d21abd4e3 100644 --- a/frontend/demo/src/reducers/data/user/index.ts +++ b/frontend/demo/src/reducers/data/user/index.ts @@ -1,6 +1,7 @@ import {ActionType, getType} from 'typesafe-actions'; import * as actions from '../../../actions/user'; -import {getUserFromStore, storeUserData, User} from 'httpclient'; +import {User} from 'httpclient'; +import {getUserFromStore, storeUserData} from '../../../cookies'; type Action = ActionType; diff --git a/frontend/demo/src/reducers/index.ts b/frontend/demo/src/reducers/index.ts index 3a874c6e92..c86d3f9c02 100644 --- a/frontend/demo/src/reducers/index.ts +++ b/frontend/demo/src/reducers/index.ts @@ -3,7 +3,7 @@ import {ActionType, getType} from 'typesafe-actions'; import _, {CombinedState} from 'redux'; import * as authActions from '../actions/user'; -import {clearUserData} from 'httpclient'; +import {clearUserData} from '../cookies'; import data, {DataState} from './data'; diff --git a/lib/typescript/httpclient/README.md b/lib/typescript/httpclient/README.md index b90afc4eb7..44d4f4c88a 100644 --- a/lib/typescript/httpclient/README.md +++ b/lib/typescript/httpclient/README.md @@ -1,23 +1,22 @@ ### HttpClient Library -The HttpClient Library includes helper functions for using Airy's endpoints on the frontend. - -Each function performs an http request and returns a promise. - -To use the library's functions, import the library and call the module's methods. +The HttpClient Library includes a HTTP client for making requests to Airy's API. +The library exports a HttpClient class. To use the library, you need to instantiate the class with the authentification token and your api url. Both the authentification token and api url are optional (the default api url is "http://api.airy"), but communication with the endpoints always requires a token, except for /users.login and /users.signup endpoints. For example: ``` import { HttpClient} from 'httpclient'; -HttpClient.listChannels() +const myInstance = new HttpClient(authtoken, apiUrl); + +myInstance.listChannels() ``` -Here is a list of the functions it includes: +Here is a list of the public methods the library's class includes: CHANNELS - listChannels @@ -27,6 +26,10 @@ CHANNELS CONVERSATIONS - listConversations +- readConversations + +MESSAGES +- listMessages TAGS - listTags diff --git a/lib/typescript/httpclient/api/airyConfig.ts b/lib/typescript/httpclient/api/airyConfig.ts deleted file mode 100644 index 182ae21ccd..0000000000 --- a/lib/typescript/httpclient/api/airyConfig.ts +++ /dev/null @@ -1,58 +0,0 @@ -import {getAuthToken} from './webStore'; - -export class AiryConfig { - static API_URL = 'http://api.airy'; - static NODE_ENV = process.env.NODE_ENV; - static FACEBOOK_APP_ID = 'CHANGE_ME'; -} - -const headers = { - Accept: 'application/json', -}; - -export const doFetchFromBackend = async (url: string, body?: Object): Promise => { - const token = getAuthToken(); - if (token) { - headers['Authorization'] = token; - } - - if (!(body instanceof FormData)) { - if (!isString(body)) { - body = JSON.stringify(body); - } - headers['Content-Type'] = 'application/json'; - } - - const response: Response = await fetch(`${AiryConfig.API_URL}/${url}`, { - method: 'POST', - headers: headers, - body: body as BodyInit, - }); - - return parseBody(response); -}; - -async function parseBody(response: Response): Promise { - if (response.ok) { - try { - return await response.json(); - } catch {} - } - - let body = await response.text(); - - if (body.length > 0) { - body = JSON.parse(body); - } - - const errorResponse = { - status: response.status, - body: body, - }; - - throw errorResponse; -} - -function isString(object: any) { - return typeof object === 'string' || object instanceof String; -} diff --git a/lib/typescript/httpclient/endpoints/connectChannel.ts b/lib/typescript/httpclient/endpoints/connectChannel.ts deleted file mode 100644 index abf59ac2b2..0000000000 --- a/lib/typescript/httpclient/endpoints/connectChannel.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {doFetchFromBackend} from '../api'; -import {ConnectChannelRequestPayload} from '../payload'; -import {ChannelApiPayload} from '../payload/ChannelApiPayload'; -import {connectChannelApiMapper} from '../mappers/connectChannelApiMapper'; -import {channelMapper} from '../mappers/channelMapper'; - -export function connectChannel(requestPayload: ConnectChannelRequestPayload) { - return doFetchFromBackend('channels.connect', connectChannelApiMapper(requestPayload)) - .then((response: ChannelApiPayload) => { - const channel = channelMapper(response); - return channel; - }) - .catch((error: Error) => { - return error; - }); -} diff --git a/lib/typescript/httpclient/endpoints/createTag.ts b/lib/typescript/httpclient/endpoints/createTag.ts deleted file mode 100644 index 2313504601..0000000000 --- a/lib/typescript/httpclient/endpoints/createTag.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {doFetchFromBackend} from '../api'; -import {Tag, TagColor} from '../model'; -import {CreateTagRequestPayload} from '../payload'; -import {TagPayload} from '../payload/TagPayload'; - -export function createTag(requestPayload: CreateTagRequestPayload) { - return doFetchFromBackend('tags.create', requestPayload) - .then((response: TagPayload) => { - const tag: Tag = { - id: response.id, - name: requestPayload.name, - color: requestPayload.color as TagColor, - }; - return tag; - }) - .catch((error: Error) => { - return error; - }); -} diff --git a/lib/typescript/httpclient/endpoints/deleteTag.ts b/lib/typescript/httpclient/endpoints/deleteTag.ts deleted file mode 100644 index 72abdf31d9..0000000000 --- a/lib/typescript/httpclient/endpoints/deleteTag.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {doFetchFromBackend} from '../api'; - -export function deleteTag(id: string) { - return doFetchFromBackend('tags.delete', { - id, - }) - .then(() => Promise.resolve(true)) - .catch((error: Error) => Promise.reject(error)); -} diff --git a/lib/typescript/httpclient/endpoints/disconnectChannel.ts b/lib/typescript/httpclient/endpoints/disconnectChannel.ts deleted file mode 100644 index 95403312be..0000000000 --- a/lib/typescript/httpclient/endpoints/disconnectChannel.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {doFetchFromBackend} from '../api'; -import {DisconnectChannelRequestPayload} from '../payload'; -import {ChannelsPayload} from '../payload/ChannelsPayload'; -import {channelsMapper} from '../mappers/channelsMapper'; -import {disconnectChannelApiMapper} from '../mappers/disconnectChannelApiMapper'; - -export function disconnectChannel(requestPayload: DisconnectChannelRequestPayload) { - return doFetchFromBackend('channels.disconnect', disconnectChannelApiMapper(requestPayload)) - .then((response: ChannelsPayload) => { - const channels = channelsMapper(response); - return channels; - }) - .catch((error: Error) => { - return error; - }); -} diff --git a/lib/typescript/httpclient/endpoints/exploreChannels.ts b/lib/typescript/httpclient/endpoints/exploreChannels.ts deleted file mode 100644 index e111e5686c..0000000000 --- a/lib/typescript/httpclient/endpoints/exploreChannels.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {doFetchFromBackend} from '../api'; -import {ExploreChannelRequestPayload} from '../payload'; -import {ChannelsPayload} from '../payload/ChannelsPayload'; -import {channelsMapper} from '../mappers/channelsMapper'; - -export function exploreChannels(requestPayload: ExploreChannelRequestPayload) { - return doFetchFromBackend('channels.explore', requestPayload) - .then((response: ChannelsPayload) => { - const channels = channelsMapper(response, requestPayload.source); - return channels; - }) - .catch((error: Error) => { - return error; - }); -} diff --git a/lib/typescript/httpclient/endpoints/index.ts b/lib/typescript/httpclient/endpoints/index.ts deleted file mode 100644 index cbb6ec6a16..0000000000 --- a/lib/typescript/httpclient/endpoints/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -export * from './listChannels'; -export * from './exploreChannels'; -export * from './connectChannel'; -export * from './disconnectChannel'; -export * from './listConversations'; -export * from './listMessages'; -export * from './listTags'; -export * from './createTag'; -export * from './updateTag'; -export * from './deleteTag'; -export * from './loginViaEmail'; -export * from './readConversations'; diff --git a/lib/typescript/httpclient/endpoints/listChannels.ts b/lib/typescript/httpclient/endpoints/listChannels.ts deleted file mode 100644 index 90f3a0563a..0000000000 --- a/lib/typescript/httpclient/endpoints/listChannels.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {doFetchFromBackend} from '../api'; -import {ChannelsPayload} from '../payload/ChannelsPayload'; -import {channelsMapper} from '../mappers/channelsMapper'; - -export function listChannels() { - return doFetchFromBackend('channels.list') - .then((response: ChannelsPayload) => { - const channels = channelsMapper(response); - return channels; - }) - .catch((error: Error) => { - return error; - }); -} diff --git a/lib/typescript/httpclient/endpoints/listConversations.ts b/lib/typescript/httpclient/endpoints/listConversations.ts deleted file mode 100644 index ca829e6708..0000000000 --- a/lib/typescript/httpclient/endpoints/listConversations.ts +++ /dev/null @@ -1,19 +0,0 @@ -import {doFetchFromBackend} from '../api'; -import {ListConversationsRequestPayload} from '../payload'; -import {ConversationPayload} from '../payload/ConversationPayload'; -import {PaginatedPayload} from '../payload/PaginatedPayload'; -import {conversationsMapper} from '../mappers/conversationsMapper'; - -export function listConversations(conversationListRequest: ListConversationsRequestPayload) { - conversationListRequest.page_size = conversationListRequest.page_size ?? 10; - conversationListRequest.cursor = conversationListRequest.cursor ?? null; - - return doFetchFromBackend('conversations.list', conversationListRequest) - .then((response: PaginatedPayload) => { - const {responseMetadata} = response; - return {data: conversationsMapper(response.data), metadata: responseMetadata}; - }) - .catch((error: Error) => { - return error; - }); -} diff --git a/lib/typescript/httpclient/endpoints/listMessages.ts b/lib/typescript/httpclient/endpoints/listMessages.ts deleted file mode 100644 index 162eafc809..0000000000 --- a/lib/typescript/httpclient/endpoints/listMessages.ts +++ /dev/null @@ -1,23 +0,0 @@ -import {doFetchFromBackend} from '../api'; -import {ListMessagesRequestPayload} from '../payload/ListMessagesRequestPayload'; -import {MessagePayload} from '../payload/MessagePayload'; -import {PaginatedPayload} from '../payload/PaginatedPayload'; -import {messageMapperData} from '../mappers/messageMapperData'; - -export function listMessages(conversationListRequest: ListMessagesRequestPayload) { - conversationListRequest.pageSize = conversationListRequest.pageSize ?? 10; - conversationListRequest.cursor = conversationListRequest.cursor ?? null; - - return doFetchFromBackend('messages.list', { - conversation_id: conversationListRequest.conversationId, - cursor: conversationListRequest.cursor, - page_size: conversationListRequest.pageSize, - }) - .then((response: PaginatedPayload) => { - const {responseMetadata} = response; - return {data: messageMapperData(response), metadata: responseMetadata}; - }) - .catch((error: Error) => { - return error; - }); -} diff --git a/lib/typescript/httpclient/endpoints/listTags.ts b/lib/typescript/httpclient/endpoints/listTags.ts deleted file mode 100644 index 1178a49e9f..0000000000 --- a/lib/typescript/httpclient/endpoints/listTags.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {doFetchFromBackend} from '../api'; -import {ListTagsResponsePayload} from '../payload'; -import {tagsMapper} from '../mappers/tagsMapper'; - -export function listTags() { - return doFetchFromBackend('tags.list') - .then((response: ListTagsResponsePayload) => { - return tagsMapper(response.data); - }) - .catch((error: Error) => { - return error; - }); -} diff --git a/lib/typescript/httpclient/endpoints/loginViaEmail.ts b/lib/typescript/httpclient/endpoints/loginViaEmail.ts deleted file mode 100644 index 9d99522e35..0000000000 --- a/lib/typescript/httpclient/endpoints/loginViaEmail.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {doFetchFromBackend} from '../api'; -import {LoginViaEmailRequestPayload} from '../payload'; -import {UserPayload} from '../payload/UserPayload'; -import {userMapper} from '../mappers/userMapper'; - -export function loginViaEmail(requestPayload: LoginViaEmailRequestPayload) { - return doFetchFromBackend('users.login', requestPayload) - .then((response: UserPayload) => { - return userMapper(response); - }) - .catch((error: Error) => { - return error; - }); -} diff --git a/lib/typescript/httpclient/endpoints/readConversations.ts b/lib/typescript/httpclient/endpoints/readConversations.ts deleted file mode 100644 index 21906acc80..0000000000 --- a/lib/typescript/httpclient/endpoints/readConversations.ts +++ /dev/null @@ -1,5 +0,0 @@ -import {doFetchFromBackend} from '../api'; - -export function readConversations(conversationId: string) { - return doFetchFromBackend('conversations.read', {conversation_id: conversationId}).then(() => Promise.resolve(true)); -} diff --git a/lib/typescript/httpclient/endpoints/updateTag.ts b/lib/typescript/httpclient/endpoints/updateTag.ts deleted file mode 100644 index 033a2e6e23..0000000000 --- a/lib/typescript/httpclient/endpoints/updateTag.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {doFetchFromBackend} from '../api'; -import {Tag} from '../model'; - -export function updateTag(tag: Tag) { - return doFetchFromBackend('tags.update', {...tag}) - .then(() => Promise.resolve(true)) - .catch((error: Error) => Promise.reject(error)); -} diff --git a/lib/typescript/httpclient/index.ts b/lib/typescript/httpclient/index.ts index 5ab208181a..68480d4f47 100644 --- a/lib/typescript/httpclient/index.ts +++ b/lib/typescript/httpclient/index.ts @@ -1,36 +1,215 @@ +import {ChannelsPayload} from './payload/ChannelsPayload'; +import {channelsMapper} from './mappers/channelsMapper'; import { - listChannels, - exploreChannels, - connectChannel, - disconnectChannel, - listConversations, - readConversations, - listMessages, - listTags, - createTag, - updateTag, - deleteTag, - loginViaEmail, -} from './endpoints'; - -export const HttpClient = (function() { - return { - listChannels: listChannels, - exploreChannels: exploreChannels, - connectChannel: connectChannel, - disconnectChannel: disconnectChannel, - listConversations: listConversations, - readConversations: readConversations, - listMessages: listMessages, - listTags: listTags, - createTag: createTag, - updateTag: updateTag, - deleteTag: deleteTag, - loginViaEmail: loginViaEmail, + ExploreChannelRequestPayload, + ConnectChannelRequestPayload, + DisconnectChannelRequestPayload, + ListConversationsRequestPayload, + ListTagsResponsePayload, + CreateTagRequestPayload, + LoginViaEmailRequestPayload, +} from './payload'; +import {ChannelApiPayload} from './payload/ChannelApiPayload'; +import {connectChannelApiMapper} from './mappers/connectChannelApiMapper'; +import {channelMapper} from './mappers/channelMapper'; +import {disconnectChannelApiMapper} from './mappers/disconnectChannelApiMapper'; +import {ConversationPayload} from './payload/ConversationPayload'; +import {PaginatedPayload} from './payload/PaginatedPayload'; +import {conversationsMapper} from './mappers/conversationsMapper'; +import {ListMessagesRequestPayload} from './payload/ListMessagesRequestPayload'; +import {MessagePayload} from './payload/MessagePayload'; +import {messageMapperData} from './mappers/messageMapperData'; +import {tagsMapper} from './mappers/tagsMapper'; +import {TagColor, Tag} from './model'; +import {TagPayload} from './payload/TagPayload'; +import {userMapper} from './mappers/userMapper'; + +const headers = { + Accept: 'application/json', +}; + +export async function parseBody(response: Response): Promise { + if (response.ok) { + try { + return await response.json(); + } catch {} + } + + let body = await response.text(); + + if (body.length > 0) { + body = JSON.parse(body); + } + + const errorResponse = { + status: response.status, + body: body, }; -})(); -export * from './api'; + throw errorResponse; +} + +export function isString(object: any) { + return typeof object === 'string' || object instanceof String; +} + +export class HttpClient { + public readonly token?: string; + public readonly apiUrlConfig?: string; + + constructor(token?: string, apiUrlConfig?: string) { + this.token = token; + this.apiUrlConfig = apiUrlConfig || 'http://api.airy'; + } + + private async doFetchFromBackend(url: string, body?: Object): Promise { + if (this.token) { + headers['Authorization'] = this.token; + } + if (!(body instanceof FormData)) { + if (!isString(body)) { + body = JSON.stringify(body); + } + headers['Content-Type'] = 'application/json'; + } + + const response: Response = await fetch(`${this.apiUrlConfig}/${url}`, { + method: 'POST', + headers: headers, + body: body as BodyInit, + }); + + return parseBody(response); + } + + public async listChannels() { + try { + const response: ChannelsPayload = await this.doFetchFromBackend('channels.list'); + return channelsMapper(response); + } catch (error) { + return error; + } + } + + public async exploreChannels(requestPayload: ExploreChannelRequestPayload) { + try { + const response: ChannelsPayload = await this.doFetchFromBackend('channels.explore', requestPayload); + return channelsMapper(response, requestPayload.source); + } catch (error) { + return error; + } + } + + public async connectChannel(requestPayload: ConnectChannelRequestPayload) { + try { + const response: ChannelApiPayload = await this.doFetchFromBackend( + 'channels.connect', + connectChannelApiMapper(requestPayload) + ); + return channelMapper(response); + } catch (error) { + return error; + } + } + + public async disconnectChannel(requestPayload: DisconnectChannelRequestPayload) { + try { + const response: ChannelsPayload = await this.doFetchFromBackend( + 'channels.disconnect', + disconnectChannelApiMapper(requestPayload) + ); + return channelsMapper(response); + } catch (error) { + return error; + } + } + + public async listConversations(conversationListRequest: ListConversationsRequestPayload) { + conversationListRequest.page_size = conversationListRequest.page_size ?? 10; + conversationListRequest.cursor = conversationListRequest.cursor ?? null; + try { + const response: PaginatedPayload = await this.doFetchFromBackend( + 'conversations.list', + conversationListRequest + ); + const {response_metadata} = response; + return {data: conversationsMapper(response.data), metadata: response_metadata}; + } catch (error) { + return error; + } + } + + public async readConversations(conversationId: string) { + const response = await this.doFetchFromBackend('conversations.read', {conversation_id: conversationId}); + return Promise.resolve(true); + } + + public async listMessages(conversationListRequest: ListMessagesRequestPayload) { + conversationListRequest.pageSize = conversationListRequest.pageSize ?? 10; + conversationListRequest.cursor = conversationListRequest.cursor ?? null; + + try { + const response: PaginatedPayload = await this.doFetchFromBackend('messages.list', { + conversation_id: conversationListRequest.conversationId, + cursor: conversationListRequest.cursor, + page_size: conversationListRequest.pageSize, + }); + const {response_metadata} = response; + return {data: messageMapperData(response), metadata: response_metadata}; + } catch (error) { + return error; + } + } + + public async listTags() { + try { + const response: ListTagsResponsePayload = await this.doFetchFromBackend('tags.list'); + return tagsMapper(response.data); + } catch (error) { + return error; + } + } + + public async createTag(requestPayload: CreateTagRequestPayload) { + try { + const response: TagPayload = await this.doFetchFromBackend('tags.create', requestPayload); + return { + id: response.id, + name: requestPayload.name, + color: requestPayload.color as TagColor, + }; + } catch (error) { + return error; + } + } + + public async updateTag(tag: Tag) { + try { + const response = await this.doFetchFromBackend('tags.update', {...tag}); + return Promise.resolve(true); + } catch (error) { + return error; + } + } + + public async deleteTag(id: string) { + try { + const response = await this.doFetchFromBackend('tags.delete', {id}); + return Promise.resolve(true); + } catch (error) { + return error; + } + } + + public async loginViaEmail(requestPayload: LoginViaEmailRequestPayload) { + try { + const response = await this.doFetchFromBackend('users.login', requestPayload); + return userMapper(response); + } catch (error) { + return error; + } + } +} + export * from './model'; -export * from './endpoints'; export * from './payload'; diff --git a/lib/typescript/httpclient/payload/PaginatedPayload.ts b/lib/typescript/httpclient/payload/PaginatedPayload.ts index 327378568f..4afb6d42a5 100644 --- a/lib/typescript/httpclient/payload/PaginatedPayload.ts +++ b/lib/typescript/httpclient/payload/PaginatedPayload.ts @@ -1,4 +1,4 @@ export interface PaginatedPayload { data: T[]; - responseMetadata: {previousCursor: string; nextCursor: string; total: number}; + response_metadata: {previous_cursor: string; next_cursor: string; total: number}; } From c6c0701a2c7bb4aec9635c88247bce11b7caed62 Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Mon, 18 Jan 2021 10:24:29 +0100 Subject: [PATCH 14/56] Remove .bazelproject from vcs since the config depends on your IJ payment status (#666) --- .gitignore | 1 - .ijwb/.bazelproject | 24 ------------------------ 2 files changed, 25 deletions(-) delete mode 100644 .ijwb/.bazelproject diff --git a/.gitignore b/.gitignore index ef4f7e8db8..91a60c8ca6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ # Editors and IDE's -!.ijwb/.bazelproject !.ijwb/.idea/fileTemplates/ .idea/ diff --git a/.ijwb/.bazelproject b/.ijwb/.bazelproject deleted file mode 100644 index 3d7333f420..0000000000 --- a/.ijwb/.bazelproject +++ /dev/null @@ -1,24 +0,0 @@ -directories: - # Add the directories you want added as source here - # By default, we've added your entire workspace ('.') - . - -# Automatically includes all relevant targets under the 'directories' above -derive_targets_from_directories: true - -targets: - # If source code isn't resolving, add additional targets that compile it here - -additional_languages: - # Uncomment any additional languages you want supported - # android - # dart - # go - javascript - # kotlin - python - # scala - typescript - -ts_config_rules: - //frontend/chat-plugin:widget_tsconfig From fe07b377f535d308dfbfe750c0d5b1de4e0a38e8 Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Mon, 18 Jan 2021 10:33:29 +0100 Subject: [PATCH 15/56] [#646] Configurable ingress hostnames (#648) --- docs/docs/guides/airy-core-in-production.md | 9 ++++----- infrastructure/helm-chart/charts/ingress/Chart.yaml | 5 +++++ .../charts/ingress/templates}/ingress.yaml | 7 ++++--- infrastructure/helm-chart/charts/ingress/values.yaml | 4 ++++ infrastructure/scripts/conf.sh | 2 -- infrastructure/scripts/provision/core.sh | 3 --- infrastructure/scripts/status.sh | 7 ++++--- 7 files changed, 21 insertions(+), 16 deletions(-) create mode 100644 infrastructure/helm-chart/charts/ingress/Chart.yaml rename infrastructure/{network => helm-chart/charts/ingress/templates}/ingress.yaml (97%) create mode 100644 infrastructure/helm-chart/charts/ingress/values.yaml diff --git a/docs/docs/guides/airy-core-in-production.md b/docs/docs/guides/airy-core-in-production.md index 4b7f3b95a8..3fe2d405b8 100644 --- a/docs/docs/guides/airy-core-in-production.md +++ b/docs/docs/guides/airy-core-in-production.md @@ -268,21 +268,20 @@ Ingress resources. You can choose an [Kubernetes ingress controller](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/) in accordance to your needs or preferences. If you are using the [Traefik](https://traefik.io/) ingress controller, you can edit the -`infrastructure/network/ingress.yaml` file to modify the `host` records and -directly apply it to your Kubernetes cluster. +`infrastructure/helm-chart/charts/ingress/templates/ingress.yaml` file to modify the `host` records and apply the ingress helm chart, which is already included in the repository: ```sh -kubectl apply -f infrastructure/network/ingress.yaml +helm install ingress infrastructure/helm-chart/charts/ingress/ ``` -You must set different `host` attributes for the following: +You must set appropriate `host` attributes in the rules for: - API endpoints (defaults to `api.airy`) - Demo (defaults to `demo.airy`) - Chat plugin (defaults to `chatplugin.airy`) If you are not using Traefik, you can use the -`infrastructure/network/ingress.yaml` file as a guide to create your own +`infrastructure/helm-chart/charts/ingress/templates/ingress.yaml` file as a guide to create your own Kubernetes manifest for your preferred ingress controller. If your Kubernetes cluster is not directly reachable on the Internet, you will diff --git a/infrastructure/helm-chart/charts/ingress/Chart.yaml b/infrastructure/helm-chart/charts/ingress/Chart.yaml new file mode 100644 index 0000000000..01e96eb5fc --- /dev/null +++ b/infrastructure/helm-chart/charts/ingress/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for the Traefik ingress controller +name: ingress +version: 0.1.0 diff --git a/infrastructure/network/ingress.yaml b/infrastructure/helm-chart/charts/ingress/templates/ingress.yaml similarity index 97% rename from infrastructure/network/ingress.yaml rename to infrastructure/helm-chart/charts/ingress/templates/ingress.yaml index bce0ce57c2..baefc9688e 100644 --- a/infrastructure/network/ingress.yaml +++ b/infrastructure/helm-chart/charts/ingress/templates/ingress.yaml @@ -2,9 +2,10 @@ kind: Ingress apiVersion: networking.k8s.io/v1 metadata: name: 'airy-core' + namespace: {{ .Values.global.namespace }} spec: rules: - - host: 'api.airy' + - host: {{ .Values.apiHost }} http: paths: - path: /users.login @@ -210,7 +211,7 @@ spec: name: sources-twilio-connector port: number: 80 - - host: 'demo.airy' + - host: {{ .Values.uiHost }} http: paths: - path: / @@ -220,7 +221,7 @@ spec: name: frontend-demo port: number: 80 - - host: 'chatplugin.airy' + - host: {{ .Values.chatpluginHost }} http: paths: - path: /ws.chatplugin diff --git a/infrastructure/helm-chart/charts/ingress/values.yaml b/infrastructure/helm-chart/charts/ingress/values.yaml new file mode 100644 index 0000000000..7dce025b09 --- /dev/null +++ b/infrastructure/helm-chart/charts/ingress/values.yaml @@ -0,0 +1,4 @@ + +apiHost: "api.airy" +uiHost: "demo.airy" +chatpluginHost: "chatplugin.airy" diff --git a/infrastructure/scripts/conf.sh b/infrastructure/scripts/conf.sh index 56895db89a..b9f977c6bd 100755 --- a/infrastructure/scripts/conf.sh +++ b/infrastructure/scripts/conf.sh @@ -10,8 +10,6 @@ if [[ ! -f ${INFRASTRUCTURE_PATH}/airy.yaml ]]; then exit 0 fi -source /vagrant/scripts/lib/k8s.sh - source ${INFRASTRUCTURE_PATH}/scripts/lib/k8s.sh diff --git a/infrastructure/scripts/provision/core.sh b/infrastructure/scripts/provision/core.sh index eebd857336..159e2d9bba 100755 --- a/infrastructure/scripts/provision/core.sh +++ b/infrastructure/scripts/provision/core.sh @@ -34,6 +34,3 @@ wait-for-service startup-helper postgres 5432 10 Postgres kubectl scale statefulset redis-cluster --replicas=1 wait-for-service startup-helper redis-cluster 6379 10 Redis kubectl delete pod startup-helper --force 2>/dev/null - -echo "Deploying ingress controller" -kubectl apply -f ../network/ingress.yaml diff --git a/infrastructure/scripts/status.sh b/infrastructure/scripts/status.sh index 062259b724..7b743dd9a9 100755 --- a/infrastructure/scripts/status.sh +++ b/infrastructure/scripts/status.sh @@ -14,7 +14,8 @@ wait-for-ingress-service wait-for-running-pod startup-helper wait-for-service startup-helper api-auth 80 10 api-auth -CORE_ID=`kubectl get configmap core-config -o jsonpath='{.data.CORE_ID}'` +CORE_ID=$(kubectl get configmap core-config -o jsonpath='{.data.CORE_ID}') +API_HOSTNAME=$(kubectl get ingress airy-core -o jsonpath='{.spec.rules[0].host}') FACEBOOK_WEBHOOK_PUBLIC_URL="https://fb-${CORE_ID}.tunnel.airy.co" GOOGLE_WEBHOOK_PUBLIC_URL="https://gl-${CORE_ID}.tunnel.airy.co" TWILIO_WEBHOOK_PUBLIC_URL="https://tw-${CORE_ID}.tunnel.airy.co" @@ -30,9 +31,9 @@ echo "Your public url for the Twilio Webhook is:" echo ${TWILIO_WEBHOOK_PUBLIC_URL}/twilio echo echo "You can access the API of the Airy Core Platform at:" -echo "http://api.airy/" +echo "http://${API_HOSTNAME}" echo echo "Example:" -echo "curl -X POST -H 'Content-Type: application/json' -d '{\"first_name\": \"Grace\",\"last_name\": \"Hopper\",\"password\": \"the_answer_is_42\",\"email\": \"grace@example.com\"}' http://api.airy/users.signup" +echo "curl -X POST -H 'Content-Type: application/json' -d '{\"first_name\": \"Grace\",\"last_name\": \"Hopper\",\"password\": \"the_answer_is_42\",\"email\": \"grace@example.com\"}' http://${API_HOSTNAME}/users.signup" kubectl delete pod startup-helper --force 2>/dev/null From 56e2dedb1e8ce5737c3517898019021fca5726bb Mon Sep 17 00:00:00 2001 From: lucapette Date: Mon, 18 Jan 2021 14:14:11 +0100 Subject: [PATCH 16/56] [#421] Introduce /client.config (#668) Fixes #421 --- .../payload/ClientConfigResponsePayload.java | 18 +++ .../api/config/ClientConfigController.java | 43 ++++++++ .../core/api/config/ServiceDiscovery.java | 26 +++++ .../src/main/resources/application.properties | 4 +- .../api/admin/ClientConfigControllerTest.java | 103 ++++++++++++++++++ .../admin/src/test/resources/test.properties | 3 +- backend/sources/facebook/connector/BUILD | 1 + .../sources/facebook/ChannelsController.java | 9 +- .../co/airy/core/sources/facebook/Stores.java | 10 +- backend/sources/google/connector/BUILD | 1 + .../co/airy/core/sources/google/Stores.java | 12 +- backend/sources/twilio/connector/BUILD | 1 + .../sources/twilio/ChannelsController.java | 3 + .../co/airy/core/sources/twilio/Stores.java | 13 ++- infrastructure/cli/cmd/root.go | 14 +-- .../api-admin/templates/deployment.yaml | 4 + .../templates/deployment.yaml | 10 +- .../templates/deployment.yaml | 10 +- .../templates/deployment.yaml | 10 +- .../charts/ingress/templates/ingress.yaml | 7 ++ 20 files changed, 269 insertions(+), 33 deletions(-) create mode 100644 backend/api/admin/src/main/java/co/airy/core/api/admin/payload/ClientConfigResponsePayload.java create mode 100644 backend/api/admin/src/main/java/co/airy/core/api/config/ClientConfigController.java create mode 100644 backend/api/admin/src/main/java/co/airy/core/api/config/ServiceDiscovery.java create mode 100644 backend/api/admin/src/test/java/co/airy/core/api/admin/ClientConfigControllerTest.java diff --git a/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/ClientConfigResponsePayload.java b/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/ClientConfigResponsePayload.java new file mode 100644 index 0000000000..5002cb7b3a --- /dev/null +++ b/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/ClientConfigResponsePayload.java @@ -0,0 +1,18 @@ +package co.airy.core.api.admin.payload; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ClientConfigResponsePayload { + private List>> components; + private List> features; +} diff --git a/backend/api/admin/src/main/java/co/airy/core/api/config/ClientConfigController.java b/backend/api/admin/src/main/java/co/airy/core/api/config/ClientConfigController.java new file mode 100644 index 0000000000..da1ff2047f --- /dev/null +++ b/backend/api/admin/src/main/java/co/airy/core/api/config/ClientConfigController.java @@ -0,0 +1,43 @@ +package co.airy.core.api.config; + +import co.airy.core.api.admin.payload.ClientConfigResponsePayload; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@RestController +public class ClientConfigController { + + private final ServiceDiscovery serviceDiscovery; + private final String namespace; + private final RestTemplate restTemplate; + + public ClientConfigController(ServiceDiscovery serviceDiscovery, @Value("${kubernetes.namespace}") String namespace, RestTemplate restTemplate) { + this.serviceDiscovery = serviceDiscovery; + this.namespace = namespace; + this.restTemplate = restTemplate; + } + + @PostMapping("/client.config") + public ResponseEntity getConfig() { + List>> components = new ArrayList<>(); + + for (String service : serviceDiscovery.getServices()) { + ResponseEntity response = restTemplate.exchange(String.format("http://%s.%s/actuator/health", service, namespace), HttpMethod.GET, null, Object.class); + + components.add(Map.of(service.replace("-connector", ""), Map.of("enabled", Boolean.toString(response.getStatusCode().is2xxSuccessful())))); + } + + return ResponseEntity.ok(ClientConfigResponsePayload.builder() + .components(components) + .features(List.of()) + .build()); + } +} diff --git a/backend/api/admin/src/main/java/co/airy/core/api/config/ServiceDiscovery.java b/backend/api/admin/src/main/java/co/airy/core/api/config/ServiceDiscovery.java new file mode 100644 index 0000000000..219fb754f4 --- /dev/null +++ b/backend/api/admin/src/main/java/co/airy/core/api/config/ServiceDiscovery.java @@ -0,0 +1,26 @@ +package co.airy.core.api.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.util.List; + +@Component +public class ServiceDiscovery { + private static final List services = List.of( + "sources-chatplugin", + "sources-facebook-connector", + "sources-twilio-connector", + "sources-google-connector" + ); + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + + public List getServices() { + return services; + } +} diff --git a/backend/api/admin/src/main/resources/application.properties b/backend/api/admin/src/main/resources/application.properties index fbd90a5ed6..444ce06a02 100644 --- a/backend/api/admin/src/main/resources/application.properties +++ b/backend/api/admin/src/main/resources/application.properties @@ -2,7 +2,5 @@ kafka.brokers=${KAFKA_BROKERS} kafka.schema-registry-url=${KAFKA_SCHEMA_REGISTRY_URL} kafka.cleanup=${KAFKA_CLEANUP:false} kafka.commit-interval-ms=${KAFKA_COMMIT_INTERVAL_MS} - -facebook.app-id=${FACEBOOK_APP_ID} -facebook.app-secret=${FACEBOOK_APP_SECRET} auth.jwt-secret=${JWT_SECRET} +kubernetes.namespace=${KUBERNETES_NAMESPACE} \ No newline at end of file diff --git a/backend/api/admin/src/test/java/co/airy/core/api/admin/ClientConfigControllerTest.java b/backend/api/admin/src/test/java/co/airy/core/api/admin/ClientConfigControllerTest.java new file mode 100644 index 0000000000..2d57c7c6e1 --- /dev/null +++ b/backend/api/admin/src/test/java/co/airy/core/api/admin/ClientConfigControllerTest.java @@ -0,0 +1,103 @@ +package co.airy.core.api.admin; + +import co.airy.core.api.config.ClientConfigController; +import co.airy.kafka.schema.application.ApplicationCommunicationChannels; +import co.airy.kafka.schema.application.ApplicationCommunicationTags; +import co.airy.kafka.schema.application.ApplicationCommunicationWebhooks; +import co.airy.kafka.test.KafkaTestHelper; +import co.airy.kafka.test.junit.SharedKafkaTestResource; +import co.airy.spring.core.AirySpringBootApplication; +import co.airy.spring.test.WebTestHelper; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.InjectMocks; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.client.ExpectedCount; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestTemplate; + +import java.net.URI; + +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = AirySpringBootApplication.class) +@TestPropertySource(value = "classpath:test.properties") +@AutoConfigureMockMvc +@ExtendWith(SpringExtension.class) +public class ClientConfigControllerTest { + @RegisterExtension + public static final SharedKafkaTestResource sharedKafkaTestResource = new SharedKafkaTestResource(); + private static KafkaTestHelper kafkaTestHelper; + + @Autowired + private RestTemplate restTemplate; + + private MockRestServiceServer mockServer; + + @InjectMocks + private ClientConfigController configController; + + @Autowired + private WebTestHelper webTestHelper; + + private static final ApplicationCommunicationChannels applicationCommunicationChannels = new ApplicationCommunicationChannels(); + private static final ApplicationCommunicationWebhooks applicationCommunicationWebhooks = new ApplicationCommunicationWebhooks(); + private static final ApplicationCommunicationTags applicationCommunicationTags = new ApplicationCommunicationTags(); + + @BeforeAll + static void beforeAll() throws Exception { + kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, + applicationCommunicationChannels, + applicationCommunicationWebhooks, + applicationCommunicationTags + ); + kafkaTestHelper.beforeAll(); + } + + @AfterAll + static void afterAll() throws Exception { + kafkaTestHelper.afterAll(); + } + + @BeforeEach + void beforeEach() throws Exception { + webTestHelper.waitUntilHealthy(); + + mockServer = MockRestServiceServer.createServer(restTemplate); + } + + @Test + public void canReturnConfig() throws Exception { + mockServer.expect(requestTo(new URI("http://sources-chatplugin.default/actuator/health"))) + .andExpect(method(HttpMethod.GET)) + .andRespond(withStatus(HttpStatus.OK)); + + mockServer.expect(requestTo(new URI("http://sources-facebook-connector.default/actuator/health"))) + .andExpect(method(HttpMethod.GET)) + .andRespond(withStatus(HttpStatus.OK)); + + mockServer.expect(requestTo(new URI("http://sources-twilio-connector.default/actuator/health"))) + .andExpect(method(HttpMethod.GET)) + .andRespond(withStatus(HttpStatus.OK)); + + mockServer.expect(requestTo(new URI("http://sources-google-connector.default/actuator/health"))) + .andExpect(method(HttpMethod.GET)) + .andRespond(withStatus(HttpStatus.OK)); + + webTestHelper.post("/client.config", "{}", "user-id").andExpect(status().isOk()); + } + +} diff --git a/backend/api/admin/src/test/resources/test.properties b/backend/api/admin/src/test/resources/test.properties index 1ad7ec68ba..4e9295c247 100644 --- a/backend/api/admin/src/test/resources/test.properties +++ b/backend/api/admin/src/test/resources/test.properties @@ -1,5 +1,4 @@ kafka.cleanup=true kafka.commit-interval-ms=100 -facebook.app-id=1234 -facebook.app-secret=secret auth.jwt-secret=42424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242 +kubernetes.namespace=default diff --git a/backend/sources/facebook/connector/BUILD b/backend/sources/facebook/connector/BUILD index 0be43cf3bb..04704f3e58 100644 --- a/backend/sources/facebook/connector/BUILD +++ b/backend/sources/facebook/connector/BUILD @@ -4,6 +4,7 @@ load("//tools/build:container_push.bzl", "container_push") app_deps = [ "//backend:base_app", + "//:springboot_actuator", "//backend/model/channel", "//backend/model/message", "//backend/model/metadata", diff --git a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/ChannelsController.java b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/ChannelsController.java index 07f505d7c9..a5bf43b2e1 100644 --- a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/ChannelsController.java +++ b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/ChannelsController.java @@ -2,13 +2,13 @@ import co.airy.avro.communication.Channel; import co.airy.avro.communication.ChannelConnectionState; -import co.airy.core.sources.facebook.payload.PageInfoResponsePayload; -import co.airy.core.sources.facebook.payload.ConnectRequestPayload; -import co.airy.core.sources.facebook.payload.ExploreRequestPayload; -import co.airy.core.sources.facebook.payload.ExploreResponsePayload; import co.airy.core.sources.facebook.api.Api; import co.airy.core.sources.facebook.api.ApiException; import co.airy.core.sources.facebook.api.model.PageWithConnectInfo; +import co.airy.core.sources.facebook.payload.ConnectRequestPayload; +import co.airy.core.sources.facebook.payload.ExploreRequestPayload; +import co.airy.core.sources.facebook.payload.ExploreResponsePayload; +import co.airy.core.sources.facebook.payload.PageInfoResponsePayload; import co.airy.spring.web.payload.RequestErrorResponsePayload; import co.airy.uuid.UUIDv5; import org.apache.kafka.streams.state.KeyValueIterator; @@ -104,4 +104,5 @@ ResponseEntity connect(@RequestBody @Valid ConnectRequestPayload requestPaylo return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build(); } } + } diff --git a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/Stores.java b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/Stores.java index 5aaf241f4e..9baa3de7ba 100644 --- a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/Stores.java +++ b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/Stores.java @@ -24,6 +24,8 @@ import org.apache.kafka.streams.kstream.Suppressed; import org.apache.kafka.streams.state.ReadOnlyKeyValueStore; import org.springframework.beans.factory.DisposableBean; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.context.event.ApplicationStartedEvent; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Service; @@ -38,7 +40,7 @@ import static co.airy.model.metadata.MetadataRepository.isConversationMetadata; @Service -public class Stores implements ApplicationListener, DisposableBean { +public class Stores implements ApplicationListener, DisposableBean, HealthIndicator { private static final String appId = "sources.facebook.ConnectorStores"; private final KafkaStreamsWrapper streams; @@ -139,6 +141,12 @@ public void destroy() { } } + @Override + public Health health() { + getChannelsStore(); + return Health.up().build(); + } + // visible for testing KafkaStreams.State getStreamState() { return streams.state(); diff --git a/backend/sources/google/connector/BUILD b/backend/sources/google/connector/BUILD index 46e8dce898..dd317369ba 100644 --- a/backend/sources/google/connector/BUILD +++ b/backend/sources/google/connector/BUILD @@ -4,6 +4,7 @@ load("//tools/build:container_push.bzl", "container_push") app_deps = [ "//backend:base_app", + "//:springboot_actuator", "//backend/model/channel", "//backend/model/message", "//lib/java/spring/kafka/core:spring-kafka-core", diff --git a/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/Stores.java b/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/Stores.java index c329992b68..b0ce5eb282 100644 --- a/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/Stores.java +++ b/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/Stores.java @@ -15,12 +15,14 @@ import org.apache.kafka.streams.kstream.Materialized; import org.apache.kafka.streams.state.ReadOnlyKeyValueStore; import org.springframework.beans.factory.DisposableBean; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; @Component -public class Stores implements DisposableBean, ApplicationListener { +public class Stores implements ApplicationListener, DisposableBean, HealthIndicator { private static final String appId = "sources.google.ConnectorStores"; private final String channelsStore = "channels-store"; private static final String applicationCommunicationChannels = new ApplicationCommunicationChannels().name(); @@ -71,6 +73,14 @@ public void destroy() { } } + + @Override + public Health health() { + getChannelsStore(); + + return Health.up().build(); + } + // visible for testing KafkaStreams.State getStreamState() { return streams.state(); diff --git a/backend/sources/twilio/connector/BUILD b/backend/sources/twilio/connector/BUILD index 85edc045c8..1ab494184b 100644 --- a/backend/sources/twilio/connector/BUILD +++ b/backend/sources/twilio/connector/BUILD @@ -4,6 +4,7 @@ load("//tools/build:container_push.bzl", "container_push") app_deps = [ "//backend:base_app", + "//:springboot_actuator", "//backend/model/channel", "//backend/model/message", "//lib/java/log", diff --git a/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/ChannelsController.java b/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/ChannelsController.java index 58cb944c6a..b25ae1fc0d 100644 --- a/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/ChannelsController.java +++ b/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/ChannelsController.java @@ -10,6 +10,8 @@ import lombok.NoArgsConstructor; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerRecord; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -111,6 +113,7 @@ private ResponseEntity disconnect(@RequestBody @Valid DisconnectChannelReques return ResponseEntity.ok(new EmptyResponsePayload()); } + } @Data diff --git a/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/Stores.java b/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/Stores.java index afcb3b0d1f..ba2746be62 100644 --- a/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/Stores.java +++ b/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/Stores.java @@ -16,12 +16,14 @@ import org.apache.kafka.streams.kstream.Materialized; import org.apache.kafka.streams.state.ReadOnlyKeyValueStore; import org.springframework.beans.factory.DisposableBean; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; @Component -public class Stores implements DisposableBean, ApplicationListener { +public class Stores implements ApplicationListener, DisposableBean, HealthIndicator { private static final String applicationCommunicationChannels = new ApplicationCommunicationChannels().name(); @@ -57,7 +59,7 @@ public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) { .groupByKey() .aggregate(SendMessageRequest::new, (conversationId, message, aggregate) -> { - SendMessageRequest.SendMessageRequestBuilder sendMessageRequestBuilder = aggregate.toBuilder(); + SendMessageRequest.SendMessageRequestBuilder sendMessageRequestBuilder = aggregate.toBuilder(); if (SenderType.SOURCE_CONTACT.equals(message.getSenderType())) { sendMessageRequestBuilder.sourceConversationId(message.getSenderId()); } @@ -88,6 +90,13 @@ public void destroy() { } } + @Override + public Health health() { + getChannelsStore(); + + return Health.up().build(); + } + // visible for testing KafkaStreams.State getStreamState() { return streams.state(); diff --git a/infrastructure/cli/cmd/root.go b/infrastructure/cli/cmd/root.go index 86c1688321..16bdc72f01 100644 --- a/infrastructure/cli/cmd/root.go +++ b/infrastructure/cli/cmd/root.go @@ -14,7 +14,7 @@ import ( "github.com/spf13/viper" ) -const configFileName = ".airycli" +const configFileName = ".airycli.yaml" var configFile string var Version string @@ -55,11 +55,10 @@ var initCmd = &cobra.Command{ os.Exit(1) } - //TODO let users choose a different name - viper.AddConfigPath(home) - viper.SetConfigName(configFileName) - - viper.WriteConfigAs(path.Join(home, configFileName)) + err = viper.WriteConfigAs(path.Join(home, configFileName)) + if err != nil { + fmt.Println("cannot write config: ", err) + } }, } @@ -84,11 +83,10 @@ func initConfig() { } viper.AddConfigPath(home) + viper.SetConfigType("yaml") viper.SetConfigName(configFileName) } - viper.AutomaticEnv() - if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); ok { fmt.Println(err) diff --git a/infrastructure/helm-chart/charts/apps/charts/api-admin/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/api-admin/templates/deployment.yaml index c85d710620..b1c26779d8 100644 --- a/infrastructure/helm-chart/charts/apps/charts/api-admin/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/api-admin/templates/deployment.yaml @@ -26,6 +26,10 @@ spec: image: "{{ .Values.global.containerRegistry}}/{{ .Values.image }}:{{ .Values.global.appImageTag }}" imagePullPolicy: Always env: + - name: KUBERNETES_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace - name: KAFKA_BROKERS valueFrom: configMapKeyRef: diff --git a/infrastructure/helm-chart/charts/apps/charts/sources-facebook-connector/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/sources-facebook-connector/templates/deployment.yaml index 57755fd259..8230e0edf4 100644 --- a/infrastructure/helm-chart/charts/apps/charts/sources-facebook-connector/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/sources-facebook-connector/templates/deployment.yaml @@ -64,11 +64,13 @@ spec: name: api-config key: JWT_SECRET livenessProbe: - tcpSocket: - port: 6000 + httpGet: + path: /actuator/health + port: 8080 + httpHeaders: + - name: Health-Check + value: health-check initialDelaySeconds: 60 - periodSeconds: 10 - failureThreshold: 3 - name: ngrok command: - /bin/bash diff --git a/infrastructure/helm-chart/charts/apps/charts/sources-google-connector/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/sources-google-connector/templates/deployment.yaml index 9681372125..a0051a9555 100644 --- a/infrastructure/helm-chart/charts/apps/charts/sources-google-connector/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/sources-google-connector/templates/deployment.yaml @@ -57,11 +57,13 @@ spec: name: api-config key: JWT_SECRET livenessProbe: - tcpSocket: - port: 6000 + httpGet: + path: /actuator/health + port: 8080 + httpHeaders: + - name: Health-Check + value: health-check initialDelaySeconds: 60 - periodSeconds: 10 - failureThreshold: 3 - name: ngrok command: - /bin/bash diff --git a/infrastructure/helm-chart/charts/apps/charts/sources-twilio-connector/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/sources-twilio-connector/templates/deployment.yaml index 807a4fa113..1fbb942ecf 100644 --- a/infrastructure/helm-chart/charts/apps/charts/sources-twilio-connector/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/sources-twilio-connector/templates/deployment.yaml @@ -57,11 +57,13 @@ spec: name: api-config key: JWT_SECRET livenessProbe: - tcpSocket: - port: 6000 + httpGet: + path: /actuator/health + port: 8080 + httpHeaders: + - name: Health-Check + value: health-check initialDelaySeconds: 60 - periodSeconds: 10 - failureThreshold: 3 - name: ngrok command: - /bin/bash diff --git a/infrastructure/helm-chart/charts/ingress/templates/ingress.yaml b/infrastructure/helm-chart/charts/ingress/templates/ingress.yaml index baefc9688e..d3b25ac926 100644 --- a/infrastructure/helm-chart/charts/ingress/templates/ingress.yaml +++ b/infrastructure/helm-chart/charts/ingress/templates/ingress.yaml @@ -78,6 +78,13 @@ spec: name: api-communication port: number: 80 + - path: /client.config + pathType: Prefix + backend: + service: + name: api-admin + port: + number: 80 - path: /channels.list pathType: Prefix backend: From 6d25007c29609a00acf2d4540d981be8b8ca872f Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Mon, 18 Jan 2021 15:37:29 +0100 Subject: [PATCH 17/56] Endpoints should return {} instead of nothing (#672) --- .../java/co/airy/core/api/admin/ChannelsController.java | 2 +- .../core/api/communication/ConversationsController.java | 7 ++++--- .../co/airy/core/sources/google/ChannelsController.java | 2 +- .../co/airy/core/sources/twilio/ChannelsController.java | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/backend/api/admin/src/main/java/co/airy/core/api/admin/ChannelsController.java b/backend/api/admin/src/main/java/co/airy/core/api/admin/ChannelsController.java index 2f8b09bcdf..7f70c5e78f 100644 --- a/backend/api/admin/src/main/java/co/airy/core/api/admin/ChannelsController.java +++ b/backend/api/admin/src/main/java/co/airy/core/api/admin/ChannelsController.java @@ -80,7 +80,7 @@ ResponseEntity disconnect(@RequestBody @Valid ChannelDisconnectRequestPayload } if (channel.getConnectionState().equals(ChannelConnectionState.DISCONNECTED)) { - return ResponseEntity.accepted().build(); + return ResponseEntity.accepted().body(new EmptyResponsePayload()); } channel.setConnectionState(ChannelConnectionState.DISCONNECTED); diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/ConversationsController.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/ConversationsController.java index eb9fb1e79d..6a63240053 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/ConversationsController.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/ConversationsController.java @@ -16,6 +16,7 @@ import co.airy.core.api.communication.payload.ResponseMetadata; import co.airy.pagination.Page; import co.airy.pagination.Paginator; +import co.airy.spring.web.payload.EmptyResponsePayload; import co.airy.spring.web.payload.RequestErrorResponsePayload; import org.apache.kafka.streams.state.KeyValueIterator; import org.apache.kafka.streams.state.ReadOnlyKeyValueStore; @@ -175,7 +176,7 @@ ResponseEntity conversationMarkRead(@RequestBody @Valid ConversationByIdReque return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new RequestErrorResponsePayload(e.getMessage())); } - return ResponseEntity.accepted().build(); + return ResponseEntity.accepted().body(new EmptyResponsePayload()); } @PostMapping("/conversations.tag") @@ -197,7 +198,7 @@ ResponseEntity conversationTag(@RequestBody @Valid ConversationTagRequestPayl return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new RequestErrorResponsePayload(e.getMessage())); } - return ResponseEntity.accepted().build(); + return ResponseEntity.accepted().body(new EmptyResponsePayload()); } @PostMapping("/conversations.untag") @@ -219,6 +220,6 @@ ResponseEntity conversationUntag(@RequestBody @Valid ConversationTagRequestPa return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new RequestErrorResponsePayload(e.getMessage())); } - return ResponseEntity.accepted().build(); + return ResponseEntity.accepted().body(new EmptyResponsePayload()); } } diff --git a/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/ChannelsController.java b/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/ChannelsController.java index 6fedf6398b..3407bbe0a9 100644 --- a/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/ChannelsController.java +++ b/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/ChannelsController.java @@ -70,7 +70,7 @@ ResponseEntity disconnect(@RequestBody @Valid DisconnectChannelRequestPayload } if (channel.getConnectionState().equals(ChannelConnectionState.DISCONNECTED)) { - return ResponseEntity.accepted().build(); + return ResponseEntity.accepted().body(new EmptyResponsePayload()); } channel.setConnectionState(ChannelConnectionState.DISCONNECTED); diff --git a/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/ChannelsController.java b/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/ChannelsController.java index b25ae1fc0d..7b7d9e04f0 100644 --- a/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/ChannelsController.java +++ b/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/ChannelsController.java @@ -99,7 +99,7 @@ private ResponseEntity disconnect(@RequestBody @Valid DisconnectChannelReques } if (channel.getConnectionState().equals(ChannelConnectionState.DISCONNECTED)) { - return ResponseEntity.accepted().build(); + return ResponseEntity.accepted().body(new EmptyResponsePayload()); } channel.setConnectionState(ChannelConnectionState.DISCONNECTED); From 8a8d698e3ff7013527d6e8e7fed9085b4b8b2bb1 Mon Sep 17 00:00:00 2001 From: lucapette Date: Mon, 18 Jan 2021 16:42:00 +0100 Subject: [PATCH 18/56] [#421] Consider service disabled if any exception occurs (#674) --- .../co/airy/core/api/config/ClientConfigController.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/backend/api/admin/src/main/java/co/airy/core/api/config/ClientConfigController.java b/backend/api/admin/src/main/java/co/airy/core/api/config/ClientConfigController.java index da1ff2047f..ee670500fd 100644 --- a/backend/api/admin/src/main/java/co/airy/core/api/config/ClientConfigController.java +++ b/backend/api/admin/src/main/java/co/airy/core/api/config/ClientConfigController.java @@ -30,9 +30,12 @@ public ResponseEntity getConfig() { List>> components = new ArrayList<>(); for (String service : serviceDiscovery.getServices()) { - ResponseEntity response = restTemplate.exchange(String.format("http://%s.%s/actuator/health", service, namespace), HttpMethod.GET, null, Object.class); - - components.add(Map.of(service.replace("-connector", ""), Map.of("enabled", Boolean.toString(response.getStatusCode().is2xxSuccessful())))); + try { + ResponseEntity response = restTemplate.exchange(String.format("http://%s.%s/actuator/health", service, namespace), HttpMethod.GET, null, Object.class); + components.add(Map.of(service.replace("-connector", ""), Map.of("enabled", Boolean.toString(response.getStatusCode().is2xxSuccessful())))); + } catch (Exception e) { + components.add(Map.of(service.replace("-connector", ""), Map.of("enabled", Boolean.toString(false)))); + } } return ResponseEntity.ok(ClientConfigResponsePayload.builder() From 6c4649907d897a259f5dd9a8b6746f96a463e199 Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Mon, 18 Jan 2021 16:53:18 +0100 Subject: [PATCH 19/56] [#621] Controller starts/stops apps based on config map changes (#647) * [#621] Controller starts/stops apps based on config map changes * Update infrastructure/controller/pkg/configmap-controller/create.go Co-authored-by: lucapette Co-authored-by: lucapette --- infrastructure/controller/BUILD | 1 + infrastructure/controller/main.go | 26 ++- .../controller/pkg/configmap-controller/BUILD | 7 +- .../configmap-controller.go | 148 ------------------ .../pkg/configmap-controller/controller.go | 126 +++++++++++++++ .../pkg/configmap-controller/create.go | 42 +++++ .../pkg/configmap-controller/delete.go | 37 +++++ .../pkg/configmap-controller/update.go | 33 ++++ .../airy-config/templates/frontend.yaml | 9 ++ .../templates/deployment.yaml | 3 + .../frontend-demo/templates/deployment.yaml | 3 + .../lib/go/k8s/handler/configmaps.go | 20 +-- .../lib/go/k8s/handler/containers.go | 23 +++ .../lib/go/k8s/handler/deployments.go | 131 ++++++++++------ 14 files changed, 391 insertions(+), 218 deletions(-) delete mode 100644 infrastructure/controller/pkg/configmap-controller/configmap-controller.go create mode 100644 infrastructure/controller/pkg/configmap-controller/controller.go create mode 100644 infrastructure/controller/pkg/configmap-controller/create.go create mode 100644 infrastructure/controller/pkg/configmap-controller/delete.go create mode 100644 infrastructure/controller/pkg/configmap-controller/update.go create mode 100644 infrastructure/helm-chart/charts/apps/charts/airy-config/templates/frontend.yaml diff --git a/infrastructure/controller/BUILD b/infrastructure/controller/BUILD index a10567ee0d..dbddf8602b 100644 --- a/infrastructure/controller/BUILD +++ b/infrastructure/controller/BUILD @@ -10,6 +10,7 @@ go_library( visibility = ["//visibility:private"], deps = [ "//infrastructure/controller/pkg/configmap-controller", + "@io_k8s_api//core/v1:core", "@io_k8s_client_go//kubernetes", "@io_k8s_client_go//tools/clientcmd", "@io_k8s_klog//:klog", diff --git a/infrastructure/controller/main.go b/infrastructure/controller/main.go index 5101b7925c..1c49b437cc 100644 --- a/infrastructure/controller/main.go +++ b/infrastructure/controller/main.go @@ -10,8 +10,9 @@ package main import ( "flag" - cm "github.com/airyhq/airy/infrastructure/controller/pkg/configmap-controller" + v1 "k8s.io/api/core/v1" + "os" "k8s.io/klog" @@ -20,28 +21,39 @@ import ( ) func main() { - var kubeconfig string + var kubeConfig string var master string // Check if kubernetes configuration is provided, otherwise use serviceAccount - flag.StringVar(&kubeconfig, "kubeconfig", "", "absolute path to the kubeconfig file") + flag.StringVar(&kubeConfig, "kubeconfig", "", "absolute path to the kubeConfig file") flag.StringVar(&master, "master", "", "master url") flag.Parse() // Create connection - config, err := clientcmd.BuildConfigFromFlags(master, kubeconfig) + config, err := clientcmd.BuildConfigFromFlags(master, kubeConfig) if err != nil { klog.Fatal(err) } - // Create clientset client - clientset, err := kubernetes.NewForConfig(config) + clientSet, err := kubernetes.NewForConfig(config) if err != nil { klog.Fatal(err) } + namespace, present := os.LookupEnv("NAMESPACE") + if present != true { + klog.Infof("Namespace not set. Defaulting to: %s", v1.NamespaceDefault) + namespace = v1.NamespaceDefault + } + + labelSelector := os.Getenv("LABEL_SELECTOR") + // Create configMap controller - configMapController := cm.ConfigMapController(clientset) + configMapController := cm.ConfigMapController(cm.Context{ + ClientSet: clientSet, + Namespace: namespace, + LabelSelector: labelSelector, + }) stop := make(chan struct{}) defer close(stop) go configMapController.Run(1, stop) diff --git a/infrastructure/controller/pkg/configmap-controller/BUILD b/infrastructure/controller/pkg/configmap-controller/BUILD index 91019d74ed..dc329e973e 100644 --- a/infrastructure/controller/pkg/configmap-controller/BUILD +++ b/infrastructure/controller/pkg/configmap-controller/BUILD @@ -2,7 +2,12 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "configmap-controller", - srcs = ["configmap-controller.go"], + srcs = [ + "controller.go", + "create.go", + "delete.go", + "update.go", + ], importpath = "github.com/airyhq/airy/infrastructure/controller/pkg/configmap-controller", visibility = ["//visibility:public"], deps = [ diff --git a/infrastructure/controller/pkg/configmap-controller/configmap-controller.go b/infrastructure/controller/pkg/configmap-controller/configmap-controller.go deleted file mode 100644 index 4e9789bfe4..0000000000 --- a/infrastructure/controller/pkg/configmap-controller/configmap-controller.go +++ /dev/null @@ -1,148 +0,0 @@ -package configmapController - -import ( - "fmt" - "time" - - "github.com/airyhq/airy/infrastructure/lib/go/k8s/handler" - "github.com/airyhq/airy/infrastructure/lib/go/k8s/util" - - v1 "k8s.io/api/core/v1" - - "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/util/runtime" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/klog" - - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/tools/cache" - "k8s.io/client-go/util/workqueue" -) - -type Controller struct { - indexer cache.Indexer - queue workqueue.RateLimitingInterface - informer cache.Controller - clientset kubernetes.Interface -} - -func NewController(queue workqueue.RateLimitingInterface, indexer cache.Indexer, informer cache.Controller, clientset kubernetes.Interface) *Controller { - return &Controller{ - informer: informer, - indexer: indexer, - queue: queue, - clientset: clientset, - } -} - -func (c *Controller) processNextItem() bool { - // Wait until there is a new item in the working queue - key, quit := c.queue.Get() - if quit { - return false - } - defer c.queue.Done(key) - // Invoke the method containing the business logic - err := c.Handle(key.(string)) - // Handle the error if something went wrong during the execution of the business logic - c.handleErr(err, key) - return true -} - -// Handle is the business logic of the controller. -func (c *Controller) Handle(key string) error { - obj, exists, err := c.indexer.GetByKey(key) - if err != nil { - klog.Errorf("Fetching object with key %s from store failed with %v", key, err) - return err - } - - if !exists { - fmt.Printf("Object %s does not exist anymore\n", key) - } else { - configmap := handler.GetConfigmapConfig(obj.(*v1.ConfigMap)) - klog.Infof("Handling change in configmap %s\n", configmap.Name) - affectedDeployments, errGetDeployments := handler.GetAffectedDeploymentsConfigmap(c.clientset, configmap.Name, "default", "") - if errGetDeployments != nil { - klog.Errorf("Error retrieving affected deployments %v", errGetDeployments) - } - for _, affectedDeployment := range affectedDeployments { - klog.Infof("Scheduling reload for deployment: %s", affectedDeployment) - handler.ReloadDeployment(c.clientset, "default", affectedDeployment) - } - } - return nil -} - -// handleErr checks if an error happened and makes sure we will retry later. -func (c *Controller) handleErr(err error, key interface{}) { - if err == nil { - c.queue.Forget(key) - return - } - - // This controller retries 5 times if something goes wrong. After that, it stops trying. - if c.queue.NumRequeues(key) < 5 { - klog.Infof("Error syncing %v: %v", key, err) - c.queue.AddRateLimited(key) - return - } - - c.queue.Forget(key) - // Report to an external entity that, even after several retries, we could not successfully process this key - runtime.HandleError(err) - klog.Infof("Dropping pod %q out of the queue: %v", key, err) -} - -func (c *Controller) Run(threadiness int, stopCh chan struct{}) { - defer runtime.HandleCrash() - - // Let the workers stop when we are done - defer c.queue.ShutDown() - klog.Info("Starting controller") - - go c.informer.Run(stopCh) - - // Wait for all involved caches to be synced, before processing items from the queue is started - if !cache.WaitForCacheSync(stopCh, c.informer.HasSynced) { - runtime.HandleError(fmt.Errorf("Timed out waiting for caches to sync")) - return - } - - for i := 0; i < threadiness; i++ { - go wait.Until(c.runWorker, time.Second, stopCh) - } - - <-stopCh - klog.Info("Stopping controller") -} - -func (c *Controller) runWorker() { - for c.processNextItem() { - } -} - -// ConfigMapController for monitoring the configmaps -func ConfigMapController(clientset kubernetes.Interface) *Controller { - - configMapListWatcher := cache.NewListWatchFromClient(clientset.CoreV1().RESTClient(), "configmaps", v1.NamespaceDefault, fields.Everything()) - queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) - indexer, informer := cache.NewIndexerInformer(configMapListWatcher, &v1.ConfigMap{}, 0, cache.ResourceEventHandlerFuncs{ - AddFunc: func(obj interface{}) { - // Currently we do nothing when a new configMap is added - klog.Infof("Added configMap: %s , sha: %s", obj.(*v1.ConfigMap).GetName(), util.GetSHAfromConfigmap(obj.(*v1.ConfigMap))) - }, - UpdateFunc: func(old interface{}, new interface{}) { - key, err := cache.MetaNamespaceKeyFunc(new) - if err == nil { - queue.Add(key) - klog.Infof("Updated configMap %s from sha: %s to sha: %s", old.(*v1.ConfigMap).GetName(), util.GetSHAfromConfigmap(old.(*v1.ConfigMap)), util.GetSHAfromConfigmap(new.(*v1.ConfigMap))) - } - }, - DeleteFunc: func(obj interface{}) { - // Currently we do nothing when a new configMap is deleted - klog.Infof("Deleted configMap %s", obj.(*v1.ConfigMap).GetName()) - }, - }, cache.Indexers{}) - return NewController(queue, indexer, informer, clientset) -} diff --git a/infrastructure/controller/pkg/configmap-controller/controller.go b/infrastructure/controller/pkg/configmap-controller/controller.go new file mode 100644 index 0000000000..c30b5a7b06 --- /dev/null +++ b/infrastructure/controller/pkg/configmap-controller/controller.go @@ -0,0 +1,126 @@ +package cmcontroller + +import ( + "fmt" + "time" + + v1 "k8s.io/api/core/v1" + + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/klog" + + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/util/workqueue" +) + +type Controller struct { + indexer cache.Indexer + queue workqueue.RateLimitingInterface + informer cache.Controller + context Context +} + +type Context struct { + ClientSet kubernetes.Interface + Namespace string + LabelSelector string +} + +type ResourceHandler interface { + Handle(context Context) error +} + +func (c *Controller) processNextItem() bool { + // Wait until there is a new item in the working queue + handler, quit := c.queue.Get() + if quit { + return false + } + defer c.queue.Done(handler) + // Invoke the method containing the business logic + err := handler.(ResourceHandler).Handle(c.context) + // Handle the error if something went wrong during the execution of the business logic + c.handleErr(err, handler) + return true +} + +// handleErr checks if an error happened and makes sure we will retry later. +func (c *Controller) handleErr(err error, key interface{}) { + if err == nil { + c.queue.Forget(key) + return + } + + // This controller retries 5 times if something goes wrong. After that, it stops trying. + if c.queue.NumRequeues(key) < 5 { + klog.Infof("Error syncing %v: %v", key, err) + c.queue.AddRateLimited(key) + return + } + + c.queue.Forget(key) + // Report to an external entity that, even after several retries, we could not successfully process this key + runtime.HandleError(err) + klog.Infof("Dropping configmap %q out of the queue: %v", key, err) +} + +func (c *Controller) Run(threadiness int, stopCh chan struct{}) { + defer runtime.HandleCrash() + + // Let the workers stop when we are done + defer c.queue.ShutDown() + klog.Info("Starting controller") + + go c.informer.Run(stopCh) + + // Wait for all involved caches to be synced, before processing items from the queue is started + if !cache.WaitForCacheSync(stopCh, c.informer.HasSynced) { + runtime.HandleError(fmt.Errorf("Timed out waiting for caches to sync")) + return + } + + for i := 0; i < threadiness; i++ { + go wait.Until(c.runWorker, time.Second, stopCh) + } + + <-stopCh + klog.Info("Stopping controller") +} + +func (c *Controller) runWorker() { + for c.processNextItem() { + } +} + +func ConfigMapController(context Context) *Controller { + configMapListWatcher := cache.NewListWatchFromClient(context.ClientSet.CoreV1().RESTClient(), "configmaps", context.Namespace, fields.Everything()) + queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()) + indexer, informer := cache.NewIndexerInformer(configMapListWatcher, &v1.ConfigMap{}, 0, cache.ResourceEventHandlerFuncs{ + AddFunc: func(obj interface{}) { + queue.Add(&ResourceCreatedHandler{ + ConfigMap: obj.(*v1.ConfigMap), + }) + }, + UpdateFunc: func(old interface{}, new interface{}) { + queue.Add(&ResourceUpdatedHandler{ + ConfigMap: new.(*v1.ConfigMap), + OldConfigMap: old.(*v1.ConfigMap), + }) + }, + DeleteFunc: func(obj interface{}) { + queue.Add(&ResourceDeleteHandler{ + ConfigMap: obj.(*v1.ConfigMap), + }) + }, + }, cache.Indexers{}) + + return &Controller{ + informer: informer, + indexer: indexer, + queue: queue, + context: context, + } +} diff --git a/infrastructure/controller/pkg/configmap-controller/create.go b/infrastructure/controller/pkg/configmap-controller/create.go new file mode 100644 index 0000000000..9ea049ae3e --- /dev/null +++ b/infrastructure/controller/pkg/configmap-controller/create.go @@ -0,0 +1,42 @@ +package cmcontroller + +import ( + "github.com/airyhq/airy/infrastructure/lib/go/k8s/handler" + "github.com/airyhq/airy/infrastructure/lib/go/k8s/util" + v1 "k8s.io/api/core/v1" + "k8s.io/klog" +) + +type ResourceCreatedHandler struct { + ConfigMap *v1.ConfigMap +} + +func (r ResourceCreatedHandler) Handle(ctx Context) error { + klog.Infof("Added configMap: %s , sha: %s", r.ConfigMap.GetName(), util.GetSHAfromConfigmap(r.ConfigMap)) + deployments, errGetDeployments := handler.GetDeploymentsReferencingCm(ctx.ClientSet, + r.ConfigMap.Name, ctx.Namespace, ctx.LabelSelector) + if errGetDeployments != nil { + klog.Errorf("Error retrieving affected deployments %v", errGetDeployments) + return errGetDeployments + } + + for _, deployment := range deployments { + if !handler.CanBeStarted(deployment, ctx.ClientSet) { + klog.Infof("Skipping deployment %s because it is missing config maps", deployment.Name) + continue + } + + klog.Infof("Scheduling start for deployment: %s", deployment.Name) + if err := handler.ScaleDeployment(handler.ScaleCommand{ + ClientSet: ctx.ClientSet, + Namespace: ctx.Namespace, + DeploymentName: deployment.Name, + DesiredReplicas: 1, //TODO extract from annotation + }); err != nil { + klog.Errorf("Starting deployment failed: %v", err) + return err + } + klog.Infof("Started deployment: %s", deployment.Name) + } + return nil +} diff --git a/infrastructure/controller/pkg/configmap-controller/delete.go b/infrastructure/controller/pkg/configmap-controller/delete.go new file mode 100644 index 0000000000..0453d6428f --- /dev/null +++ b/infrastructure/controller/pkg/configmap-controller/delete.go @@ -0,0 +1,37 @@ +package cmcontroller + +import ( + "github.com/airyhq/airy/infrastructure/lib/go/k8s/handler" + "github.com/airyhq/airy/infrastructure/lib/go/k8s/util" + v1 "k8s.io/api/core/v1" + "k8s.io/klog" +) + +type ResourceDeleteHandler struct { + ConfigMap *v1.ConfigMap +} + +func (r ResourceDeleteHandler) Handle(ctx Context) error { + klog.Infof("Deleted configMap: %s , sha: %s", r.ConfigMap.GetName(), util.GetSHAfromConfigmap(r.ConfigMap)) + deployments, errGetDeployments := handler.GetDeploymentsReferencingCm(ctx.ClientSet, + r.ConfigMap.Name, ctx.Namespace, ctx.LabelSelector) + if errGetDeployments != nil { + klog.Errorf("Error retrieving affected deployments %v", errGetDeployments) + return errGetDeployments + } + + for _, deployment := range deployments { + klog.Infof("Scheduling stopping for deployment: %s", deployment.Name) + if err := handler.ScaleDeployment(handler.ScaleCommand{ + ClientSet: ctx.ClientSet, + Namespace: ctx.Namespace, + DeploymentName: deployment.Name, + DesiredReplicas: 0, + }); err != nil { + klog.Errorf("Stopping deployment failed: %v", err) + return err + } + klog.Infof("Stopped deployment: %s", deployment.Name) + } + return nil +} diff --git a/infrastructure/controller/pkg/configmap-controller/update.go b/infrastructure/controller/pkg/configmap-controller/update.go new file mode 100644 index 0000000000..7a0f07b726 --- /dev/null +++ b/infrastructure/controller/pkg/configmap-controller/update.go @@ -0,0 +1,33 @@ +package cmcontroller + +import ( + "github.com/airyhq/airy/infrastructure/lib/go/k8s/handler" + "github.com/airyhq/airy/infrastructure/lib/go/k8s/util" + v1 "k8s.io/api/core/v1" + "k8s.io/klog" +) + +type ResourceUpdatedHandler struct { + ConfigMap *v1.ConfigMap + OldConfigMap *v1.ConfigMap +} + +func (r ResourceUpdatedHandler) Handle(ctx Context) error { + klog.Infof("Updated configMap %s from sha: %s to sha: %s", + r.ConfigMap.GetName(), util.GetSHAfromConfigmap(r.OldConfigMap), util.GetSHAfromConfigmap(r.ConfigMap)) + deployments, errGetDeployments := handler.GetDeploymentsReferencingCm(ctx.ClientSet, + r.ConfigMap.Name, ctx.Namespace, ctx.LabelSelector) + if errGetDeployments != nil { + klog.Errorf("Error retrieving affected deployments %v", errGetDeployments) + } + + for _, deployment := range deployments { + klog.Infof("Scheduling reload for deployment: %s", deployment.Name) + if err := handler.ReloadDeployment(deployment, ctx.ClientSet); err != nil { + klog.Errorf("Reloading deployment failed: %v", err) + return err + } + klog.Infof("Reloaded deployment: %s", deployment.Name) + } + return nil +} diff --git a/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/frontend.yaml b/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/frontend.yaml new file mode 100644 index 0000000000..533db43ce5 --- /dev/null +++ b/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/frontend.yaml @@ -0,0 +1,9 @@ +# Dummy configmap so that frontend deployments can be started by the controller +# TODO remove once there is an annotation +apiVersion: v1 +kind: ConfigMap +metadata: + name: frontend-config + namespace: {{ .Values.global.namespace }} +data: + dummy: "blank" diff --git a/infrastructure/helm-chart/charts/apps/charts/frontend-chat-plugin/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/frontend-chat-plugin/templates/deployment.yaml index 50a59433ff..ddf58398fe 100644 --- a/infrastructure/helm-chart/charts/apps/charts/frontend-chat-plugin/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/frontend-chat-plugin/templates/deployment.yaml @@ -25,6 +25,9 @@ spec: - name: app image: "{{ .Values.global.containerRegistry}}/{{ .Values.image }}:{{ .Values.global.appImageTag }}" imagePullPolicy: Always + envFrom: + - configMapRef: + name: frontend-config livenessProbe: httpGet: path: /health diff --git a/infrastructure/helm-chart/charts/apps/charts/frontend-demo/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/frontend-demo/templates/deployment.yaml index bec1b7c43a..ddd62e7a55 100644 --- a/infrastructure/helm-chart/charts/apps/charts/frontend-demo/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/frontend-demo/templates/deployment.yaml @@ -25,6 +25,9 @@ spec: - name: app image: "{{ .Values.global.containerRegistry}}/{{ .Values.image }}:{{ .Values.global.appImageTag }}" imagePullPolicy: Always + envFrom: + - configMapRef: + name: frontend-config livenessProbe: httpGet: path: /health diff --git a/infrastructure/lib/go/k8s/handler/configmaps.go b/infrastructure/lib/go/k8s/handler/configmaps.go index a58f7d0273..4e47c519d3 100644 --- a/infrastructure/lib/go/k8s/handler/configmaps.go +++ b/infrastructure/lib/go/k8s/handler/configmaps.go @@ -1,17 +1,17 @@ package handler import ( - "github.com/airyhq/airy/infrastructure/lib/go/k8s/util" - + "context" v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" ) -// GetConfigmapConfig provides utility config for configmap -func GetConfigmapConfig(configmap *v1.ConfigMap) Config { - return Config{ - Namespace: configmap.Namespace, - Name: configmap.Name, - SHAValue: util.GetSHAfromConfigmap(configmap), - Type: "CONFIGMAP", - } +func ConfigMapExists(name string, clientSet kubernetes.Interface, namespace string) bool { + configMap, err := GetConfigMap(name, clientSet, namespace) + return configMap != nil && err == nil +} + +func GetConfigMap(name string, clientSet kubernetes.Interface, namespace string) (*v1.ConfigMap, error) { + return clientSet.CoreV1().ConfigMaps(namespace).Get(context.TODO(), name, metav1.GetOptions{}) } diff --git a/infrastructure/lib/go/k8s/handler/containers.go b/infrastructure/lib/go/k8s/handler/containers.go index 6e22022ff7..662a0656b9 100644 --- a/infrastructure/lib/go/k8s/handler/containers.go +++ b/infrastructure/lib/go/k8s/handler/containers.go @@ -74,3 +74,26 @@ func getContainerWithEnvReference(containers []v1.Container, resourceName string } return nil } + +// Does not return config maps whose only use is marked "optional" +func GetReferencedConfigMaps(container v1.Container) []string { + envs := container.Env + var configMaps []string + for j := range envs { + envVarSource := envs[j].ValueFrom + if envVarSource != nil && envVarSource.ConfigMapKeyRef != nil && + (envVarSource.ConfigMapKeyRef.Optional == nil || *envVarSource.ConfigMapKeyRef.Optional != true) { + configMaps = append(configMaps, envVarSource.ConfigMapKeyRef.LocalObjectReference.Name) + } + } + + envsFrom := container.EnvFrom + for j := range envsFrom { + if envsFrom[j].ConfigMapRef != nil && + (envsFrom[j].ConfigMapRef.Optional == nil || *envsFrom[j].ConfigMapRef.Optional != true) { + configMaps = append(configMaps, envsFrom[j].ConfigMapRef.LocalObjectReference.Name) + } + } + + return configMaps +} diff --git a/infrastructure/lib/go/k8s/handler/deployments.go b/infrastructure/lib/go/k8s/handler/deployments.go index ad86ebab9b..2790e29a52 100644 --- a/infrastructure/lib/go/k8s/handler/deployments.go +++ b/infrastructure/lib/go/k8s/handler/deployments.go @@ -2,38 +2,22 @@ package handler import ( "context" + apps_v1 "k8s.io/api/apps/v1" "github.com/airyhq/airy/infrastructure/lib/go/k8s/util" v1 "k8s.io/api/core/v1" - meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/util/retry" "k8s.io/klog" ) -func GetDeployments(clientset kubernetes.Interface, namespace string, labelSelector string) ([]string, error) { - var deployments []string - deploymentsClient := clientset.AppsV1().Deployments(namespace) +func GetDeploymentsReferencingCm(clientSet kubernetes.Interface, configMapName string, namespace string, labelSelector string) ([]apps_v1.Deployment, error) { + deploymentsClient := clientSet.AppsV1().Deployments(namespace) + var deployments []apps_v1.Deployment retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { - result, getErr := deploymentsClient.List(context.TODO(), meta_v1.ListOptions{LabelSelector: labelSelector}) - if getErr != nil { - klog.Errorf("Failed to get latest version of the Deployments: %v", getErr) - return getErr - } - for _, deploymentItem := range (*result).Items { - deployments = append(deployments, deploymentItem.Name) - } - return nil - }) - return deployments, retryErr -} - -func GetAffectedDeploymentsConfigmap(clientset kubernetes.Interface, configmapName string, namespace string, labelSelector string) ([]string, error) { - deploymentsClient := clientset.AppsV1().Deployments(namespace) - var affectedDeployments []string - retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { - result, getErr := deploymentsClient.List(context.TODO(), meta_v1.ListOptions{LabelSelector: labelSelector}) + result, getErr := deploymentsClient.List(context.TODO(), metav1.ListOptions{LabelSelector: labelSelector}) if getErr != nil { klog.Errorf("Failed to get latest version of the Deployments: %v", getErr) return getErr @@ -44,64 +28,107 @@ func GetAffectedDeploymentsConfigmap(clientset kubernetes.Interface, configmapNa initContainers := GetDeploymentInitContainers(deploymentItem) // Check the containers which have an EnvReference - container = getContainerWithEnvReference(containers, configmapName, "CONFIGMAP") + container = getContainerWithEnvReference(containers, configMapName, ConfigmapEnvVarPostfix) if container != nil { klog.Infof("Found affected container in deployment: %s", deploymentItem.Name) - affectedDeployments = append(affectedDeployments, deploymentItem.Name) + deployments = append(deployments, deploymentItem) } else { - container = getContainerWithEnvReference(initContainers, configmapName, "CONFIGMAP") + container = getContainerWithEnvReference(initContainers, configMapName, ConfigmapEnvVarPostfix) if container != nil { klog.Infof("Found affected initContainer in deployment: %s", deploymentItem.Name) - affectedDeployments = append(affectedDeployments, deploymentItem.Name) + deployments = append(deployments, deploymentItem) } } // Check the containers which have a VolumeMount volumes := GetDeploymentVolumes(deploymentItem) - volumeMountName := getVolumeMountName(volumes, "CONFIGMAP", configmapName) + volumeMountName := getVolumeMountName(volumes, ConfigmapEnvVarPostfix, configMapName) if volumeMountName != "" { container = getContainerWithVolumeMount(containers, volumeMountName) if container == nil && len(initContainers) > 0 { container = getContainerWithVolumeMount(initContainers, volumeMountName) if container != nil { // if configmap/secret is being used in init container then return the first Pod container to save reloader env - affectedDeployments = append(affectedDeployments, deploymentItem.Name) + deployments = append(deployments, deploymentItem) } } else if container != nil { - affectedDeployments = append(affectedDeployments, deploymentItem.Name) + deployments = append(deployments, deploymentItem) } } } return nil }) - return affectedDeployments, retryErr + return deployments, retryErr } -func ReloadDeployment(clientset kubernetes.Interface, namespace string, deploymentName string) error { - deploymentsClient := clientset.AppsV1().Deployments(namespace) - deployment, getErr := deploymentsClient.Get(context.TODO(), deploymentName, meta_v1.GetOptions{}) +// Won't do anything for replicas that are scaled down +func ReloadDeployment(deployment apps_v1.Deployment, clientSet kubernetes.Interface) error { + deploymentsClient := clientSet.AppsV1().Deployments(deployment.Namespace) + currentReplicas := deployment.Spec.Replicas - // If currentReplicas is 0 - don't do anything - if *currentReplicas != 0 { - retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { - deployment, getErr = deploymentsClient.Get(context.TODO(), deploymentName, meta_v1.GetOptions{}) - if getErr != nil { - klog.Errorf("Failed to get latest version of Deployment: %v", getErr) - return getErr + if *currentReplicas == 0 { + return nil + } + + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + deployment, err := deploymentsClient.Get(context.TODO(), deployment.Name, metav1.GetOptions{}) + if err != nil { + klog.Errorf("Failed to get latest version of Deployment: %v", err) + return err + } + deployment.Spec.Replicas = util.Int32Ptr(0) // reduce replica count + _, updateErr := deploymentsClient.Update(context.TODO(), deployment, metav1.UpdateOptions{}) + deployment.Spec.Replicas = currentReplicas // increase replica count + _, updateErr = deploymentsClient.Update(context.TODO(), deployment, metav1.UpdateOptions{}) + return updateErr + }) +} + +type ScaleCommand struct { + ClientSet kubernetes.Interface + Namespace string + DeploymentName string + DesiredReplicas int32 +} + +func ScaleDeployment(command ScaleCommand) error { + deploymentsClient := command.ClientSet.AppsV1().Deployments(command.Namespace) + return retry.RetryOnConflict(retry.DefaultRetry, func() error { + deployment, err := deploymentsClient.Get(context.TODO(), command.DeploymentName, metav1.GetOptions{}) + if err != nil { + klog.Errorf("failed to get latest version of Deployment: %v", err) + return err + } + + if *deployment.Spec.Replicas == command.DesiredReplicas { + return nil + } + + deployment.Spec.Replicas = util.Int32Ptr(command.DesiredReplicas) + _, updateErr := deploymentsClient.Update(context.TODO(), deployment, metav1.UpdateOptions{}) + return updateErr + }) +} + +func CanBeStarted(deployment apps_v1.Deployment, clientSet kubernetes.Interface) bool { + containers := GetDeploymentContainers(deployment) + + // Check that all referenced configMaps are present + checkedConfigMaps := make(map[string]bool) + for _, container := range containers { + configMaps := GetReferencedConfigMaps(container) + + for _, configMapName := range configMaps { + if !checkedConfigMaps[configMapName] { + if !ConfigMapExists(configMapName, clientSet, deployment.Namespace) { + return false + } + checkedConfigMaps[configMapName] = true } - deployment.Spec.Replicas = util.Int32Ptr(0) // reduce replica count - _, updateErr := deploymentsClient.Update(context.TODO(), deployment, meta_v1.UpdateOptions{}) - deployment.Spec.Replicas = currentReplicas // increase replica count - _, updateErr = deploymentsClient.Update(context.TODO(), deployment, meta_v1.UpdateOptions{}) - return updateErr - }) - if retryErr != nil { - klog.Errorf("Update failed: %v", retryErr) - return retryErr } - klog.Infof("Reloaded deployment %s", deploymentName) - return nil } - return getErr + + return true } + From 9a6a3aebc5cfe91f554e2a10db10870a9173ec29 Mon Sep 17 00:00:00 2001 From: Pascal Holy <54705263+pascal-airy@users.noreply.github.com> Date: Mon, 18 Jan 2021 16:55:58 +0100 Subject: [PATCH 20/56] [#308] Upload cli binaries to S3 (#669) Fixes #308 --- .github/release-drafter.yml | 8 ++++++++ .github/workflows/main.yml | 13 +++++++++++++ infrastructure/cli/BUILD | 28 ++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 500cf23915..32d7f65396 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -13,3 +13,11 @@ template: | ## Changes $CHANGES + + ## Airy CLI + + You can download the Airy CLI for your operating system from the following links: + + [MacOS](https://airy-core-binaries.s3.amazonaws.com/$RESOLVED_VERSION/darwin/amd64/airy) + [Linux](https://airy-core-binaries.s3.amazonaws.com/$RESOLVED_VERSION/linux/amd64/airy) + [Windows](https://airy-core-binaries.s3.amazonaws.com/$RESOLVED_VERSION/windows/amd64/airy.exe) \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2fd6e8bf81..f186c7500c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -49,3 +49,16 @@ jobs: run: | echo ${{ secrets.PAT }} | docker login ghcr.io -u airydevci --password-stdin ./scripts/push-images.sh ${{ github.ref }} + - name: Install aws cli + uses: chrislennon/action-aws-cli@v1.1 + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' + - name: Upload airy binary to S3 + if: startsWith(github.ref, 'refs/heads/release') || startsWith(github.ref, 'refs/heads/main') || startsWith(github.ref, 'refs/heads/develop') + run: | + aws s3 cp bazel-bin/infrastructure/cli/airy_linux_bin s3://airy-core-binaries/`cat ./VERSION`/linux/amd64/airy + aws s3 cp bazel-bin/infrastructure/cli/airy_darwin_bin s3://airy-core-binaries/`cat ./VERSION`/darwin/amd64/airy + aws s3 cp bazel-bin/infrastructure/cli/airy_windows_bin s3://airy-core-binaries/`cat ./VERSION`/windows/amd64/airy.exe + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/infrastructure/cli/BUILD b/infrastructure/cli/BUILD index 63bf621f7e..0aa478f382 100644 --- a/infrastructure/cli/BUILD +++ b/infrastructure/cli/BUILD @@ -16,6 +16,24 @@ go_binary( visibility = ["//visibility:public"], ) +os_list = [ + "linux", + "darwin", + "windows", +] + +[ + go_binary( + name = "airy_" + os, + out = "airy_" + os, + embed = [":cli_lib"], + goarch = "amd64", + goos = os, + visibility = ["//visibility:public"], + ) + for os in os_list +] + go_test( name = "cli_test", srcs = ["main_test.go"], @@ -26,3 +44,13 @@ go_test( embed = [":cli_lib"], deps = ["//infrastructure/cli/pkg/tests"], ) + +[ + genrule( + name = "airy_" + os + "_bin_rule", + srcs = [":airy_" + os], + outs = ["airy_" + os + "_bin"], + cmd = "cp $(SRCS) $@", + ) + for os in os_list +] From 2e8f321d5f309910db22515ab3a59b532103ee81 Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Tue, 19 Jan 2021 09:14:13 +0100 Subject: [PATCH 21/56] [#620] Download kubeconf file (#673) --- infrastructure/cli/cmd/root.go | 19 +++++++++++++++---- .../cli/pkg/tests/golden/cli.no-args.golden | 2 +- scripts/bootstrap.sh | 4 ++++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/infrastructure/cli/cmd/root.go b/infrastructure/cli/cmd/root.go index 16bdc72f01..99c790007c 100644 --- a/infrastructure/cli/cmd/root.go +++ b/infrastructure/cli/cmd/root.go @@ -14,7 +14,8 @@ import ( "github.com/spf13/viper" ) -const configFileName = ".airycli.yaml" +const configFileName = "cli.yaml" +const configDirName = ".airy" var configFile string var Version string @@ -55,7 +56,17 @@ var initCmd = &cobra.Command{ os.Exit(1) } - err = viper.WriteConfigAs(path.Join(home, configFileName)) + configDirPath := path.Join(home, configDirName) + + if _, errConfigDir := os.Stat(configDirPath); os.IsNotExist(errConfigDir) { + errDir := os.MkdirAll(configDirPath, 0700) + if errDir != nil { + fmt.Println(errDir) + os.Exit(1) + } + } + + err = viper.WriteConfigAs(path.Join(home, configDirName, configFileName)) if err != nil { fmt.Println("cannot write config: ", err) } @@ -82,7 +93,7 @@ func initConfig() { os.Exit(1) } - viper.AddConfigPath(home) + viper.AddConfigPath(path.Join(home, configDirName)) viper.SetConfigType("yaml") viper.SetConfigName(configFileName) } @@ -105,7 +116,7 @@ func init() { viper.BindPFlag("apihost", rootCmd.PersistentFlags().Lookup("apihost")) viper.SetDefault("apihost", "http://api.airy") - rootCmd.PersistentFlags().StringVar(&configFile, "config", "", "config file (default is $HOME/.airycli.yaml)") + rootCmd.PersistentFlags().StringVar(&configFile, "config", "", "config file (default is $HOME/.airy/cli.yaml)") rootCmd.AddCommand(auth.AuthCmd) rootCmd.AddCommand(config.ConfigCmd) rootCmd.AddCommand(ui.UICmd) diff --git a/infrastructure/cli/pkg/tests/golden/cli.no-args.golden b/infrastructure/cli/pkg/tests/golden/cli.no-args.golden index d14a1b5cde..a419bd207c 100644 --- a/infrastructure/cli/pkg/tests/golden/cli.no-args.golden +++ b/infrastructure/cli/pkg/tests/golden/cli.no-args.golden @@ -13,7 +13,7 @@ Available Commands: Flags: --apihost string Airy Core Platform HTTP API host (default "http://api.airy") - --config string config file (default is $HOME/.airycli.yaml) + --config string config file (default is $HOME/.airy/cli.yaml) -h, --help help for airy Use "airy [command] --help" for more information about a command. diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index e856536e6b..2f0b1625b8 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -140,3 +140,7 @@ if [ -z ${AIRY_VERSION+x} ]; then fi AIRY_VERSION=${AIRY_VERSION} vagrant up + +mkdir -p ~/.airy +cd $infra_path +vagrant ssh -c "cat /etc/rancher/k3s/k3s.yaml" 2>/dev/null | sed "s/127.0.0.1/192.168.50.5/g" > ~/.airy/kube.conf From e5b27c448589f0467fed86a5d761b6de605bb5ca Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Tue, 19 Jan 2021 09:57:00 +0100 Subject: [PATCH 22/56] [#641] Add content type for source templates (#676) --- .../java/co/airy/mapping/model/Content.java | 3 +- .../co/airy/mapping/model/SourceTemplate.java | 20 +++++++++++ .../sources/facebook/FacebookMapper.java | 8 +++++ .../java/co/airy/mapping/FacebookTest.java | 15 +++++++++ .../resources/facebook/template_generic.json | 33 +++++++++++++++++++ 5 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 lib/java/mapping/src/main/java/co/airy/mapping/model/SourceTemplate.java create mode 100644 lib/java/mapping/src/test/resources/facebook/template_generic.json diff --git a/lib/java/mapping/src/main/java/co/airy/mapping/model/Content.java b/lib/java/mapping/src/main/java/co/airy/mapping/model/Content.java index ffb4681b99..bdcd8534f2 100644 --- a/lib/java/mapping/src/main/java/co/airy/mapping/model/Content.java +++ b/lib/java/mapping/src/main/java/co/airy/mapping/model/Content.java @@ -9,7 +9,8 @@ @JsonSubTypes.Type(value = Audio.class, name = "audio"), @JsonSubTypes.Type(value = File.class, name = "file"), @JsonSubTypes.Type(value = Image.class, name = "image"), - @JsonSubTypes.Type(value = Video.class, name = "video") + @JsonSubTypes.Type(value = Video.class, name = "video"), + @JsonSubTypes.Type(value = SourceTemplate.class, name = "source.template") }) public abstract class Content { } diff --git a/lib/java/mapping/src/main/java/co/airy/mapping/model/SourceTemplate.java b/lib/java/mapping/src/main/java/co/airy/mapping/model/SourceTemplate.java new file mode 100644 index 0000000000..1270e05252 --- /dev/null +++ b/lib/java/mapping/src/main/java/co/airy/mapping/model/SourceTemplate.java @@ -0,0 +1,20 @@ +package co.airy.mapping.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = false) +public class SourceTemplate extends Content implements Serializable { + @NotNull + private String payload; +} diff --git a/lib/java/mapping/src/main/java/co/airy/mapping/sources/facebook/FacebookMapper.java b/lib/java/mapping/src/main/java/co/airy/mapping/sources/facebook/FacebookMapper.java index 0c8c10fb0b..34db2bbdd7 100644 --- a/lib/java/mapping/src/main/java/co/airy/mapping/sources/facebook/FacebookMapper.java +++ b/lib/java/mapping/src/main/java/co/airy/mapping/sources/facebook/FacebookMapper.java @@ -5,6 +5,7 @@ import co.airy.mapping.model.Content; import co.airy.mapping.model.File; import co.airy.mapping.model.Image; +import co.airy.mapping.model.SourceTemplate; import co.airy.mapping.model.Text; import co.airy.mapping.model.Video; import com.fasterxml.jackson.databind.JsonNode; @@ -31,6 +32,7 @@ public FacebookMapper() { "audio", Audio::new, "file", File::new ); + @Override public List getIdentifiers() { return List.of("facebook"); @@ -52,6 +54,12 @@ public List render(String payload) throws Exception { .elements() .forEachRemaining(attachmentNode -> { final String attachmentType = attachmentNode.get("type").textValue(); + + if (attachmentType.equals("template")) { + contents.add(new SourceTemplate(attachmentNode.get("payload").toString())); + return; + } + final String url = attachmentNode.get("payload").get("url").textValue(); final Content mediaContent = mediaContentFactory.get(attachmentType).apply(url); diff --git a/lib/java/mapping/src/test/java/co/airy/mapping/FacebookTest.java b/lib/java/mapping/src/test/java/co/airy/mapping/FacebookTest.java index 5f20f56751..8a3fcb9335 100644 --- a/lib/java/mapping/src/test/java/co/airy/mapping/FacebookTest.java +++ b/lib/java/mapping/src/test/java/co/airy/mapping/FacebookTest.java @@ -3,13 +3,16 @@ import co.airy.mapping.model.Audio; import co.airy.mapping.model.Content; import co.airy.mapping.model.File; +import co.airy.mapping.model.SourceTemplate; import co.airy.mapping.model.Video; import co.airy.mapping.sources.facebook.FacebookMapper; import org.junit.jupiter.api.Test; import org.springframework.util.StreamUtils; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -74,4 +77,16 @@ void canRenderFile() throws Exception { assertThat(contents, everyItem(isA(File.class))); assertThat(contents, everyItem(hasProperty("url", equalTo(fileUrl)))); } + + @Test + void canRenderTemplates() throws Exception { + final List templateTypes = List.of("generic"); + + for (String templateType : templateTypes) { + final String content = StreamUtils.copyToString(getClass().getClassLoader().getResourceAsStream(String.format("facebook/template_%s.json", templateType)), StandardCharsets.UTF_8); + final List contents = mapper.render(content); + assertThat(contents, hasSize(1)); + assertThat(contents, everyItem(isA(SourceTemplate.class))); + } + } } diff --git a/lib/java/mapping/src/test/resources/facebook/template_generic.json b/lib/java/mapping/src/test/resources/facebook/template_generic.json new file mode 100644 index 0000000000..9012127a66 --- /dev/null +++ b/lib/java/mapping/src/test/resources/facebook/template_generic.json @@ -0,0 +1,33 @@ +{ + "sender": { + "id": "4616529495039079" + }, + "recipient": { + "id": "778234505682382" + }, + "timestamp": 1550050473934, + "message": { + "is_echo": true, + "app_id": 123, + "mid": "l9sIeXHGFbkAL5m62DbqF2nKw8PPGcoZ0ruggAoYBrnyu8w-rcnEazyvqHqp3VeTu8k3NK-N1fCnAdzcs9kcUw", + "seq": 85083, + "attachments": [ + { + "title": "test", + "url": null, + "type": "template", + "payload": { + "template_type": "generic", + "sharable": true, + "elements": [ + { + "title": "awdadw", + "image_url": "https://airy-layer-production.s3.amazonaws.com/templates/8787c530-2f72-11e9-867e-f7de52fd949f.jpeg", + "subtitle": "adww" + } + ] + } + } + ] + } +} From c2f9ce72dc438c88161173abea90b4a13f293888 Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Tue, 19 Jan 2021 13:53:01 +0100 Subject: [PATCH 23/56] [#403] Allow leading wildcard searches for Lucene (#681) --- .../co/airy/core/api/communication/ConversationsController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/ConversationsController.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/ConversationsController.java index 6a63240053..a283d04cbe 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/ConversationsController.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/ConversationsController.java @@ -64,6 +64,8 @@ private ResponseEntity queryConversations(ConversationListRequestPayload requ final ReadOnlyKeyValueStore conversationsStore = stores.getConversationsStore(); final QueryParser simpleQueryParser = new QueryParser("id", new WhitespaceAnalyzer()); + // TODO Index display names more efficiently + simpleQueryParser.setAllowLeadingWildcard(true); final Query query; try { From e6b866017bbdf30641a905b80ed2dd0c8734a336 Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Tue, 19 Jan 2021 16:50:30 +0100 Subject: [PATCH 24/56] [#640] Enrich send message API with source template messages (#680) --- .../payload/SendMessageRequestPayload.java | 10 +-- .../communication/ConversationsInfoTest.java | 17 +--- .../communication/ConversationsListTest.java | 23 ++--- .../communication/ConversationsTagTest.java | 17 +--- .../core/api/communication/MessagesTest.java | 20 +---- .../communication/MetadataControllerTest.java | 17 +--- .../SendMessageControllerTest.java | 84 +++++++++++++------ .../api/communication/UnreadCountTest.java | 14 +--- .../WebSocketControllerTest.java | 21 +---- .../core/api/communication/util/Topics.java | 18 ++++ docs/docs/api/http.md | 41 +++++++-- .../co/airy/mapping/model/SourceTemplate.java | 3 +- .../sources/facebook/FacebookMapper.java | 2 +- lib/typescript/types/content.ts | 12 ++- 14 files changed, 150 insertions(+), 149 deletions(-) create mode 100644 backend/api/communication/src/test/java/co/airy/core/api/communication/util/Topics.java diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/payload/SendMessageRequestPayload.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/payload/SendMessageRequestPayload.java index 42fea33633..b843e80bd6 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/payload/SendMessageRequestPayload.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/payload/SendMessageRequestPayload.java @@ -1,5 +1,6 @@ package co.airy.core.api.communication.payload; +import co.airy.mapping.model.Content; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -16,12 +17,5 @@ public class SendMessageRequestPayload { private UUID conversationId; @Valid @NotNull - private MessagePayload message; - - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class MessagePayload { - private String text; - } + private Content message; } diff --git a/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsInfoTest.java b/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsInfoTest.java index e735fd2a8c..9945a8050a 100644 --- a/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsInfoTest.java +++ b/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsInfoTest.java @@ -3,10 +3,6 @@ import co.airy.avro.communication.Channel; import co.airy.avro.communication.ChannelConnectionState; import co.airy.core.api.communication.util.TestConversation; -import co.airy.kafka.schema.application.ApplicationCommunicationChannels; -import co.airy.kafka.schema.application.ApplicationCommunicationMessages; -import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; -import co.airy.kafka.schema.application.ApplicationCommunicationReadReceipts; import co.airy.kafka.test.KafkaTestHelper; import co.airy.kafka.test.junit.SharedKafkaTestResource; import co.airy.spring.core.AirySpringBootApplication; @@ -26,6 +22,8 @@ import java.util.UUID; +import static co.airy.core.api.communication.util.Topics.getTopics; +import static co.airy.core.api.communication.util.Topics.applicationCommunicationChannels; import static co.airy.test.Timing.retryOnException; import static org.hamcrest.core.Is.is; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -44,18 +42,9 @@ class ConversationsInfoTest { @Autowired private WebTestHelper webTestHelper; - private static final ApplicationCommunicationMessages applicationCommunicationMessages = new ApplicationCommunicationMessages(); - private static final ApplicationCommunicationChannels applicationCommunicationChannels = new ApplicationCommunicationChannels(); - private static final ApplicationCommunicationMetadata applicationCommunicationMetadata = new ApplicationCommunicationMetadata(); - private static final ApplicationCommunicationReadReceipts applicationCommunicationReadReceipts = new ApplicationCommunicationReadReceipts(); - @BeforeAll static void beforeAll() throws Exception { - kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, - applicationCommunicationMessages, - applicationCommunicationChannels, - applicationCommunicationMetadata, - applicationCommunicationReadReceipts); + kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, getTopics()); kafkaTestHelper.beforeAll(); } diff --git a/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsListTest.java b/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsListTest.java index 1f486ff277..9f6a6e9bf0 100644 --- a/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsListTest.java +++ b/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsListTest.java @@ -2,15 +2,11 @@ import co.airy.avro.communication.Channel; import co.airy.avro.communication.ChannelConnectionState; -import co.airy.model.metadata.MetadataKeys; import co.airy.core.api.communication.util.TestConversation; -import co.airy.kafka.schema.application.ApplicationCommunicationChannels; -import co.airy.kafka.schema.application.ApplicationCommunicationMessages; -import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; -import co.airy.kafka.schema.application.ApplicationCommunicationReadReceipts; +import co.airy.date.format.DateFormat; import co.airy.kafka.test.KafkaTestHelper; import co.airy.kafka.test.junit.SharedKafkaTestResource; -import co.airy.date.format.DateFormat; +import co.airy.model.metadata.MetadataKeys; import co.airy.spring.core.AirySpringBootApplication; import co.airy.spring.test.WebTestHelper; import com.fasterxml.jackson.databind.node.JsonNodeFactory; @@ -33,6 +29,8 @@ import java.util.Map; import java.util.UUID; +import static co.airy.core.api.communication.util.Topics.getTopics; +import static co.airy.core.api.communication.util.Topics.applicationCommunicationChannels; import static co.airy.test.Timing.retryOnException; import static java.util.Comparator.reverseOrder; import static java.util.stream.Collectors.toList; @@ -55,11 +53,6 @@ class ConversationsListTest { @Autowired private WebTestHelper webTestHelper; - private static final ApplicationCommunicationMessages applicationCommunicationMessages = new ApplicationCommunicationMessages(); - private static final ApplicationCommunicationChannels applicationCommunicationChannels = new ApplicationCommunicationChannels(); - private static final ApplicationCommunicationMetadata applicationCommunicationMetadata = new ApplicationCommunicationMetadata(); - private static final ApplicationCommunicationReadReceipts applicationCommunicationReadReceipts = new ApplicationCommunicationReadReceipts(); - private static final String firstNameToFind = "Grace"; private static final Channel defaultChannel = Channel.newBuilder() @@ -92,16 +85,10 @@ class ConversationsListTest { @BeforeAll static void beforeAll() throws Exception { - kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, - applicationCommunicationMessages, - applicationCommunicationChannels, - applicationCommunicationMetadata, - applicationCommunicationReadReceipts - ); + kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, getTopics()); kafkaTestHelper.beforeAll(); - kafkaTestHelper.produceRecord(new ProducerRecord<>(applicationCommunicationChannels.name(), defaultChannel.getId(), defaultChannel)); kafkaTestHelper.produceRecord(new ProducerRecord<>(applicationCommunicationChannels.name(), channelToFind.getId(), channelToFind)); diff --git a/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsTagTest.java b/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsTagTest.java index 6f93ab853d..368939368a 100644 --- a/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsTagTest.java +++ b/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsTagTest.java @@ -4,9 +4,6 @@ import co.airy.avro.communication.ChannelConnectionState; import co.airy.core.api.communication.util.TestConversation; import co.airy.kafka.schema.application.ApplicationCommunicationChannels; -import co.airy.kafka.schema.application.ApplicationCommunicationMessages; -import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; -import co.airy.kafka.schema.application.ApplicationCommunicationReadReceipts; import co.airy.kafka.test.KafkaTestHelper; import co.airy.kafka.test.junit.SharedKafkaTestResource; import co.airy.spring.core.AirySpringBootApplication; @@ -26,6 +23,8 @@ import java.util.UUID; +import static co.airy.core.api.communication.util.Topics.getTopics; +import static co.airy.core.api.communication.util.Topics.applicationCommunicationChannels; import static co.airy.test.Timing.retryOnException; import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; import static org.hamcrest.core.Is.is; @@ -45,19 +44,9 @@ class ConversationsTagTest { @Autowired private WebTestHelper webTestHelper; - private static final ApplicationCommunicationMessages applicationCommunicationMessages = new ApplicationCommunicationMessages(); - private static final ApplicationCommunicationChannels applicationCommunicationChannels = new ApplicationCommunicationChannels(); - private static final ApplicationCommunicationMetadata applicationCommunicationMetadata = new ApplicationCommunicationMetadata(); - private static final ApplicationCommunicationReadReceipts applicationCommunicationReadReceipts = new ApplicationCommunicationReadReceipts(); - @BeforeAll static void beforeAll() throws Exception { - kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, - applicationCommunicationMessages, - applicationCommunicationChannels, - applicationCommunicationMetadata, - applicationCommunicationReadReceipts - ); + kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, getTopics()); kafkaTestHelper.beforeAll(); } diff --git a/backend/api/communication/src/test/java/co/airy/core/api/communication/MessagesTest.java b/backend/api/communication/src/test/java/co/airy/core/api/communication/MessagesTest.java index c2f09d46a4..6763071239 100644 --- a/backend/api/communication/src/test/java/co/airy/core/api/communication/MessagesTest.java +++ b/backend/api/communication/src/test/java/co/airy/core/api/communication/MessagesTest.java @@ -4,13 +4,9 @@ import co.airy.avro.communication.ChannelConnectionState; import co.airy.avro.communication.Message; import co.airy.core.api.communication.util.TestConversation; -import co.airy.kafka.schema.application.ApplicationCommunicationChannels; -import co.airy.kafka.schema.application.ApplicationCommunicationMessages; -import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; -import co.airy.kafka.schema.application.ApplicationCommunicationReadReceipts; +import co.airy.date.format.DateFormat; import co.airy.kafka.test.KafkaTestHelper; import co.airy.kafka.test.junit.SharedKafkaTestResource; -import co.airy.date.format.DateFormat; import co.airy.spring.core.AirySpringBootApplication; import co.airy.spring.test.WebTestHelper; import org.apache.avro.specific.SpecificRecordBase; @@ -30,6 +26,8 @@ import java.util.List; import java.util.UUID; +import static co.airy.core.api.communication.util.Topics.getTopics; +import static co.airy.core.api.communication.util.Topics.applicationCommunicationChannels; import static co.airy.test.Timing.retryOnException; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.hasSize; @@ -49,19 +47,9 @@ public class MessagesTest { @Autowired private WebTestHelper webTestHelper; - private static final ApplicationCommunicationMessages applicationCommunicationMessages = new ApplicationCommunicationMessages(); - private static final ApplicationCommunicationChannels applicationCommunicationChannels = new ApplicationCommunicationChannels(); - private static final ApplicationCommunicationMetadata applicationCommunicationMetadata = new ApplicationCommunicationMetadata(); - private static final ApplicationCommunicationReadReceipts applicationCommunicationReadReceipts = new ApplicationCommunicationReadReceipts(); - @BeforeAll static void beforeAll() throws Exception { - kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, - applicationCommunicationMessages, - applicationCommunicationChannels, - applicationCommunicationMetadata, - applicationCommunicationReadReceipts - ); + kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, getTopics()); kafkaTestHelper.beforeAll(); } diff --git a/backend/api/communication/src/test/java/co/airy/core/api/communication/MetadataControllerTest.java b/backend/api/communication/src/test/java/co/airy/core/api/communication/MetadataControllerTest.java index daa4800552..74af0d44de 100644 --- a/backend/api/communication/src/test/java/co/airy/core/api/communication/MetadataControllerTest.java +++ b/backend/api/communication/src/test/java/co/airy/core/api/communication/MetadataControllerTest.java @@ -3,10 +3,6 @@ import co.airy.avro.communication.Channel; import co.airy.avro.communication.ChannelConnectionState; import co.airy.core.api.communication.util.TestConversation; -import co.airy.kafka.schema.application.ApplicationCommunicationChannels; -import co.airy.kafka.schema.application.ApplicationCommunicationMessages; -import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; -import co.airy.kafka.schema.application.ApplicationCommunicationReadReceipts; import co.airy.kafka.test.KafkaTestHelper; import co.airy.kafka.test.junit.SharedKafkaTestResource; import co.airy.spring.core.AirySpringBootApplication; @@ -26,6 +22,8 @@ import java.util.UUID; +import static co.airy.core.api.communication.util.Topics.getTopics; +import static co.airy.core.api.communication.util.Topics.applicationCommunicationChannels; import static co.airy.test.Timing.retryOnException; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -42,18 +40,9 @@ public class MetadataControllerTest { @Autowired private WebTestHelper webTestHelper; - private static final ApplicationCommunicationMessages applicationCommunicationMessages = new ApplicationCommunicationMessages(); - private static final ApplicationCommunicationChannels applicationCommunicationChannels = new ApplicationCommunicationChannels(); - private static final ApplicationCommunicationMetadata applicationCommunicationMetadata = new ApplicationCommunicationMetadata(); - private static final ApplicationCommunicationReadReceipts applicationCommunicationReadReceipts = new ApplicationCommunicationReadReceipts(); - @BeforeAll static void beforeAll() throws Exception { - kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, - applicationCommunicationMessages, - applicationCommunicationChannels, - applicationCommunicationMetadata, - applicationCommunicationReadReceipts); + kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, getTopics()); kafkaTestHelper.beforeAll(); } diff --git a/backend/api/communication/src/test/java/co/airy/core/api/communication/SendMessageControllerTest.java b/backend/api/communication/src/test/java/co/airy/core/api/communication/SendMessageControllerTest.java index 4672c2dc4b..3bb6539c35 100644 --- a/backend/api/communication/src/test/java/co/airy/core/api/communication/SendMessageControllerTest.java +++ b/backend/api/communication/src/test/java/co/airy/core/api/communication/SendMessageControllerTest.java @@ -5,14 +5,16 @@ import co.airy.avro.communication.Message; import co.airy.avro.communication.SenderType; import co.airy.core.api.communication.util.TestConversation; -import co.airy.kafka.schema.application.ApplicationCommunicationChannels; -import co.airy.kafka.schema.application.ApplicationCommunicationMessages; -import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; -import co.airy.kafka.schema.application.ApplicationCommunicationReadReceipts; import co.airy.kafka.test.KafkaTestHelper; import co.airy.kafka.test.junit.SharedKafkaTestResource; +import co.airy.mapping.ContentMapper; +import co.airy.mapping.model.Audio; +import co.airy.mapping.model.Content; +import co.airy.mapping.model.Text; import co.airy.spring.core.AirySpringBootApplication; import co.airy.spring.test.WebTestHelper; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.producer.ProducerRecord; import org.junit.jupiter.api.AfterAll; @@ -31,10 +33,15 @@ import java.util.Optional; import java.util.UUID; +import static co.airy.core.api.communication.util.Topics.applicationCommunicationChannels; +import static co.airy.core.api.communication.util.Topics.applicationCommunicationMessages; +import static co.airy.core.api.communication.util.Topics.getTopics; import static co.airy.test.Timing.retryOnException; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.everyItem; import static org.hamcrest.Matchers.is; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.hamcrest.core.Is.isA; import static org.junit.jupiter.api.Assertions.fail; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -47,11 +54,11 @@ public class SendMessageControllerTest { public static final SharedKafkaTestResource sharedKafkaTestResource = new SharedKafkaTestResource(); private static KafkaTestHelper kafkaTestHelper; - private static final String facebookConversationId = UUID.randomUUID().toString(); + private static final String conversationId = UUID.randomUUID().toString(); - private static final Channel facebookChannel = Channel.newBuilder() + private static final Channel channel = Channel.newBuilder() .setConnectionState(ChannelConnectionState.CONNECTED) - .setId("facebook-channel-id") + .setId("channel-id") .setName("channel-name") .setSource("facebook") .setSourceChannelId("ps-id") @@ -61,24 +68,17 @@ public class SendMessageControllerTest { @Autowired private WebTestHelper webTestHelper; - private static final ApplicationCommunicationMessages applicationCommunicationMessages = new ApplicationCommunicationMessages(); - private static final ApplicationCommunicationChannels applicationCommunicationChannels = new ApplicationCommunicationChannels(); - private static final ApplicationCommunicationMetadata applicationCommunicationMetadata = new ApplicationCommunicationMetadata(); - private static final ApplicationCommunicationReadReceipts applicationCommunicationReadReceipts = new ApplicationCommunicationReadReceipts(); + @Autowired + private ContentMapper contentMapper; @BeforeAll static void beforeAll() throws Exception { - kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, - applicationCommunicationMessages, - applicationCommunicationChannels, - applicationCommunicationMetadata, - applicationCommunicationReadReceipts - ); + kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, getTopics()); kafkaTestHelper.beforeAll(); - kafkaTestHelper.produceRecord(new ProducerRecord<>(applicationCommunicationChannels.name(), facebookChannel.getId(), facebookChannel)); - kafkaTestHelper.produceRecords(TestConversation.generateRecords(facebookConversationId, facebookChannel, 1)); + kafkaTestHelper.produceRecord(new ProducerRecord<>(applicationCommunicationChannels.name(), channel.getId(), channel)); + kafkaTestHelper.produceRecords(TestConversation.generateRecords(conversationId, channel, 1)); } @AfterAll @@ -91,19 +91,49 @@ void beforeEach() throws Exception { webTestHelper.waitUntilHealthy(); } + + @Test + void failsForUnknownContentSchema() throws Exception { + String payload = String.format("{\"conversation_id\":\"%s\"," + + "\"message\":{\"text\":\"answeris42\",\"type\":\"unknown\"}}", + conversationId); + final String userId = "user-id"; + + webTestHelper.post("/messages.send", payload, userId) + .andExpect(status().isBadRequest()); + } + @Test - void canSendMessages() throws Exception { - String payload = "{\"conversation_id\": \"" + facebookConversationId + "\", \"message\": { \"text\": \"answer is 42\" }}"; + void canSendTemplateMessages() throws Exception { + String payload = String.format("{\"conversation_id\":\"%s\"," + + "\"message\":{\"payload\":{\"a nested\":\"structure\"},\"type\":\"source.template\"}}", + conversationId); final String userId = "user-id"; - retryOnException(() -> webTestHelper.post("/messages.send", payload, userId).andExpect(status().isOk()), "Facebook Message was not sent"); + webTestHelper.post("/messages.send", payload, userId) + .andExpect(status().isOk()); + } + + @Test + void canSendTextMessages() throws Exception { + String payload = String.format("{\"conversation_id\":\"%s\"," + + "\"message\":{\"text\":\"answeris42\",\"type\":\"text\"}}", + conversationId); + final String userId = "user-id"; + + final String response = webTestHelper.post("/messages.send", payload, userId) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + final JsonNode responseNode = new ObjectMapper().readTree(response); + final String messageId = responseNode.get("id").textValue(); - List> records = kafkaTestHelper.consumeRecords(2, applicationCommunicationMessages.name()); - assertThat(records, hasSize(2)); + List> records = kafkaTestHelper.consumeRecords(3, applicationCommunicationMessages.name()); + assertThat(records, hasSize(3)); final Optional maybeMessage = records.stream() .map(ConsumerRecord::value) - .filter(m -> m.getSenderType().equals(SenderType.APP_USER)) + .filter(m -> m.getSenderType().equals(SenderType.APP_USER) && m.getId().equals(messageId)) .findFirst(); if (maybeMessage.isEmpty()) { @@ -111,7 +141,9 @@ void canSendMessages() throws Exception { } final Message message = maybeMessage.get(); - assertThat(message.getContent(), is("{\"text\":\"answer is 42\"}")); + final List contents = contentMapper.render(message); + assertThat(contents, hasSize(1)); + assertThat(contents, everyItem(isA(Text.class))); assertThat(message.getSenderId(), is(userId)); } } diff --git a/backend/api/communication/src/test/java/co/airy/core/api/communication/UnreadCountTest.java b/backend/api/communication/src/test/java/co/airy/core/api/communication/UnreadCountTest.java index 868ef90441..598ce5cbce 100644 --- a/backend/api/communication/src/test/java/co/airy/core/api/communication/UnreadCountTest.java +++ b/backend/api/communication/src/test/java/co/airy/core/api/communication/UnreadCountTest.java @@ -26,6 +26,8 @@ import java.util.UUID; +import static co.airy.core.api.communication.util.Topics.getTopics; +import static co.airy.core.api.communication.util.Topics.applicationCommunicationChannels; import static co.airy.test.Timing.retryOnException; import static org.hamcrest.core.IsEqual.equalTo; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -44,19 +46,9 @@ class UnreadCountTest { @Autowired private WebTestHelper webTestHelper; - private static final ApplicationCommunicationMessages applicationCommunicationMessages = new ApplicationCommunicationMessages(); - private static final ApplicationCommunicationChannels applicationCommunicationChannels = new ApplicationCommunicationChannels(); - private static final ApplicationCommunicationMetadata applicationCommunicationMetadata = new ApplicationCommunicationMetadata(); - private static final ApplicationCommunicationReadReceipts applicationCommunicationReadReceipts = new ApplicationCommunicationReadReceipts(); - @BeforeAll static void beforeAll() throws Exception { - kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, - applicationCommunicationMessages, - applicationCommunicationChannels, - applicationCommunicationMetadata, - applicationCommunicationReadReceipts - ); + kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, getTopics()); kafkaTestHelper.beforeAll(); } diff --git a/backend/api/communication/src/test/java/co/airy/core/api/communication/WebSocketControllerTest.java b/backend/api/communication/src/test/java/co/airy/core/api/communication/WebSocketControllerTest.java index 685dcfd57f..f037203f75 100644 --- a/backend/api/communication/src/test/java/co/airy/core/api/communication/WebSocketControllerTest.java +++ b/backend/api/communication/src/test/java/co/airy/core/api/communication/WebSocketControllerTest.java @@ -2,16 +2,12 @@ import co.airy.avro.communication.Channel; import co.airy.avro.communication.ChannelConnectionState; -import co.airy.model.channel.ChannelPayload; import co.airy.core.api.communication.payload.MessageUpsertPayload; import co.airy.core.api.communication.payload.UnreadCountPayload; import co.airy.core.api.communication.util.TestConversation; -import co.airy.kafka.schema.application.ApplicationCommunicationChannels; -import co.airy.kafka.schema.application.ApplicationCommunicationMessages; -import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; -import co.airy.kafka.schema.application.ApplicationCommunicationReadReceipts; import co.airy.kafka.test.KafkaTestHelper; import co.airy.kafka.test.junit.SharedKafkaTestResource; +import co.airy.model.channel.ChannelPayload; import co.airy.spring.core.AirySpringBootApplication; import co.airy.spring.jwt.Jwt; import com.fasterxml.jackson.databind.ObjectMapper; @@ -46,6 +42,8 @@ import static co.airy.core.api.communication.WebSocketController.QUEUE_CHANNEL_CONNECTED; import static co.airy.core.api.communication.WebSocketController.QUEUE_MESSAGE; import static co.airy.core.api.communication.WebSocketController.QUEUE_UNREAD_COUNT; +import static co.airy.core.api.communication.util.Topics.getTopics; +import static co.airy.core.api.communication.util.Topics.applicationCommunicationChannels; import static co.airy.test.Timing.retryOnException; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; @@ -61,12 +59,6 @@ public class WebSocketControllerTest { @RegisterExtension public static final SharedKafkaTestResource sharedKafkaTestResource = new SharedKafkaTestResource(); - - private static final ApplicationCommunicationChannels applicationCommunicationChannels = new ApplicationCommunicationChannels(); - private static final ApplicationCommunicationMessages applicationCommunicationMessages = new ApplicationCommunicationMessages(); - private static final ApplicationCommunicationMetadata applicationCommunicationMetadata = new ApplicationCommunicationMetadata(); - private static final ApplicationCommunicationReadReceipts applicationCommunicationReadReceipts = new ApplicationCommunicationReadReceipts(); - private static KafkaTestHelper kafkaTestHelper; private static boolean testDataInitialized = false; @@ -93,12 +85,7 @@ public class WebSocketControllerTest { @BeforeAll static void beforeAll() throws Exception { - kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, - applicationCommunicationMetadata, - applicationCommunicationMessages, - applicationCommunicationChannels, - applicationCommunicationReadReceipts - ); + kafkaTestHelper = new KafkaTestHelper(sharedKafkaTestResource, getTopics()); kafkaTestHelper.beforeAll(); } diff --git a/backend/api/communication/src/test/java/co/airy/core/api/communication/util/Topics.java b/backend/api/communication/src/test/java/co/airy/core/api/communication/util/Topics.java new file mode 100644 index 0000000000..c849fc7591 --- /dev/null +++ b/backend/api/communication/src/test/java/co/airy/core/api/communication/util/Topics.java @@ -0,0 +1,18 @@ +package co.airy.core.api.communication.util; + +import co.airy.kafka.schema.Topic; +import co.airy.kafka.schema.application.ApplicationCommunicationChannels; +import co.airy.kafka.schema.application.ApplicationCommunicationMessages; +import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; +import co.airy.kafka.schema.application.ApplicationCommunicationReadReceipts; + +public class Topics { + public static final ApplicationCommunicationMessages applicationCommunicationMessages = new ApplicationCommunicationMessages(); + public static final ApplicationCommunicationChannels applicationCommunicationChannels = new ApplicationCommunicationChannels(); + public static final ApplicationCommunicationMetadata applicationCommunicationMetadata = new ApplicationCommunicationMetadata(); + public static final ApplicationCommunicationReadReceipts applicationCommunicationReadReceipts = new ApplicationCommunicationReadReceipts(); + + public static Topic[] getTopics() { + return new Topic[]{applicationCommunicationMessages, applicationCommunicationChannels, applicationCommunicationMetadata, applicationCommunicationReadReceipts}; + } +} diff --git a/docs/docs/api/http.md b/docs/docs/api/http.md index 7e14f9814d..318cd23b1f 100644 --- a/docs/docs/api/http.md +++ b/docs/docs/api/http.md @@ -352,13 +352,45 @@ This is a [paginated](#pagination) endpoint. Messages are sorted from oldest to Sends a message to a conversation and returns a payload. -**Sample request** +**Sending a text message** ```json5 { "conversation_id": "a688d36c-a85e-44af-bc02-4248c2c97622", "message": { - "text": "{String}" + "text": "Hello World", + "type": "text" + } +} +``` + +**Sending an attachment message** + +```json5 +{ + "conversation_id": "a688d36c-a85e-44af-bc02-4248c2c97622", + "message": { + "url": "http://example.org/myfile", + "type": "image|video|audio|file" + } +} +``` + +**Sending source templates** + +Some sources support sending templates, which can be used to display rich content such as buttons or cards. You +can send source templates by setting the type to `source.template`. Please refer to the source documentation +to see the expected values for the `payload` field. + +```json5 +{ + "conversation_id": "a688d36c-a85e-44af-bc02-4248c2c97622", + "message": { + "payload": { + "template_type": "buttons", + "buttons": ["Welcome to our shop"] + }, + "type": "source.template" } } ``` @@ -372,12 +404,9 @@ Sends a message to a conversation and returns a payload. { "text": "{String}", "type": "text" - // Determines the schema of the content } ], - // typed source message model - "state": "{String}", - // delivery state of message, one of PENDING, FAILED, DELIVERED + "state": "pending|failed|delivered", "sender_type": "{string/enum}", // See glossary "sent_at": "{string}" diff --git a/lib/java/mapping/src/main/java/co/airy/mapping/model/SourceTemplate.java b/lib/java/mapping/src/main/java/co/airy/mapping/model/SourceTemplate.java index 1270e05252..84456cdc02 100644 --- a/lib/java/mapping/src/main/java/co/airy/mapping/model/SourceTemplate.java +++ b/lib/java/mapping/src/main/java/co/airy/mapping/model/SourceTemplate.java @@ -1,5 +1,6 @@ package co.airy.mapping.model; +import com.fasterxml.jackson.databind.JsonNode; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -16,5 +17,5 @@ @EqualsAndHashCode(callSuper = false) public class SourceTemplate extends Content implements Serializable { @NotNull - private String payload; + private JsonNode payload; } diff --git a/lib/java/mapping/src/main/java/co/airy/mapping/sources/facebook/FacebookMapper.java b/lib/java/mapping/src/main/java/co/airy/mapping/sources/facebook/FacebookMapper.java index 34db2bbdd7..85705ae58b 100644 --- a/lib/java/mapping/src/main/java/co/airy/mapping/sources/facebook/FacebookMapper.java +++ b/lib/java/mapping/src/main/java/co/airy/mapping/sources/facebook/FacebookMapper.java @@ -56,7 +56,7 @@ public List render(String payload) throws Exception { final String attachmentType = attachmentNode.get("type").textValue(); if (attachmentType.equals("template")) { - contents.add(new SourceTemplate(attachmentNode.get("payload").toString())); + contents.add(new SourceTemplate(attachmentNode.get("payload"))); return; } diff --git a/lib/typescript/types/content.ts b/lib/typescript/types/content.ts index 068d883a86..1ad4c12925 100644 --- a/lib/typescript/types/content.ts +++ b/lib/typescript/types/content.ts @@ -1,13 +1,14 @@ /* tslint:disable */ /* eslint-disable */ -// Generated using typescript-generator version 2.26.723 on 2021-01-13 15:17:35. + +// Generated using typescript-generator version 2.26.723 on 2021-01-19 11:27:56. export interface Audio extends Content, DataUrl { type: 'audio'; } export interface Content { - type: 'audio' | 'file' | 'image' | 'text' | 'video'; + type: 'audio' | 'file' | 'image' | 'source.template' | 'text' | 'video'; } export interface DataUrl { @@ -22,6 +23,11 @@ export interface Image extends Content, DataUrl { type: 'image'; } +export interface SourceTemplate extends Content { + type: 'source.template'; + payload: any; +} + export interface Text extends Content { type: 'text'; text: string; @@ -31,4 +37,4 @@ export interface Video extends Content, DataUrl { type: 'video'; } -export type ContentUnion = Text | Audio | File | Image | Video; +export type ContentUnion = Text | Audio | File | Image | Video | SourceTemplate; From d270fd68333a24e8765960e40f8d860ba857bba2 Mon Sep 17 00:00:00 2001 From: Bodo Tasche Date: Wed, 20 Jan 2021 11:28:37 +0100 Subject: [PATCH 25/56] [#402] Tag conversations (#682) closes #402 --- frontend/demo/src/App.tsx | 29 ++- .../demo/src/actions/conversations/index.ts | 28 +++ frontend/demo/src/actions/tags/index.tsx | 2 +- .../demo/src/components/ColorSelector.tsx | 12 +- .../Tag/index.module.scss} | 10 +- .../Tags/Tag.tsx => components/Tag/index.tsx} | 33 +-- .../ConversationMetadata/index.module.scss | 66 ++++++ .../Messenger/ConversationMetadata/index.tsx | 206 ++++++++++++++++++ .../Messenger/MessageList/index.module.scss | 1 + .../Inbox/Messenger/MessageList/index.tsx | 39 ++-- .../Messenger/MessengerContainer/index.tsx | 40 ++-- .../{index.scss => index.module.scss} | 0 .../demo/src/pages/Inbox/Messenger/index.tsx | 14 +- frontend/demo/src/pages/Tags/FAKESETTINGS.ts | 5 +- .../demo/src/pages/Tags/SimpleTagForm.tsx | 2 +- frontend/demo/src/pages/Tags/TableRow.tsx | 18 +- frontend/demo/src/pages/Tags/index.tsx | 3 - .../src/reducers/data/conversations/index.ts | 47 +++- .../demo/src/reducers/data/settings/index.ts | 12 +- frontend/demo/src/types/tag.ts | 16 -- lib/typescript/httpclient/index.ts | 36 ++- .../payload/TagConversationRequestPayload.ts | 4 + .../UntagConversationRequestPayload.ts | 4 + lib/typescript/httpclient/payload/index.ts | 8 +- yarn.lock | 16 +- 25 files changed, 509 insertions(+), 142 deletions(-) rename frontend/demo/src/{pages/Tags/Tag.module.scss => components/Tag/index.module.scss} (91%) rename frontend/demo/src/{pages/Tags/Tag.tsx => components/Tag/index.tsx} (61%) create mode 100644 frontend/demo/src/pages/Inbox/Messenger/ConversationMetadata/index.module.scss create mode 100644 frontend/demo/src/pages/Inbox/Messenger/ConversationMetadata/index.tsx rename frontend/demo/src/pages/Inbox/Messenger/{index.scss => index.module.scss} (100%) create mode 100644 lib/typescript/httpclient/payload/TagConversationRequestPayload.ts create mode 100644 lib/typescript/httpclient/payload/UntagConversationRequestPayload.ts diff --git a/frontend/demo/src/App.tsx b/frontend/demo/src/App.tsx index fc7e2b5779..a00b2b3e0c 100644 --- a/frontend/demo/src/App.tsx +++ b/frontend/demo/src/App.tsx @@ -11,13 +11,27 @@ import Tags from './pages/Tags'; import Logout from './pages/Logout'; import NotFound from './pages/NotFound'; import Sidebar from './components/Sidebar'; - +import {fakeSettingsAPICall} from './actions/settings'; import {StateModel} from './reducers'; import {INBOX_ROUTE, CHANNELS_ROUTE, LOGIN_ROUTE, LOGOUT_ROUTE, ROOT_ROUTE, TAGS_ROUTE} from './routes/routes'; import styles from './App.module.scss'; +const mapStateToProps = (state: StateModel, ownProps: RouteComponentProps) => { + return { + user: state.data.user, + pathname: ownProps.location.pathname, + token: state.data.user.token, + }; +}; + +const mapDispatchToProps = { + fakeSettingsAPICall, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + const publicRoutes = [LOGIN_ROUTE]; const shouldRedirect = (path: string) => @@ -27,6 +41,9 @@ class App extends Component & RouteComponentPro constructor(props: ConnectedProps & RouteComponentProps) { super(props); } + componentDidMount() { + this.props.fakeSettingsAPICall(); + } get isAuthSuccess() { return this.props.user.token && this.props.user.token !== ''; @@ -73,14 +90,4 @@ class App extends Component & RouteComponentPro } } -const mapStateToProps = (state: StateModel, ownProps: RouteComponentProps) => { - return { - user: state.data.user, - pathname: ownProps.location.pathname, - token: state.data.user.token, - }; -}; - -const connector = connect(mapStateToProps, null); - export default withRouter(connector(App)); diff --git a/frontend/demo/src/actions/conversations/index.ts b/frontend/demo/src/actions/conversations/index.ts index 8a90651a37..6edca80f17 100644 --- a/frontend/demo/src/actions/conversations/index.ts +++ b/frontend/demo/src/actions/conversations/index.ts @@ -11,6 +11,8 @@ export const CONVERSATIONS_MERGE = '@@conversations/MERGE'; export const CONVERSATION_ADD_ERROR = '@@conversations/ADD_ERROR_TO_CONVERSATION'; export const CONVERSATION_REMOVE_ERROR = '@@conversations/REMOVE_ERROR_FROM_CONVERSATION'; export const CONVERSATION_READ = '@@conversations/CONVERSATION_READ'; +export const CONVERSATION_ADD_TAG = '@@conversations/CONVERSATION_ADD_TAG'; +export const CONVERSATION_REMOVE_TAG = '@@conversations/CONVERSATION_REMOVE_TAG'; export const loadingConversationAction = createAction(CONVERSATION_LOADING, resolve => (conversationId: string) => resolve(conversationId) @@ -38,6 +40,16 @@ export const removeErrorFromConversationAction = createAction( resolve => (conversationId: string) => resolve({conversationId}) ); +export const addTagToConversationAction = createAction( + CONVERSATION_ADD_TAG, + resolve => (conversationId: string, tagId: string) => resolve({conversationId, tagId}) +); + +export const removeTagFromConversationAction = createAction( + CONVERSATION_REMOVE_TAG, + resolve => (conversationId: string, tagId: string) => resolve({conversationId, tagId}) +); + export function listConversations() { return async (dispatch: Dispatch) => { dispatch(loadingConversationsAction()); @@ -72,3 +84,19 @@ export function readConversations(conversationId: string) { HttpClientInstance.readConversations(conversationId).then(() => dispatch(readConversationsAction(conversationId))); }; } + +export function addTagToConversation(conversationId: string, tagId: string) { + return function(dispatch: Dispatch) { + HttpClientInstance.tagConversation({conversationId, tagId}).then(() => + dispatch(addTagToConversationAction(conversationId, tagId)) + ); + }; +} + +export function removeTagFromConversation(conversationId: string, tagId: string) { + return function(dispatch: Dispatch) { + HttpClientInstance.untagConversation({conversationId, tagId}).then(() => + dispatch(removeTagFromConversationAction(conversationId, tagId)) + ); + }; +} diff --git a/frontend/demo/src/actions/tags/index.tsx b/frontend/demo/src/actions/tags/index.tsx index c9c23def20..a096ecec99 100644 --- a/frontend/demo/src/actions/tags/index.tsx +++ b/frontend/demo/src/actions/tags/index.tsx @@ -31,7 +31,7 @@ export function createTag(requestPayload: CreateTagRequestPayload) { return HttpClientInstance.createTag(requestPayload) .then((response: Tag) => { dispatch(addTagAction(response)); - return Promise.resolve(true); + return Promise.resolve(response); }) .catch((error: string) => { dispatch(errorTagAction(error)); diff --git a/frontend/demo/src/components/ColorSelector.tsx b/frontend/demo/src/components/ColorSelector.tsx index eeb6b6e32a..e9a6b87c5e 100644 --- a/frontend/demo/src/components/ColorSelector.tsx +++ b/frontend/demo/src/components/ColorSelector.tsx @@ -1,7 +1,7 @@ import React, {useCallback} from 'react'; import {connect} from 'react-redux'; import {RootState} from '../reducers'; -import {TagSettings} from '../types'; +import {Settings} from '../reducers/data/settings'; import styles from './ColorSelector.module.scss'; @@ -13,12 +13,12 @@ type ColorSelectorProps = { }; type ColorSelectorState = { - tagSettings: TagSettings; + settings: Settings; }; -const ColorSelector = ({handleUpdate, color, editing, id, tagSettings}: ColorSelectorProps & ColorSelectorState) => { - const getColorValue = useCallback((color: string) => (tagSettings && tagSettings.colors[color].default) || '1578D4', [ - tagSettings, +const ColorSelector = ({handleUpdate, color, editing, id, settings}: ColorSelectorProps & ColorSelectorState) => { + const getColorValue = useCallback((color: string) => (settings && settings.colors[color].default) || '1578D4', [ + settings, ]); return ( @@ -85,7 +85,7 @@ const ColorSelector = ({handleUpdate, color, editing, id, tagSettings}: ColorSel const mapStateToProps = (state: RootState) => { return { - tagSettings: state.data.settings, + settings: state.data.settings, }; }; diff --git a/frontend/demo/src/pages/Tags/Tag.module.scss b/frontend/demo/src/components/Tag/index.module.scss similarity index 91% rename from frontend/demo/src/pages/Tags/Tag.module.scss rename to frontend/demo/src/components/Tag/index.module.scss index b18c1dcd24..94c42ce3dc 100644 --- a/frontend/demo/src/pages/Tags/Tag.module.scss +++ b/frontend/demo/src/components/Tag/index.module.scss @@ -34,13 +34,9 @@ } .closeButton { - display: inline-block; - padding-left: 4px; - - svg { - path { - fill: #fff; - } + margin-left: 4px; + path { + fill: #fff; } } diff --git a/frontend/demo/src/pages/Tags/Tag.tsx b/frontend/demo/src/components/Tag/index.tsx similarity index 61% rename from frontend/demo/src/pages/Tags/Tag.tsx rename to frontend/demo/src/components/Tag/index.tsx index fbc6495481..c951c78f88 100644 --- a/frontend/demo/src/pages/Tags/Tag.tsx +++ b/frontend/demo/src/components/Tag/index.tsx @@ -1,34 +1,27 @@ import React from 'react'; import {connect} from 'react-redux'; import {Tag as TagModel} from 'httpclient'; -import {TagSettings} from '../../types'; +import {Settings} from '../../reducers/data/settings'; -import close from '../../assets/images/icons/close.svg'; -import styles from './Tag.module.scss'; +import {ReactComponent as Close} from '../../assets/images/icons/close.svg'; +import styles from './index.module.scss'; import {RootState} from '../../reducers'; type TagProps = { tag: TagModel; expanded?: boolean; onClick?: () => void; - removeTagFromContact?: () => void; + removeTag?: () => void; variant?: 'default' | 'light'; type?: string; }; type tagState = { - tagSettings: TagSettings; + settings: Settings; }; -export const Tag = ({ - tag, - expanded, - variant, - onClick, - removeTagFromContact, - tagSettings, -}: TagProps & tagState): JSX.Element => { - const tagColor = (tagSettings && tagSettings.colors[tag.color]) || { +export const Tag = ({tag, expanded, variant, onClick, removeTag, settings}: TagProps & tagState): JSX.Element => { + const tagColor = (settings && settings.colors[tag.color]) || { background: 'F1FAFF', border: '1578D4', default: '1578D4', @@ -50,14 +43,12 @@ export const Tag = ({ return (
{tag.name} - {removeTagFromContact && ( - - + {removeTag && ( + + )}
@@ -67,7 +58,7 @@ export const Tag = ({ const mapStateToProps = (state: RootState) => { return { - tagSettings: state.data.settings, + settings: state.data.settings, }; }; diff --git a/frontend/demo/src/pages/Inbox/Messenger/ConversationMetadata/index.module.scss b/frontend/demo/src/pages/Inbox/Messenger/ConversationMetadata/index.module.scss new file mode 100644 index 0000000000..c81b76f9cd --- /dev/null +++ b/frontend/demo/src/pages/Inbox/Messenger/ConversationMetadata/index.module.scss @@ -0,0 +1,66 @@ +@import '../../../../assets/scss/colors.scss'; +@import '../../../../assets/scss/fonts.scss'; + +.content { + display: flex; + width: 290px; + height: auto; + flex-direction: column; + overflow: hidden; + background-color: #fff; + margin: 16px 8px 0 8px; + padding: 16px; + border-top-left-radius: 8px; + border-top-right-radius: 8px; +} + +.contact { + border-bottom: 1px solid var(--color-light-gray); + padding-bottom: 16px; +} + +.avatarImage { + width: 80px; + height: 80px; + margin: 0 auto 16px auto; +} + +.displayName { + @include font-m; + text-align: center; + color: var(--color-text-contrast); +} + +.tags { + border-bottom: 1px solid var(--color-light-gray); + padding-bottom: 16px; +} + +.tagsHeader { + display: flex; + margin: 8px 0; + justify-content: space-between; +} + +.tagsHeaderTitle { + font-weight: bold; +} + +.addTags { + padding: 8px; + width: 260px; +} + +.addTagsRow { + display: flex; + margin: 4px 0; + justify-content: space-between; +} + +.addTagsDescription { + margin: 8px 0; +} + +.addTagsButtonRow { + margin: 8px 0; +} diff --git a/frontend/demo/src/pages/Inbox/Messenger/ConversationMetadata/index.tsx b/frontend/demo/src/pages/Inbox/Messenger/ConversationMetadata/index.tsx new file mode 100644 index 0000000000..e11ada183d --- /dev/null +++ b/frontend/demo/src/pages/Inbox/Messenger/ConversationMetadata/index.tsx @@ -0,0 +1,206 @@ +import React, {FormEvent, useEffect, useState} from 'react'; +import _, {connect, ConnectedProps} from 'react-redux'; +import {Conversation, Tag as TagModel, TagColor} from 'httpclient'; + +import {createTag, listTags} from '../../../../actions/tags'; +import {addTagToConversation, removeTagFromConversation} from '../../../../actions/conversations'; +import AvatarImage from '../../../../components/AvatarImage'; +import ColorSelector from '../../../../components/ColorSelector'; +import Dialog from '../../../../components/Dialog'; +import {StateModel} from '../../../../reducers'; + +import styles from './index.module.scss'; +import Tag from '../../../../components/Tag'; +import {Button, Input, LinkButton} from '@airyhq/components'; + +type ConversationMetadataProps = {conversation: Conversation} & ConnectedProps; + +const mapStateToProps = (state: StateModel) => { + return { + tags: state.data.tags.all, + }; +}; + +const mapDispatchToProps = { + createTag, + listTags, + addTagToConversation, + removeTagFromConversation, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +const ConversationMetadata = (props: ConversationMetadataProps) => { + const {tags, createTag, conversation, listTags, addTagToConversation, removeTagFromConversation} = props; + const [showTagsDialog, setShowTagsDialog] = useState(false); + const [color, setColor] = useState('tag-blue'); + const [tagName, setTagName] = useState(''); + + useEffect(() => { + if (tags.length == 0) { + listTags(); + } + }); + + const showAddTags = () => { + setTagName(''); + setShowTagsDialog(true); + }; + + const addTag = (tag: TagModel) => { + addTagToConversation(conversation.id, tag.id); + setShowTagsDialog(false); + }; + + const removeTag = (tag: TagModel) => { + removeTagFromConversation(conversation.id, tag.id); + }; + + const filterForUnusedTags = (tags: TagModel[]): TagModel[] => { + return tags.filter(tag => !conversation.tags.includes(tag.id)); + }; + + const filterForUsedTags = (tags: TagModel[]): TagModel[] => { + return tags.filter(tag => conversation.tags.includes(tag.id)); + }; + + const tagSorter = (tagA: TagModel, tagB: TagModel) => { + if (tagA.name < tagB.name) { + return -1; + } + if (tagA.name > tagB.name) { + return 1; + } + + return 0; + }; + + const checkIfExists = (value: string) => { + const usedTags = filterForUsedTags(tags); + if (value.length == 0) { + return true; + } + if (usedTags.find(tag => tag.name === value)) { + return 'Tag already added'; + } + + return true; + }; + + const getFilterdTags = (): TagModel[] => { + return filterForUnusedTags(tags) + .sort(tagSorter) + .filter(tag => tag.name.startsWith(tagName)); + }; + + const submitForm = (event: FormEvent) => { + event.preventDefault(); + const filteredTags = getFilterdTags(); + + if (filteredTags.length == 1) { + addTag(filteredTags[0]); + } else if (filteredTags.length == 0 && tagName.trim().length > 0) { + createTag({name: tagName.trim(), color}).then((tag: TagModel) => { + if (tag) { + addTag(tag); + } + }); + } + }; + + const renderTagsDialog = () => { + const filteredTags = getFilterdTags(); + + return ( + setShowTagsDialog(false)}> +
+
Add a tag
+ ) => { + setTagName(e.target.value); + }} + height={32} + value={tagName} + name="tag_name" + placeholder="Please enter a tag name" + autoComplete="off" + autoFocus={true} + fontClass="font-s" + minLength={1} + maxLength={50} + validation={checkIfExists} + showErrors={true} + /> + {filteredTags.length > 0 ? ( + filteredTags.map(tag => { + return ( +
+ + addTag(tag)}> + Add + +
+ ); + }) + ) : ( +
+
+ +
+

Pick a color

+ ) => setColor(e.target.value as TagColor)} + color={color} + editing={true} + /> +
+ +
+
+ )} +
+
+ ); + }; + + const findTag = (tagId: string): TagModel => { + return tags.find(tag => tag.id === tagId); + }; + + return ( +
+ {conversation && ( +
+
+
+ +
+ +
{conversation.contact.displayName}
+
+
+
+

Tags

+ {showTagsDialog ? 'Close' : '+ Add Tag'} +
+ + {showTagsDialog && renderTagsDialog()} + +
+ {tags && + conversation.tags + .map(tagId => findTag(tagId)) + .sort(tagSorter) + .map(tag => tag && removeTag(tag)} />)} +
+
+
+ )} +
+ ); +}; + +export default connector(ConversationMetadata); diff --git a/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.module.scss b/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.module.scss index 6329092f1c..c457fed0d1 100644 --- a/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.module.scss +++ b/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.module.scss @@ -18,4 +18,5 @@ border-radius: 4px; background-color: var(--color-background-gray); color: var(--color-text-gray); + width: max-content; } diff --git a/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.tsx b/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.tsx index 918b1da09f..ff781e0a90 100644 --- a/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.tsx +++ b/frontend/demo/src/pages/Inbox/Messenger/MessageList/index.tsx @@ -1,9 +1,8 @@ -import React, {useEffect, useState, createRef} from 'react'; -import {RouteComponentProps, useParams} from 'react-router-dom'; +import React, {useEffect, createRef} from 'react'; import _, {connect, ConnectedProps} from 'react-redux'; import _redux from 'redux'; -import {Message, SenderType} from 'httpclient'; +import {Conversation, Message, SenderType} from 'httpclient'; import {StateModel} from '../../../../reducers'; import {MessageById} from '../../../../reducers/data/messages'; @@ -11,13 +10,11 @@ import {MessageById} from '../../../../reducers/data/messages'; import MessageListItem from '../MessengerListItem'; import {listMessages} from '../../../../actions/messages'; -import {allConversationSelector} from '../../../../selectors/conversations'; import styles from './index.module.scss'; import {formatDateOfMessage} from '../../../../services/format/date'; -type MessageListProps = {conversationId: string} & ConnectedProps & - RouteComponentProps<{conversationId: string}>; +type MessageListProps = {conversation: Conversation} & ConnectedProps; const messagesMapToArray = ( messageInfo: {[conversationId: string]: MessageById}, @@ -30,10 +27,9 @@ const messagesMapToArray = ( return []; }; -const mapStateToProps = (state: StateModel, ownProps: {conversationId: string}) => { +const mapStateToProps = (state: StateModel, ownProps: {conversation: Conversation}) => { return { - conversations: allConversationSelector(state), - messages: messagesMapToArray(state.data.messages.all, ownProps.conversationId), + messages: messagesMapToArray(state.data.messages.all, ownProps.conversation && ownProps.conversation.id), }; }; @@ -44,21 +40,13 @@ const mapDispatchToProps = { const connector = connect(mapStateToProps, mapDispatchToProps); const MessageList = (props: MessageListProps) => { - const {conversations, listMessages, messages} = props; - const conversationIdParams = useParams(); - const currentConversationId = conversationIdParams[Object.keys(conversationIdParams)[0]]; - const [currentConversation, setCurrentConversation] = useState(null); - + const {listMessages, messages, conversation} = props; const messageListRef = createRef(); useEffect(() => { - currentConversationId && listMessages(currentConversationId); + conversation && listMessages(conversation.id); scrollBottom(); - }, [currentConversationId]); - - useEffect(() => { - setCurrentConversation(conversations.find(item => item && item.id === currentConversationId)); - }, [currentConversationId, conversations]); + }, [conversation && conversation.id]); const scrollBottom = () => { messageListRef.current.scrollTop = messageListRef.current.scrollHeight; @@ -87,18 +75,19 @@ const MessageList = (props: MessageListProps) => { const nextIsSameUser = nextMessage ? isContact(message) == isContact(nextMessage) : false; return ( - <> +
{hasDateChanged(prevMessage, message) && ( -
{formatDateOfMessage(message)}
+
+ {formatDateOfMessage(message)} +
)} - +
); })}
diff --git a/frontend/demo/src/pages/Inbox/Messenger/MessengerContainer/index.tsx b/frontend/demo/src/pages/Inbox/Messenger/MessengerContainer/index.tsx index 3e1c13a6c5..b238fa13d3 100644 --- a/frontend/demo/src/pages/Inbox/Messenger/MessengerContainer/index.tsx +++ b/frontend/demo/src/pages/Inbox/Messenger/MessengerContainer/index.tsx @@ -1,9 +1,12 @@ -import React from 'react'; +import React, {useEffect, useState} from 'react'; import _, {connect, ConnectedProps} from 'react-redux'; +import {useParams} from 'react-router-dom'; + import {StateModel} from '../../../../reducers'; import MessageList from '../MessageList'; import {ReactComponent as EmptyStateImage} from '../../../../assets/images/empty-state/inbox-empty-state.svg'; import styles from './index.module.scss'; +import ConversationMetadata from '../ConversationMetadata'; const mapStateToProps = (state: StateModel) => { return { @@ -13,23 +16,32 @@ const mapStateToProps = (state: StateModel) => { const connector = connect(mapStateToProps, null); -type MessengerContainerProps = {match: any} & ConnectedProps; +type MessengerContainerProps = ConnectedProps; const MessengerContainer = (props: MessengerContainerProps) => { - const {conversations, match} = props; + const {conversations} = props; + const {conversationId} = useParams<{conversationId: string}>(); + const [currentConversation, setCurrentConversation] = useState(null); + + useEffect(() => { + setCurrentConversation(conversations[conversationId]); + }, [conversationId, conversations]); return ( -
- {!conversations ? ( -
-

Your conversations will appear here as soon as a contact messages you.

-

Airy Messenger only shows new conversations from the moment you connect at least one channel.

- -
- ) : ( - - )} -
+ <> +
+ {!conversations ? ( +
+

Your conversations will appear here as soon as a contact messages you.

+

Airy Messenger only shows new conversations from the moment you connect at least one channel.

+ +
+ ) : ( + + )} +
+ + ); }; diff --git a/frontend/demo/src/pages/Inbox/Messenger/index.scss b/frontend/demo/src/pages/Inbox/Messenger/index.module.scss similarity index 100% rename from frontend/demo/src/pages/Inbox/Messenger/index.scss rename to frontend/demo/src/pages/Inbox/Messenger/index.module.scss diff --git a/frontend/demo/src/pages/Inbox/Messenger/index.tsx b/frontend/demo/src/pages/Inbox/Messenger/index.tsx index a3ac4bdef5..0024878342 100644 --- a/frontend/demo/src/pages/Inbox/Messenger/index.tsx +++ b/frontend/demo/src/pages/Inbox/Messenger/index.tsx @@ -1,4 +1,4 @@ -import React, {Fragment} from 'react'; +import React from 'react'; import {Route, withRouter, Redirect, RouteComponentProps} from 'react-router-dom'; import _, {connect, ConnectedProps} from 'react-redux'; @@ -7,7 +7,7 @@ import ConversationList from '../ConversationList'; import {StateModel} from '../../../reducers'; import {AllConversationsState} from '../../../reducers/data/conversations'; -import './index.scss'; +import styles from './index.module.scss'; import MessengerContainer from './MessengerContainer'; const mapStateToProps = (state: StateModel) => { @@ -35,20 +35,16 @@ const Messenger = (props: ConnectedProps & RouteComponentProps } return ( -
+
{!!conversations.items && ( -
+
)} ( - - - - )} + render={props => } />
); diff --git a/frontend/demo/src/pages/Tags/FAKESETTINGS.ts b/frontend/demo/src/pages/Tags/FAKESETTINGS.ts index 05a31a2619..4442236d92 100644 --- a/frontend/demo/src/pages/Tags/FAKESETTINGS.ts +++ b/frontend/demo/src/pages/Tags/FAKESETTINGS.ts @@ -1,11 +1,12 @@ -export const fakeData = () => { +import {Settings} from '../../reducers/data/settings'; + +export const fakeData = (): Settings => { return { colors: { 'tag-green': {default: '0E764F', background: 'F5FFFB', font: '0E764F', position: 3, border: '0E764F'}, 'tag-blue': {default: '1578D4', background: 'F1FAFF', font: '1578D4', position: 1, border: '1578D4'}, 'tag-red': {default: 'E0243A', background: 'FFF7F9', font: 'E0243A', position: 2, border: 'E0243A'}, 'tag-purple': {default: '730A80', background: 'FEF7FF', font: '730A80', position: 4, border: '730A80'}, - enabled: true, }, }; }; diff --git a/frontend/demo/src/pages/Tags/SimpleTagForm.tsx b/frontend/demo/src/pages/Tags/SimpleTagForm.tsx index 38da9472d4..4941ca39c3 100644 --- a/frontend/demo/src/pages/Tags/SimpleTagForm.tsx +++ b/frontend/demo/src/pages/Tags/SimpleTagForm.tsx @@ -8,7 +8,7 @@ import {Button, Input} from '@airyhq/components'; import Dialog from '../../components/Dialog'; import ColorSelector from '../../components/ColorSelector'; -import Tag from '../../pages/Tags/Tag'; +import Tag from '../../components/Tag'; import {Tag as TagModel, TagColor} from 'httpclient'; import styles from './SimpleTagForm.module.scss'; diff --git a/frontend/demo/src/pages/Tags/TableRow.tsx b/frontend/demo/src/pages/Tags/TableRow.tsx index 350ee6e2e4..e786fc3664 100644 --- a/frontend/demo/src/pages/Tags/TableRow.tsx +++ b/frontend/demo/src/pages/Tags/TableRow.tsx @@ -7,19 +7,19 @@ import {Button, LinkButton} from '@airyhq/components'; import {ReactComponent as EditIcon} from '../../assets/images/icons/edit.svg'; import {ReactComponent as TrashIcon} from '../../assets/images/icons/trash.svg'; import ColorSelector from '../../components/ColorSelector'; -import Tag from './Tag'; +import Tag from '../../components/Tag'; import {Tag as TagModel, TagColor} from 'httpclient'; -import {TagSettings} from '../../types'; +import {Settings} from '../../reducers/data/settings'; import {RootState} from '../../reducers'; type TableRowProps = { tag: TagModel; - tagSettings: TagSettings; + settings: Settings; showModal(label: string, id: string, name: string): void; } & ConnectedProps; const TableRowComponent = (props: TableRowProps) => { - const {tag, updateTag, tagSettings, showModal} = props; + const {tag, updateTag, settings, showModal} = props; const [tagState, setTagState] = useState({ edit: false, @@ -81,9 +81,11 @@ const TableRowComponent = (props: TableRowProps) => { [showModal, tag] ); - const getColorValue = useCallback((color: string) => (tagSettings && tagSettings.colors[color].default) || '1578D4', [ - tagSettings, - ]); + const getColorValue = useCallback( + (color: string) => + (settings && settings.colors && settings.colors[color] && settings.colors[color].default) || '1578D4', + [settings] + ); const isEditing = tagState.edit && tagState.id === tag.id; @@ -145,7 +147,7 @@ const TableRowComponent = (props: TableRowProps) => { const mapStateToProps = (state: RootState) => { return { - tagSettings: state.data.settings, + settings: state.data.settings, }; }; diff --git a/frontend/demo/src/pages/Tags/index.tsx b/frontend/demo/src/pages/Tags/index.tsx index 318101edf4..1e9fd344f5 100644 --- a/frontend/demo/src/pages/Tags/index.tsx +++ b/frontend/demo/src/pages/Tags/index.tsx @@ -6,7 +6,6 @@ import {SettingsModal, LinkButton, Button, SearchField, Input} from '@airyhq/com import plus from '../../assets/images/icons/plus.svg'; import {listTags, deleteTag, filterTags, errorTag} from '../../actions/tags'; -import {fakeSettingsAPICall} from '../../actions/settings'; import {filteredTags} from '../../selectors/tags'; import {Tag} from 'httpclient'; import {ModalType} from '../../types'; @@ -34,7 +33,6 @@ class Tags extends Component, typeof initialSta componentDidMount() { this.props.listTags(); - this.props.fakeSettingsAPICall(); this.props.filterTags(''); } @@ -217,7 +215,6 @@ const mapDispatchToProps = { deleteTag, errorTag, filterTags, - fakeSettingsAPICall, }; const connector = connect(mapStateToProps, mapDispatchToProps); diff --git a/frontend/demo/src/reducers/data/conversations/index.ts b/frontend/demo/src/reducers/data/conversations/index.ts index e3998359e0..90d9d39184 100644 --- a/frontend/demo/src/reducers/data/conversations/index.ts +++ b/frontend/demo/src/reducers/data/conversations/index.ts @@ -1,6 +1,6 @@ import {ActionType, getType} from 'typesafe-actions'; import {combineReducers} from 'redux'; -import {cloneDeep} from 'lodash-es'; +import {cloneDeep, uniq} from 'lodash-es'; import {Conversation, Message} from 'httpclient'; import {ResponseMetadataPayload} from 'httpclient/payload/ResponseMetadataPayload'; @@ -99,6 +99,45 @@ const initialState: AllConversationsState = { }, }; +const addTagToConversation = (state: AllConversationsState, conversationId, tagId) => { + const conversation: Conversation = state.items[conversationId]; + if (conversation) { + const tags: string[] = [...state.items[conversationId].tags]; + tags.push(tagId); + + return { + ...state, + items: { + ...state.items, + [conversation.id]: { + ...conversation, + tags: uniq(tags), + }, + }, + }; + } + + return state; +}; + +const removeTagFromConversation = (state: AllConversationsState, conversationId, tagId) => { + const conversation: Conversation = state.items[conversationId]; + if (conversation) { + return { + ...state, + items: { + ...state.items, + [conversation.id]: { + ...conversation, + tags: conversation.tags.filter(tag => tag !== tagId), + }, + }, + }; + } + + return state; +}; + function allReducer(state: AllConversationsState = initialState, action: Action): AllConversationsState { switch (action.type) { case getType(actions.mergeConversationsAction): @@ -138,6 +177,12 @@ function allReducer(state: AllConversationsState = initialState, action: Action) }, }; + case getType(actions.addTagToConversationAction): + return addTagToConversation(state, action.payload.conversationId, action.payload.tagId); + + case getType(actions.removeTagFromConversationAction): + return removeTagFromConversation(state, action.payload.conversationId, action.payload.tagId); + default: return state; } diff --git a/frontend/demo/src/reducers/data/settings/index.ts b/frontend/demo/src/reducers/data/settings/index.ts index ceb6d1dc26..b87264068d 100644 --- a/frontend/demo/src/reducers/data/settings/index.ts +++ b/frontend/demo/src/reducers/data/settings/index.ts @@ -8,12 +8,20 @@ export type SettingsState = { data: DataState; }; +export interface ColorSettings { + default: string; + background: string; + font: string; + position: number; + border: string; +} + export type Settings = { - colors: {}; + colors: {[id: string]: ColorSettings}; }; const defaultState = { - colors: [], + colors: {}, }; export default function tagsReducer(state = defaultState, action: Action): Settings { diff --git a/frontend/demo/src/types/tag.ts b/frontend/demo/src/types/tag.ts index 6cb778d004..be668108bf 100644 --- a/frontend/demo/src/types/tag.ts +++ b/frontend/demo/src/types/tag.ts @@ -1,19 +1,3 @@ -import {Tag} from 'httpclient'; - -export interface ColorSettings { - default: string; - background: string; - font: string; - position: number; - border: string; -} - -export interface TagSettings { - colors: ColorSettings[]; - enabled: boolean; - channels: Tag[]; -} - export interface ErrorTag { status: string; data?: string; diff --git a/lib/typescript/httpclient/index.ts b/lib/typescript/httpclient/index.ts index 68480d4f47..6f1994d915 100644 --- a/lib/typescript/httpclient/index.ts +++ b/lib/typescript/httpclient/index.ts @@ -17,6 +17,8 @@ import {ConversationPayload} from './payload/ConversationPayload'; import {PaginatedPayload} from './payload/PaginatedPayload'; import {conversationsMapper} from './mappers/conversationsMapper'; import {ListMessagesRequestPayload} from './payload/ListMessagesRequestPayload'; +import {TagConversationRequestPayload} from './payload/TagConversationRequestPayload'; +import {UntagConversationRequestPayload} from './payload/UntagConversationRequestPayload'; import {MessagePayload} from './payload/MessagePayload'; import {messageMapperData} from './mappers/messageMapperData'; import {tagsMapper} from './mappers/tagsMapper'; @@ -32,7 +34,9 @@ export async function parseBody(response: Response): Promise { if (response.ok) { try { return await response.json(); - } catch {} + } catch { + // NOP + } } let body = await response.text(); @@ -140,7 +144,7 @@ export class HttpClient { } public async readConversations(conversationId: string) { - const response = await this.doFetchFromBackend('conversations.read', {conversation_id: conversationId}); + await this.doFetchFromBackend('conversations.read', {conversation_id: conversationId}); return Promise.resolve(true); } @@ -185,7 +189,7 @@ export class HttpClient { public async updateTag(tag: Tag) { try { - const response = await this.doFetchFromBackend('tags.update', {...tag}); + await this.doFetchFromBackend('tags.update', {...tag}); return Promise.resolve(true); } catch (error) { return error; @@ -194,7 +198,7 @@ export class HttpClient { public async deleteTag(id: string) { try { - const response = await this.doFetchFromBackend('tags.delete', {id}); + await this.doFetchFromBackend('tags.delete', {id}); return Promise.resolve(true); } catch (error) { return error; @@ -209,6 +213,30 @@ export class HttpClient { return error; } } + + public async tagConversation(requestPayload: TagConversationRequestPayload) { + try { + await this.doFetchFromBackend('conversations.tag', { + conversation_id: requestPayload.conversationId, + tag_id: requestPayload.tagId, + }); + return Promise.resolve(true); + } catch (error) { + return error; + } + } + + public async untagConversation(requestPayload: UntagConversationRequestPayload) { + try { + await this.doFetchFromBackend('conversations.untag', { + conversation_id: requestPayload.conversationId, + tag_id: requestPayload.tagId, + }); + return Promise.resolve(true); + } catch (error) { + return error; + } + } } export * from './model'; diff --git a/lib/typescript/httpclient/payload/TagConversationRequestPayload.ts b/lib/typescript/httpclient/payload/TagConversationRequestPayload.ts new file mode 100644 index 0000000000..264411731d --- /dev/null +++ b/lib/typescript/httpclient/payload/TagConversationRequestPayload.ts @@ -0,0 +1,4 @@ +export interface TagConversationRequestPayload { + conversationId: string; + tagId: string; +} diff --git a/lib/typescript/httpclient/payload/UntagConversationRequestPayload.ts b/lib/typescript/httpclient/payload/UntagConversationRequestPayload.ts new file mode 100644 index 0000000000..60b6ee60c6 --- /dev/null +++ b/lib/typescript/httpclient/payload/UntagConversationRequestPayload.ts @@ -0,0 +1,4 @@ +export interface UntagConversationRequestPayload { + conversationId: string; + tagId: string; +} diff --git a/lib/typescript/httpclient/payload/index.ts b/lib/typescript/httpclient/payload/index.ts index 70e7fc7ad6..dbe7695df8 100644 --- a/lib/typescript/httpclient/payload/index.ts +++ b/lib/typescript/httpclient/payload/index.ts @@ -1,10 +1,12 @@ export * from './ConnectChannelRequestApiPayload'; export * from './ConnectChannelRequestPayload'; +export * from './CreateTagRequestPayload'; export * from './DisconnectChannelRequestApiPayload'; export * from './DisconnectChannelRequestPayload'; export * from './ExploreChannelRequestPayload'; -export * from './LoginViaEmailRequestPayload'; -export * from './ListTagsResponsePayload'; -export * from './CreateTagRequestPayload'; export * from './ListConversationsRequestPayload'; +export * from './ListTagsResponsePayload'; +export * from './LoginViaEmailRequestPayload'; export * from './ResponseMetadataPayload'; +export * from './TagConversationRequestPayload'; +export * from './UntagConversationRequestPayload'; diff --git a/yarn.lock b/yarn.lock index 955ccffe65..5bc1b62f4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,9 +3,9 @@ "@airyhq/components@latest": - version "0.4.8" - resolved "https://registry.yarnpkg.com/@airyhq/components/-/components-0.4.8.tgz#5fca05ebbcd1195d70075b35c1f48cc32b3b0056" - integrity sha512-qxdNLgMukxHq5cFacdOTPRC/Km8zHrJf0y2UoE0KcCfPMc7dzh1ziM5EFoHtxsIQJkhPlzcYyNaZxA6IpTdBLQ== + version "0.4.11" + resolved "https://registry.yarnpkg.com/@airyhq/components/-/components-0.4.11.tgz#68e803bb502ae201f64199c025878fd360f51577" + integrity sha512-aZkv/ncpbMeCHaIstuPDPliVZxKjOBDyVeFFUfu+/Y1dA6TOTc3JRGxKQ0wQqMtYrycV+FIuktnszCcIgswzLA== dependencies: "@crello/react-lottie" "^0.0.9" emoji-mart "^3.0.0" @@ -7193,12 +7193,12 @@ react-hot-loader@^4.12.20: shallowequal "^1.1.0" source-map "^0.7.3" -react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: +react-is@^16.6.0, react-is@^16.7.0: version "16.12.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c" integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q== -react-is@^16.9.0: +react-is@^16.8.1, react-is@^16.9.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -7209,9 +7209,9 @@ react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4: integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== react-modal@^3.11.2: - version "3.11.2" - resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.11.2.tgz#bad911976d4add31aa30dba8a41d11e21c4ac8a4" - integrity sha512-o8gvvCOFaG1T7W6JUvsYjRjMVToLZgLIsi5kdhFIQCtHxDkA47LznX62j+l6YQkpXDbvQegsDyxe/+JJsFQN7w== + version "3.12.1" + resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.12.1.tgz#38c33f70d81c33d02ff1ed115530443a3dc2afd3" + integrity sha512-WGuXn7Fq31PbFJwtWmOk+jFtGC7E9tJVbFX0lts8ZoS5EPi9+WWylUJWLKKVm3H4GlQ7ZxY7R6tLlbSIBQ5oZA== dependencies: exenv "^1.2.0" prop-types "^15.5.10" From 2c26a9d1d0f5a7036785469cdcaa74b4c98fb2e3 Mon Sep 17 00:00:00 2001 From: Pascal Holy <54705263+pascal-airy@users.noreply.github.com> Date: Wed, 20 Jan 2021 12:05:21 +0100 Subject: [PATCH 26/56] [#678] Update VERSION file after release (#684) Fixes #678 --- VERSION | 2 +- scripts/release.sh | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index 8f0916f768..2f35653bc3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.5.0 +0.6.0.alpha diff --git a/scripts/release.sh b/scripts/release.sh index c729483914..2f87070569 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -8,7 +8,7 @@ start() { echo -e "Starting release ${release_number}\n" create_issue create_release_branch - increase_version + update_release_version commit_version } @@ -36,9 +36,10 @@ finish() { merge_main merge_develop echo -e "Release ${release_number} is finished\n" + create_alpha_version } -increase_version() { +update_release_version() { issue_number=$(curl -s\ -H "Accept: application/vnd.github.v3+json" \ "https://api.github.com/repos/airyhq/airy/issues?labels=release" | jq '.[0].number') @@ -53,6 +54,21 @@ commit_version() { echo -e "Updated VERSION file\n" } +create_alpha_version() { + regex="([0-9]+).([0-9]+).([0-9]+)" + if [[ $release_number =~ $regex ]]; then + major="${BASH_REMATCH[1]}" + minor="${BASH_REMATCH[2]}" + patch="${BASH_REMATCH[3]}" + fi + alpha_version=$(printf "%s.%s.%s.alpha\n" $major $((minor+1)) $patch) + command echo ${alpha_version}> VERSION + command git add VERSION + command git commit -m "Bump version to ${alpha_version}" + command git push origin develop + echo -e "Updated VERSION file to ${alpha_version}\n" +} + merge_main() { command git checkout main command git pull origin main @@ -84,4 +100,3 @@ case $1 in "finish") finish $2 esac - From 5ef51d52d9c7745e5cac27df78b9e6c87def7899 Mon Sep 17 00:00:00 2001 From: lucapette Date: Wed, 20 Jan 2021 13:37:06 +0100 Subject: [PATCH 27/56] [#679] Introduce status command (#686) Fixes #679 --- .../api/config/ClientConfigController.java | 29 ++--------- .../ClientConfigResponsePayload.java | 7 ++- .../api/config/ClientControllerConfig.java | 15 ++++++ .../core/api/config/ServiceDiscovery.java | 34 +++++++++++-- .../ClientConfigControllerTest.java | 24 ++++++--- infrastructure/cli/cmd/BUILD | 3 +- infrastructure/cli/cmd/{auth => api}/BUILD | 10 ++-- infrastructure/cli/cmd/api/api.go | 18 +++++++ infrastructure/cli/cmd/api/login.go | 42 ++++++++++++++++ infrastructure/cli/cmd/api/signup.go | 43 ++++++++++++++++ infrastructure/cli/cmd/auth/auth.go | 49 ------------------- infrastructure/cli/cmd/root.go | 11 ++++- infrastructure/cli/cmd/status/BUILD | 13 +++++ infrastructure/cli/cmd/status/status.go | 44 +++++++++++++++++ infrastructure/cli/go.mod | 6 --- infrastructure/cli/go.sum | 23 ++------- infrastructure/cli/main_test.go | 16 ++---- infrastructure/cli/pkg/tests/golden.go | 2 +- infrastructure/cli/pkg/tests/golden/BUILD | 11 ++--- .../cli/pkg/tests/golden/airycli.yaml | 1 - .../cli/pkg/tests/golden/cli.auth.golden | 1 - .../cli/pkg/tests/golden/cli.login.golden | 1 + .../cli/pkg/tests/golden/cli.no-args.golden | 3 +- infrastructure/cli/pkg/tests/golden/cli.yaml | 2 + lib/go/httpclient/BUILD | 1 + lib/go/httpclient/client.go | 23 +++++++++ .../httpclient/http-client-test/users_test.go | 7 +-- lib/go/httpclient/httpclient.go | 49 +++++++------------ lib/go/httpclient/payloads/BUILD | 10 ++-- .../payloads/client_config_request_payload.go | 4 ++ .../client_config_response_payload.go | 6 +++ ...in_request.go => login_request_payload.go} | 0 ..._response.go => login_response_payload.go} | 0 ...p_request.go => signup_request_payload.go} | 0 ...response.go => signup_response_payload.go} | 0 lib/go/httpclient/users.go | 19 ++++--- 36 files changed, 333 insertions(+), 194 deletions(-) rename backend/api/admin/src/main/java/co/airy/core/api/{admin/payload => config}/ClientConfigResponsePayload.java (57%) create mode 100644 backend/api/admin/src/main/java/co/airy/core/api/config/ClientControllerConfig.java rename backend/api/admin/src/test/java/co/airy/core/api/{admin => config}/ClientConfigControllerTest.java (76%) rename infrastructure/cli/cmd/{auth => api}/BUILD (68%) create mode 100644 infrastructure/cli/cmd/api/api.go create mode 100644 infrastructure/cli/cmd/api/login.go create mode 100644 infrastructure/cli/cmd/api/signup.go delete mode 100644 infrastructure/cli/cmd/auth/auth.go create mode 100644 infrastructure/cli/cmd/status/BUILD create mode 100644 infrastructure/cli/cmd/status/status.go delete mode 100644 infrastructure/cli/pkg/tests/golden/airycli.yaml delete mode 100644 infrastructure/cli/pkg/tests/golden/cli.auth.golden create mode 100644 infrastructure/cli/pkg/tests/golden/cli.login.golden create mode 100644 infrastructure/cli/pkg/tests/golden/cli.yaml create mode 100644 lib/go/httpclient/client.go create mode 100644 lib/go/httpclient/payloads/client_config_request_payload.go create mode 100644 lib/go/httpclient/payloads/client_config_response_payload.go rename lib/go/httpclient/payloads/{login_request.go => login_request_payload.go} (100%) rename lib/go/httpclient/payloads/{login_response.go => login_response_payload.go} (100%) rename lib/go/httpclient/payloads/{signup_request.go => signup_request_payload.go} (100%) rename lib/go/httpclient/payloads/{signup_response.go => signup_response_payload.go} (100%) diff --git a/backend/api/admin/src/main/java/co/airy/core/api/config/ClientConfigController.java b/backend/api/admin/src/main/java/co/airy/core/api/config/ClientConfigController.java index ee670500fd..6977821c0c 100644 --- a/backend/api/admin/src/main/java/co/airy/core/api/config/ClientConfigController.java +++ b/backend/api/admin/src/main/java/co/airy/core/api/config/ClientConfigController.java @@ -1,46 +1,25 @@ package co.airy.core.api.config; -import co.airy.core.api.admin.payload.ClientConfigResponsePayload; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.client.RestTemplate; -import java.util.ArrayList; -import java.util.List; +import java.util.HashMap; import java.util.Map; @RestController public class ClientConfigController { - private final ServiceDiscovery serviceDiscovery; - private final String namespace; - private final RestTemplate restTemplate; - public ClientConfigController(ServiceDiscovery serviceDiscovery, @Value("${kubernetes.namespace}") String namespace, RestTemplate restTemplate) { + public ClientConfigController(ServiceDiscovery serviceDiscovery) { this.serviceDiscovery = serviceDiscovery; - this.namespace = namespace; - this.restTemplate = restTemplate; } @PostMapping("/client.config") public ResponseEntity getConfig() { - List>> components = new ArrayList<>(); - - for (String service : serviceDiscovery.getServices()) { - try { - ResponseEntity response = restTemplate.exchange(String.format("http://%s.%s/actuator/health", service, namespace), HttpMethod.GET, null, Object.class); - components.add(Map.of(service.replace("-connector", ""), Map.of("enabled", Boolean.toString(response.getStatusCode().is2xxSuccessful())))); - } catch (Exception e) { - components.add(Map.of(service.replace("-connector", ""), Map.of("enabled", Boolean.toString(false)))); - } - } - return ResponseEntity.ok(ClientConfigResponsePayload.builder() - .components(components) - .features(List.of()) + .components(serviceDiscovery.getComponents()) + .features(Map.of()) .build()); } } diff --git a/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/ClientConfigResponsePayload.java b/backend/api/admin/src/main/java/co/airy/core/api/config/ClientConfigResponsePayload.java similarity index 57% rename from backend/api/admin/src/main/java/co/airy/core/api/admin/payload/ClientConfigResponsePayload.java rename to backend/api/admin/src/main/java/co/airy/core/api/config/ClientConfigResponsePayload.java index 5002cb7b3a..bd11534e48 100644 --- a/backend/api/admin/src/main/java/co/airy/core/api/admin/payload/ClientConfigResponsePayload.java +++ b/backend/api/admin/src/main/java/co/airy/core/api/config/ClientConfigResponsePayload.java @@ -1,11 +1,10 @@ -package co.airy.core.api.admin.payload; +package co.airy.core.api.config; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import java.util.List; import java.util.Map; @Builder @@ -13,6 +12,6 @@ @NoArgsConstructor @AllArgsConstructor public class ClientConfigResponsePayload { - private List>> components; - private List> features; + private Map> components; + private Map features; } diff --git a/backend/api/admin/src/main/java/co/airy/core/api/config/ClientControllerConfig.java b/backend/api/admin/src/main/java/co/airy/core/api/config/ClientControllerConfig.java new file mode 100644 index 0000000000..c3f56961f1 --- /dev/null +++ b/backend/api/admin/src/main/java/co/airy/core/api/config/ClientControllerConfig.java @@ -0,0 +1,15 @@ +package co.airy.core.api.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.web.client.RestTemplate; + +@EnableScheduling +@Configuration +public class ClientControllerConfig { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/backend/api/admin/src/main/java/co/airy/core/api/config/ServiceDiscovery.java b/backend/api/admin/src/main/java/co/airy/core/api/config/ServiceDiscovery.java index 219fb754f4..404921a692 100644 --- a/backend/api/admin/src/main/java/co/airy/core/api/config/ServiceDiscovery.java +++ b/backend/api/admin/src/main/java/co/airy/core/api/config/ServiceDiscovery.java @@ -1,13 +1,25 @@ package co.airy.core.api.config; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; @Component public class ServiceDiscovery { + private final String namespace; + private final RestTemplate restTemplate; + + private final Map> components = new ConcurrentHashMap<>(); + private static final List services = List.of( "sources-chatplugin", "sources-facebook-connector", @@ -15,12 +27,24 @@ public class ServiceDiscovery { "sources-google-connector" ); - @Bean - public RestTemplate restTemplate() { - return new RestTemplate(); + public ServiceDiscovery(@Value("${kubernetes.namespace}") String namespace, RestTemplate restTemplate) { + this.namespace = namespace; + this.restTemplate = restTemplate; + } + + public Map> getComponents() { + return components; } - public List getServices() { - return services; + @Scheduled(fixedRate = 1_000) + private void updateComponentsStatus() { + for (String service : services) { + try { + ResponseEntity response = restTemplate.exchange(String.format("http://%s.%s/actuator/health", service, namespace), HttpMethod.GET, null, Object.class); + components.put(service.replace("-connector", ""), Map.of("enabled", response.getStatusCode().is2xxSuccessful())); + } catch (Exception e) { + components.put(service.replace("-connector", ""), Map.of("enabled",false)); + } + } } } diff --git a/backend/api/admin/src/test/java/co/airy/core/api/admin/ClientConfigControllerTest.java b/backend/api/admin/src/test/java/co/airy/core/api/config/ClientConfigControllerTest.java similarity index 76% rename from backend/api/admin/src/test/java/co/airy/core/api/admin/ClientConfigControllerTest.java rename to backend/api/admin/src/test/java/co/airy/core/api/config/ClientConfigControllerTest.java index 2d57c7c6e1..f5cd36a103 100644 --- a/backend/api/admin/src/test/java/co/airy/core/api/admin/ClientConfigControllerTest.java +++ b/backend/api/admin/src/test/java/co/airy/core/api/config/ClientConfigControllerTest.java @@ -1,6 +1,5 @@ -package co.airy.core.api.admin; +package co.airy.core.api.config; -import co.airy.core.api.config.ClientConfigController; import co.airy.kafka.schema.application.ApplicationCommunicationChannels; import co.airy.kafka.schema.application.ApplicationCommunicationTags; import co.airy.kafka.schema.application.ApplicationCommunicationWebhooks; @@ -22,15 +21,20 @@ import org.springframework.http.HttpStatus; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.web.client.ExpectedCount; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestTemplate; import java.net.URI; +import static co.airy.test.Timing.retryOnException; +import static org.hamcrest.CoreMatchers.everyItem; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.collection.IsCollectionWithSize.hasSize; +import static org.springframework.test.web.client.ExpectedCount.min; import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = AirySpringBootApplication.class) @@ -81,23 +85,27 @@ void beforeEach() throws Exception { @Test public void canReturnConfig() throws Exception { - mockServer.expect(requestTo(new URI("http://sources-chatplugin.default/actuator/health"))) + mockServer.expect(min(1), requestTo(new URI("http://sources-chatplugin.default/actuator/health"))) .andExpect(method(HttpMethod.GET)) .andRespond(withStatus(HttpStatus.OK)); - mockServer.expect(requestTo(new URI("http://sources-facebook-connector.default/actuator/health"))) + mockServer.expect(min(1), requestTo(new URI("http://sources-facebook-connector.default/actuator/health"))) .andExpect(method(HttpMethod.GET)) .andRespond(withStatus(HttpStatus.OK)); - mockServer.expect(requestTo(new URI("http://sources-twilio-connector.default/actuator/health"))) + mockServer.expect(min(1), requestTo(new URI("http://sources-twilio-connector.default/actuator/health"))) .andExpect(method(HttpMethod.GET)) .andRespond(withStatus(HttpStatus.OK)); - mockServer.expect(requestTo(new URI("http://sources-google-connector.default/actuator/health"))) + mockServer.expect(min(1), requestTo(new URI("http://sources-google-connector.default/actuator/health"))) .andExpect(method(HttpMethod.GET)) .andRespond(withStatus(HttpStatus.OK)); - webTestHelper.post("/client.config", "{}", "user-id").andExpect(status().isOk()); + retryOnException(() -> webTestHelper.post("/client.config", "{}", "user-id") + .andExpect(status().isOk()) + .andExpect(jsonPath("$.components.*", hasSize(4))) + .andExpect(jsonPath("$.components.*.enabled", everyItem(is(true)))), + "client.config call failed"); } } diff --git a/infrastructure/cli/cmd/BUILD b/infrastructure/cli/cmd/BUILD index f506ed40eb..4b275c53a6 100644 --- a/infrastructure/cli/cmd/BUILD +++ b/infrastructure/cli/cmd/BUILD @@ -10,8 +10,9 @@ go_library( "CommitSHA1": "{STABLE_GIT_COMMIT}", }, deps = [ - "//infrastructure/cli/cmd/auth", + "//infrastructure/cli/cmd/api", "//infrastructure/cli/cmd/config", + "//infrastructure/cli/cmd/status", "//infrastructure/cli/cmd/ui", "@com_github_mitchellh_go_homedir//:go-homedir", "@com_github_spf13_cobra//:cobra", diff --git a/infrastructure/cli/cmd/auth/BUILD b/infrastructure/cli/cmd/api/BUILD similarity index 68% rename from infrastructure/cli/cmd/auth/BUILD rename to infrastructure/cli/cmd/api/BUILD index 32624f8ea0..0802f7ccae 100644 --- a/infrastructure/cli/cmd/auth/BUILD +++ b/infrastructure/cli/cmd/api/BUILD @@ -1,9 +1,13 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( - name = "auth", - srcs = ["auth.go"], - importpath = "cli/cmd/auth", + name = "api", + srcs = [ + "api.go", + "login.go", + "signup.go", + ], + importpath = "cli/cmd/api", visibility = ["//visibility:public"], deps = [ "//lib/go/httpclient", diff --git a/infrastructure/cli/cmd/api/api.go b/infrastructure/cli/cmd/api/api.go new file mode 100644 index 0000000000..bc364ed762 --- /dev/null +++ b/infrastructure/cli/cmd/api/api.go @@ -0,0 +1,18 @@ +package api + +import ( + "github.com/spf13/cobra" +) + +// APICmd subcommand for Airy Core +var APICmd = &cobra.Command{ + Use: "api", + TraverseChildren: true, + Short: "Interacts with the Airy Core Platform HTTP API", + Long: ``, +} + +func init() { + APICmd.AddCommand(signupCmd) + APICmd.AddCommand(loginCmd) +} diff --git a/infrastructure/cli/cmd/api/login.go b/infrastructure/cli/cmd/api/login.go new file mode 100644 index 0000000000..4fd812a491 --- /dev/null +++ b/infrastructure/cli/cmd/api/login.go @@ -0,0 +1,42 @@ +package api + +import ( + "fmt" + "os" + + "github.com/airyhq/airy/lib/go/httpclient" + "github.com/airyhq/airy/lib/go/httpclient/payloads" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var loginCmd = &cobra.Command{ + Use: "login", + Short: "Logs you in in the Airy Core Platform", + Long: ``, + Run: login, +} + +func login(cmd *cobra.Command, args []string) { + email, _ := cmd.Flags().GetString("email") + password, _ := cmd.Flags().GetString("password") + c := httpclient.NewClient(viper.GetString("apihost")) + + loginRequestPayload := payloads.LoginRequestPayload{Email: email, Password: password} + res, err := c.Login(loginRequestPayload) + if err != nil { + fmt.Println("could not login:", err) + os.Exit(1) + } + fmt.Printf("logged in correctly: %s\n", res.Token) + + viper.Set("apiJWTToken", res.Token) + viper.WriteConfig() +} + +func init() { + var email, password string + loginCmd.Flags().StringVarP(&email, "email", "e", "grace@hopper.com", "Email") + loginCmd.Flags().StringVarP(&password, "password", "p", "the_answer_is_42", "Password") +} diff --git a/infrastructure/cli/cmd/api/signup.go b/infrastructure/cli/cmd/api/signup.go new file mode 100644 index 0000000000..6470d19878 --- /dev/null +++ b/infrastructure/cli/cmd/api/signup.go @@ -0,0 +1,43 @@ +package api + +import ( + "fmt" + "os" + + "github.com/airyhq/airy/lib/go/httpclient" + "github.com/airyhq/airy/lib/go/httpclient/payloads" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var signupCmd = &cobra.Command{ + Use: "signup", + Short: "Signs users up in the Airy Core Platform", + Long: ``, + Run: signup, +} + +func signup(cmd *cobra.Command, args []string) { + firstName, _ := cmd.Flags().GetString("firstName") + lastName, _ := cmd.Flags().GetString("lastName") + email, _ := cmd.Flags().GetString("email") + password, _ := cmd.Flags().GetString("password") + c := httpclient.NewClient(viper.GetString("apihost")) + + signupRequestPayload := payloads.SignupRequestPayload{FirstName: firstName, LastName: lastName, Email: email, Password: password} + res, err := c.Signup(signupRequestPayload) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + fmt.Printf("user created: %s\n", res.ID) +} + +func init() { + var firstName, lastName, email, password string + signupCmd.Flags().StringVarP(&firstName, "firstName", "f", "Grace", "First name") + signupCmd.Flags().StringVarP(&lastName, "lastName", "l", "Hopper", "Last name") + signupCmd.Flags().StringVarP(&email, "email", "e", "grace@hopper.com", "Email") + signupCmd.Flags().StringVarP(&password, "password", "p", "the_answer_is_42", "Password") +} diff --git a/infrastructure/cli/cmd/auth/auth.go b/infrastructure/cli/cmd/auth/auth.go deleted file mode 100644 index 34f47db2a7..0000000000 --- a/infrastructure/cli/cmd/auth/auth.go +++ /dev/null @@ -1,49 +0,0 @@ -package auth - -import ( - "fmt" - "log" - - "github.com/airyhq/airy/lib/go/httpclient" - "github.com/airyhq/airy/lib/go/httpclient/payloads" - - "github.com/spf13/cobra" - "github.com/spf13/viper" -) - -// AuthCmd subcommand for Airy Core -var AuthCmd = &cobra.Command{ - Use: "auth", - TraverseChildren: true, - Short: "Create a default user and return a JWT token", - Long: ``, - Run: auth, -} - -func auth(cmd *cobra.Command, args []string) { - url := viper.GetString("apihost") - email, _ := cmd.Flags().GetString("email") - password, _ := cmd.Flags().GetString("password") - c := httpclient.NewClient() - c.BaseURL = url - - loginRequestPayload := payloads.LoginRequestPayload{Email: email, Password: password} - - res, err := c.Login(loginRequestPayload) - if err != nil { - signupRequestPayload := payloads.SignupRequestPayload{FirstName: "Firstname", LastName: "Lastname", Email: email, Password: password} - res, err := c.Signup(signupRequestPayload) - if err != nil { - log.Fatal(err) - } - fmt.Println(res.Token) - return - } - fmt.Println(res.Token) -} - -func init() { - var email, password string - AuthCmd.Flags().StringVarP(&email, "email", "e", "grace@hopper.com", "Email to use for the authentication") - AuthCmd.Flags().StringVarP(&password, "password", "p", "the_answer_is_42", "Password to use for the authentication") -} diff --git a/infrastructure/cli/cmd/root.go b/infrastructure/cli/cmd/root.go index 99c790007c..aa28653931 100644 --- a/infrastructure/cli/cmd/root.go +++ b/infrastructure/cli/cmd/root.go @@ -5,8 +5,9 @@ import ( "os" "path" - "cli/cmd/auth" + "cli/cmd/api" "cli/cmd/config" + "cli/cmd/status" "cli/cmd/ui" homedir "github.com/mitchellh/go-homedir" @@ -116,9 +117,15 @@ func init() { viper.BindPFlag("apihost", rootCmd.PersistentFlags().Lookup("apihost")) viper.SetDefault("apihost", "http://api.airy") + apiJWTToken := "" + rootCmd.PersistentFlags().StringVarP(&apiJWTToken, "apiJWTToken", "", "", "apiJWTToken") + rootCmd.PersistentFlags().MarkHidden("apiJWTToken") + viper.BindPFlag("apiJWTToken", rootCmd.PersistentFlags().Lookup("apiJWTToken")) + rootCmd.PersistentFlags().StringVar(&configFile, "config", "", "config file (default is $HOME/.airy/cli.yaml)") - rootCmd.AddCommand(auth.AuthCmd) + rootCmd.AddCommand(api.APICmd) rootCmd.AddCommand(config.ConfigCmd) + rootCmd.AddCommand(status.StatusCmd) rootCmd.AddCommand(ui.UICmd) rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(initCmd) diff --git a/infrastructure/cli/cmd/status/BUILD b/infrastructure/cli/cmd/status/BUILD new file mode 100644 index 0000000000..6ff870c55d --- /dev/null +++ b/infrastructure/cli/cmd/status/BUILD @@ -0,0 +1,13 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "status", + srcs = ["status.go"], + importpath = "cli/cmd/status", + visibility = ["//visibility:public"], + deps = [ + "//lib/go/httpclient", + "@com_github_spf13_cobra//:cobra", + "@com_github_spf13_viper//:viper", + ], +) diff --git a/infrastructure/cli/cmd/status/status.go b/infrastructure/cli/cmd/status/status.go new file mode 100644 index 0000000000..f39437ac89 --- /dev/null +++ b/infrastructure/cli/cmd/status/status.go @@ -0,0 +1,44 @@ +package status + +import ( + "fmt" + "os" + "text/tabwriter" + + "github.com/airyhq/airy/lib/go/httpclient" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// StatusCmd reports the status of your Airy Core Platform +var StatusCmd = &cobra.Command{ + Use: "status", + Short: "Reports the status of your Airy Core Platform", + Long: ``, + Run: status, +} + +func status(cmd *cobra.Command, args []string) { + c := httpclient.NewClient(viper.GetString("apihost")) + + c.JWTToken = viper.GetString("apiJWTToken") + + res, err := c.Config() + + if err != nil { + fmt.Println("could not read status: ", err) + os.Exit(1) + } + + w := tabwriter.NewWriter(os.Stdout, 1, 1, 1, ' ', 0) + + for k, v := range res.Components { + if v["enabled"].(bool) { + fmt.Fprintf(w, "%s\t\u2713\n", k) + } else { + fmt.Fprintf(w, "%s\t\u2717\n", k) + } + } + + w.Flush() +} diff --git a/infrastructure/cli/go.mod b/infrastructure/cli/go.mod index 7d7459829f..0880d133be 100644 --- a/infrastructure/cli/go.mod +++ b/infrastructure/cli/go.mod @@ -3,17 +3,11 @@ module cli go 1.12 require ( - github.com/BurntSushi/toml v0.3.1 // indirect github.com/airyhq/airy/lib/go/httpclient v0.0.0 - github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 // indirect - github.com/coreos/go-etcd v2.0.0+incompatible // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/kr/pretty v0.2.1 github.com/mitchellh/go-homedir v1.1.0 github.com/spf13/cobra v1.1.1 github.com/spf13/viper v1.7.1 - github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 // indirect - github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 // indirect goji.io v2.0.2+incompatible ) diff --git a/infrastructure/cli/go.sum b/infrastructure/cli/go.sum index 3ad81e4526..230424ee3b 100644 --- a/infrastructure/cli/go.sum +++ b/infrastructure/cli/go.sum @@ -18,7 +18,6 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -28,10 +27,7 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= @@ -70,6 +66,7 @@ github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OI github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -101,6 +98,7 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -113,8 +111,6 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -159,7 +155,9 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -167,8 +165,6 @@ github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= -github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= @@ -177,8 +173,6 @@ github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.3.1 h1:5+8j8FTpnFV4nEImW/ofkzEt8VoOiLXxdYIDsB73T38= -github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= @@ -192,9 +186,7 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -205,8 +197,6 @@ goji.io v2.0.2+incompatible h1:uIssv/elbKRLznFUy3Xj4+2Mz/qKhek/9aZQDUMae7c= goji.io v2.0.2+incompatible/go.mod h1:sbqFwrtqZACxLBTQcdgVjFh54yGVCvwq8+w49MVMMIk= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -256,8 +246,6 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -315,6 +303,7 @@ google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ij gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= @@ -322,8 +311,6 @@ gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/infrastructure/cli/main_test.go b/infrastructure/cli/main_test.go index 02690ef28c..59f6564e33 100644 --- a/infrastructure/cli/main_test.go +++ b/infrastructure/cli/main_test.go @@ -1,7 +1,6 @@ package main import ( - "fmt" "os/exec" "testing" @@ -18,11 +17,9 @@ func TestCli(t *testing.T) { golden string wantErr bool }{ - {"no args", []string{}, "cli.no-args.golden", false}, - {"auth", []string{"auth", "--config", "pkg/tests/golden/airycli.yaml"}, "cli.auth.golden", false}, - {"auth", []string{"auth", "--config", "pkg/tests/golden/airycli.yaml", "--email", "grace@example.com"}, "cli.auth.golden", false}, - {"auth", []string{"auth", "--config", "pkg/tests/golden/airycli.yaml", "--email", "grace@example.com", "--password", "examplepassword"}, "cli.auth.golden", false}, - {"version", []string{"version", "--config", "pkg/tests/golden/airycli.yaml"}, "cli.version.golden", false}, + {"no args", []string{}, "cli.no-args", false}, + {"login", []string{"api", "login", "--config", "pkg/tests/golden/cli.yaml"}, "cli.login", false}, + {"version", []string{"version", "--config", "pkg/tests/golden/cli.yaml"}, "cli.version", false}, } go func() { @@ -33,13 +30,10 @@ func TestCli(t *testing.T) { t.Run(tt.name, func(testing *testing.T) { cmd := exec.Command(binaryName, tt.args...) output, err := cmd.CombinedOutput() - + actual := string(output) if (err != nil) != tt.wantErr { - t.Fatalf("Test expected to fail: %t. Did the test pass: %t. Error message: %v\n", tt.wantErr, err == nil, err) + t.Fatalf("Test %s expected to fail: %t. Did the test pass: %t. Error message: %v Output: %s\n", tt.name, tt.wantErr, err == nil, err, actual) } - fmt.Println(output) - - actual := string(output) golden := airytests.NewGoldenFile(t, tt.golden) expected := golden.Load() diff --git a/infrastructure/cli/pkg/tests/golden.go b/infrastructure/cli/pkg/tests/golden.go index fee90e27c2..5a1fe2be72 100644 --- a/infrastructure/cli/pkg/tests/golden.go +++ b/infrastructure/cli/pkg/tests/golden.go @@ -22,7 +22,7 @@ func Diff(expected, actual interface{}) []string { // NewGoldenFile function func NewGoldenFile(t *testing.T, name string) *TestFile { - return &TestFile{t: t, name: name, dir: "./pkg/tests/golden/"} + return &TestFile{t: t, name: name + ".golden", dir: "./pkg/tests/golden/"} } func (tf *TestFile) path() string { diff --git a/infrastructure/cli/pkg/tests/golden/BUILD b/infrastructure/cli/pkg/tests/golden/BUILD index 895c692957..b0e79c68ab 100644 --- a/infrastructure/cli/pkg/tests/golden/BUILD +++ b/infrastructure/cli/pkg/tests/golden/BUILD @@ -2,11 +2,8 @@ package(default_visibility = ["//visibility:public"]) filegroup( name = "golden_files", - srcs = [ - "airycli.yaml", - "api.signup.golden", - "cli.auth.golden", - "cli.no-args.golden", - "cli.version.golden", - ], + srcs = glob([ + "*.golden", + "*.yaml", + ]), ) diff --git a/infrastructure/cli/pkg/tests/golden/airycli.yaml b/infrastructure/cli/pkg/tests/golden/airycli.yaml deleted file mode 100644 index 8e86406679..0000000000 --- a/infrastructure/cli/pkg/tests/golden/airycli.yaml +++ /dev/null @@ -1 +0,0 @@ -apihost: http://localhost:3001 diff --git a/infrastructure/cli/pkg/tests/golden/cli.auth.golden b/infrastructure/cli/pkg/tests/golden/cli.auth.golden deleted file mode 100644 index 7511a74d6d..0000000000 --- a/infrastructure/cli/pkg/tests/golden/cli.auth.golden +++ /dev/null @@ -1 +0,0 @@ -eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDM1MmU3NC00MjEyLTQyODItYWM3NC0wZjU4ZjgzYzJkYjUiLCJzdWIiOiIxMDM1MmU3NC00MjEyLTQyODItYWM3NC0wZjU4ZjgzYzJkYjUiLCJpYXQiOjE2MDc2MTk0ODksInVzZXJfaWQiOiIxMDM1MmU3NC00MjEyLTQyODItYWM3NC0wZjU4ZjgzYzJkYjUiLCJleHAiOjE2MDc3MDU4ODl9.ZuIv_t0D358n04gamNwz3U_tkxr4IO36gXuZyU9X3e4 diff --git a/infrastructure/cli/pkg/tests/golden/cli.login.golden b/infrastructure/cli/pkg/tests/golden/cli.login.golden new file mode 100644 index 0000000000..44e53ecb9a --- /dev/null +++ b/infrastructure/cli/pkg/tests/golden/cli.login.golden @@ -0,0 +1 @@ +logged in correctly: eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDM1MmU3NC00MjEyLTQyODItYWM3NC0wZjU4ZjgzYzJkYjUiLCJzdWIiOiIxMDM1MmU3NC00MjEyLTQyODItYWM3NC0wZjU4ZjgzYzJkYjUiLCJpYXQiOjE2MDc2MTk0ODksInVzZXJfaWQiOiIxMDM1MmU3NC00MjEyLTQyODItYWM3NC0wZjU4ZjgzYzJkYjUiLCJleHAiOjE2MDc3MDU4ODl9.ZuIv_t0D358n04gamNwz3U_tkxr4IO36gXuZyU9X3e4 diff --git a/infrastructure/cli/pkg/tests/golden/cli.no-args.golden b/infrastructure/cli/pkg/tests/golden/cli.no-args.golden index a419bd207c..73148849a8 100644 --- a/infrastructure/cli/pkg/tests/golden/cli.no-args.golden +++ b/infrastructure/cli/pkg/tests/golden/cli.no-args.golden @@ -4,10 +4,11 @@ Usage: airy [command] Available Commands: - auth Create a default user and return a JWT token + api Interacts with the Airy Core Platform HTTP API config Reloads configuration based on airy.yaml help Help about any command init Inits your Airy CLI configuration + status Reports the status of your Airy Core Platform ui Opens the Airy Core Platform UI in your local browser version Prints version information diff --git a/infrastructure/cli/pkg/tests/golden/cli.yaml b/infrastructure/cli/pkg/tests/golden/cli.yaml new file mode 100644 index 0000000000..cb422e6be7 --- /dev/null +++ b/infrastructure/cli/pkg/tests/golden/cli.yaml @@ -0,0 +1,2 @@ +apihost: http://localhost:3001 +apijwttoken: eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDM1MmU3NC00MjEyLTQyODItYWM3NC0wZjU4ZjgzYzJkYjUiLCJzdWIiOiIxMDM1MmU3NC00MjEyLTQyODItYWM3NC0wZjU4ZjgzYzJkYjUiLCJpYXQiOjE2MDc2MTk0ODksInVzZXJfaWQiOiIxMDM1MmU3NC00MjEyLTQyODItYWM3NC0wZjU4ZjgzYzJkYjUiLCJleHAiOjE2MDc3MDU4ODl9.ZuIv_t0D358n04gamNwz3U_tkxr4IO36gXuZyU9X3e4 diff --git a/lib/go/httpclient/BUILD b/lib/go/httpclient/BUILD index dae39ebd2f..6ac19451f5 100644 --- a/lib/go/httpclient/BUILD +++ b/lib/go/httpclient/BUILD @@ -5,6 +5,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "httpclient", srcs = [ + "client.go", "httpclient.go", "users.go", ], diff --git a/lib/go/httpclient/client.go b/lib/go/httpclient/client.go new file mode 100644 index 0000000000..681cd4dc11 --- /dev/null +++ b/lib/go/httpclient/client.go @@ -0,0 +1,23 @@ +package httpclient + +import ( + "encoding/json" + + "github.com/airyhq/airy/lib/go/httpclient/payloads" +) + +func (c *Client) Config() (*payloads.ClientConfigResponsePayload, error) { + payload, err := json.Marshal(payloads.ClientConfigRequestPayload{}) + if err != nil { + return nil, err + } + + res := payloads.ClientConfigResponsePayload{} + + e := c.post("client.config", payload, &res) + if e != nil { + return nil, e + } + + return &res, nil +} diff --git a/lib/go/httpclient/http-client-test/users_test.go b/lib/go/httpclient/http-client-test/users_test.go index e2cb5ea19f..fc3bea1d35 100644 --- a/lib/go/httpclient/http-client-test/users_test.go +++ b/lib/go/httpclient/http-client-test/users_test.go @@ -13,12 +13,11 @@ import ( ) func TestSignup(t *testing.T) { - c := httpclient.NewClient() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "{\"id\":\"a6c413a7-8d42-4c2b-8736-d033134eec59\",\"first_name\":\"Grace\",\"last_name\":\"Hopper\",\"token\":\"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJhNmM0MTNhNy04ZDQyLTRjMmItODczNi1kMDMzMTM0ZWVjNTkiLCJzdWIiOiJhNmM0MTNhNy04ZDQyLTRjMmItODczNi1kMDMzMTM0ZWVjNTkiLCJpYXQiOjE2MDczMzY2NjMsInVzZXJfaWQiOiJhNmM0MTNhNy04ZDQyLTRjMmItODczNi1kMDMzMTM0ZWVjNTkiLCJleHAiOjE2MDc0MjMwNjN9.I4sf2j36RQCPRrirzSYyRhJ4U3bG2sUmHfxX4yBJvQA\"}") })) - c.BaseURL = ts.URL + c := httpclient.NewClient(ts.URL) signupRequestPayload := payloads.SignupRequestPayload{FirstName: "Grace", LastName: "Hopper", Password: "the_answer_is_42", Email: "grace@example.com"} @@ -32,12 +31,11 @@ func TestSignup(t *testing.T) { } func TestLogin(t *testing.T) { - c := httpclient.NewClient() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "{\"id\":\"a6c413a7-8d42-4c2b-8736-d033134eec59\",\"first_name\":\"Grace\",\"last_name\":\"Hopper\",\"token\":\"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJhNmM0MTNhNy04ZDQyLTRjMmItODczNi1kMDMzMTM0ZWVjNTkiLCJzdWIiOiJhNmM0MTNhNy04ZDQyLTRjMmItODczNi1kMDMzMTM0ZWVjNTkiLCJpYXQiOjE2MDczMzY2NjMsInVzZXJfaWQiOiJhNmM0MTNhNy04ZDQyLTRjMmItODczNi1kMDMzMTM0ZWVjNTkiLCJleHAiOjE2MDc0MjMwNjN9.I4sf2j36RQCPRrirzSYyRhJ4U3bG2sUmHfxX4yBJvQA\"}") })) - c.BaseURL = ts.URL + c := httpclient.NewClient(ts.URL) loginRequestPayload := payloads.LoginRequestPayload{Password: "the_answer_is_42", Email: "grace@example.com"} @@ -47,5 +45,4 @@ func TestLogin(t *testing.T) { assert.NotNil(t, res, "expecting non-nil result") assert.NotEmpty(t, res.Token, "expecting non-empty token") - } diff --git a/lib/go/httpclient/httpclient.go b/lib/go/httpclient/httpclient.go index f557d556a9..8d214ca43c 100644 --- a/lib/go/httpclient/httpclient.go +++ b/lib/go/httpclient/httpclient.go @@ -3,63 +3,48 @@ package httpclient import ( "bytes" "encoding/json" - "errors" "fmt" "net/http" "time" ) -const ( - BaseURL = "http://api.airy" -) - type Client struct { - BaseURL string - HTTPClient *http.Client + BaseURL string + JWTToken string + c *http.Client } -func NewClient() *Client { +func NewClient(baseURL string) *Client { return &Client{ - BaseURL: BaseURL, - HTTPClient: &http.Client{ + BaseURL: baseURL, + c: &http.Client{ Timeout: time.Minute, }, } } -type errorResponse struct { - Code int `json:"code"` - Message string `json:"message"` -} - -func (c *Client) sendRequest(requestDataJSON []byte, endpoint string, v interface{}) error { - - req, err := http.NewRequest("POST", fmt.Sprintf("%s/%s", c.BaseURL, endpoint), bytes.NewBuffer(requestDataJSON)) +func (c *Client) post(endpoint string, payload []byte, res interface{}) error { + req, err := http.NewRequest("POST", fmt.Sprintf("%s/%s", c.BaseURL, endpoint), bytes.NewBuffer(payload)) if err != nil { return err } + req.Header.Set("Content-Type", "application/json; charset=utf-8") req.Header.Set("Accept", "application/json; charset=utf-8") + if c.JWTToken != "" { + req.Header.Set("Authorization", c.JWTToken) + } - res, err := c.HTTPClient.Do(req) + r, err := c.c.Do(req) if err != nil { return err } - defer res.Body.Close() - - if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest { - var errRes errorResponse - if err = json.NewDecoder(res.Body).Decode(&errRes); err == nil { - return errors.New(errRes.Message) - } + defer r.Body.Close() - return fmt.Errorf("unknown error, status code: %d", res.StatusCode) - } - - if err = json.NewDecoder(res.Body).Decode(v); err != nil { - return err + if r.StatusCode < http.StatusOK || r.StatusCode >= http.StatusBadRequest { + return fmt.Errorf("request was unsuccessful. Status code: %d", r.StatusCode) } - return nil + return json.NewDecoder(r.Body).Decode(&res) } diff --git a/lib/go/httpclient/payloads/BUILD b/lib/go/httpclient/payloads/BUILD index a1c95042f5..33775dcaf9 100644 --- a/lib/go/httpclient/payloads/BUILD +++ b/lib/go/httpclient/payloads/BUILD @@ -4,10 +4,12 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "payloads", srcs = [ - "login_request.go", - "login_response.go", - "signup_request.go", - "signup_response.go", + "client_config_request_payload.go", + "client_config_response_payload.go", + "login_request_payload.go", + "login_response_payload.go", + "signup_request_payload.go", + "signup_response_payload.go", ], importpath = "github.com/airyhq/airy/lib/go/httpclient/payloads", visibility = ["//visibility:public"], diff --git a/lib/go/httpclient/payloads/client_config_request_payload.go b/lib/go/httpclient/payloads/client_config_request_payload.go new file mode 100644 index 0000000000..f132cbb455 --- /dev/null +++ b/lib/go/httpclient/payloads/client_config_request_payload.go @@ -0,0 +1,4 @@ +package payloads + +type ClientConfigRequestPayload struct { +} diff --git a/lib/go/httpclient/payloads/client_config_response_payload.go b/lib/go/httpclient/payloads/client_config_response_payload.go new file mode 100644 index 0000000000..23ab1d77a6 --- /dev/null +++ b/lib/go/httpclient/payloads/client_config_response_payload.go @@ -0,0 +1,6 @@ +package payloads + +type ClientConfigResponsePayload struct { + Components map[string]map[string]interface{} `json:"components"` + Features map[string]string `json:"features"` +} diff --git a/lib/go/httpclient/payloads/login_request.go b/lib/go/httpclient/payloads/login_request_payload.go similarity index 100% rename from lib/go/httpclient/payloads/login_request.go rename to lib/go/httpclient/payloads/login_request_payload.go diff --git a/lib/go/httpclient/payloads/login_response.go b/lib/go/httpclient/payloads/login_response_payload.go similarity index 100% rename from lib/go/httpclient/payloads/login_response.go rename to lib/go/httpclient/payloads/login_response_payload.go diff --git a/lib/go/httpclient/payloads/signup_request.go b/lib/go/httpclient/payloads/signup_request_payload.go similarity index 100% rename from lib/go/httpclient/payloads/signup_request.go rename to lib/go/httpclient/payloads/signup_request_payload.go diff --git a/lib/go/httpclient/payloads/signup_response.go b/lib/go/httpclient/payloads/signup_response_payload.go similarity index 100% rename from lib/go/httpclient/payloads/signup_response.go rename to lib/go/httpclient/payloads/signup_response_payload.go diff --git a/lib/go/httpclient/users.go b/lib/go/httpclient/users.go index 9c858be202..8a5af50f6f 100644 --- a/lib/go/httpclient/users.go +++ b/lib/go/httpclient/users.go @@ -7,31 +7,30 @@ import ( ) func (c *Client) Signup(signupRequestPayload payloads.SignupRequestPayload) (*payloads.SignupResponsePayload, error) { - requestDataJSON, err := json.Marshal(signupRequestPayload) + payload, err := json.Marshal(signupRequestPayload) if err != nil { return nil, err } + res := payloads.SignupResponsePayload{} - if err := c.sendRequest(requestDataJSON, "users.signup", &res); err != nil { - return nil, err + e := c.post("users.signup", payload, &res) + if e != nil { + return nil, e } return &res, nil - } func (c *Client) Login(loginRequestPayload payloads.LoginRequestPayload) (*payloads.LoginResponsePayload, error) { - requestDataJSON, err := json.Marshal(loginRequestPayload) + payload, err := json.Marshal(loginRequestPayload) if err != nil { return nil, err } res := payloads.LoginResponsePayload{} - - if err := c.sendRequest(requestDataJSON, "users.login", &res); err != nil { - return nil, err + e := c.post("users.login", payload, &res) + if e != nil { + return nil, e } - return &res, nil - } From 824d0b5c1345624d4d0564bd5099b20187785aa7 Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Wed, 20 Jan 2021 14:28:56 +0100 Subject: [PATCH 28/56] Add missing docs for authenticating with the websocket (#689) --- docs/docs/api/http.md | 2 +- docs/docs/api/websocket.md | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/docs/api/http.md b/docs/docs/api/http.md index 318cd23b1f..bf5a2260e8 100644 --- a/docs/docs/api/http.md +++ b/docs/docs/api/http.md @@ -11,7 +11,7 @@ compose the Airy API. The HTTP endpoints adhere to the following conventions: - Endpoints only accept `POST` JSON requests. -- Communication always requires a valid [JWT token](#authorization), except for +- Communication always requires a valid [JWT token](#authentication), except for `/users.login` and `/users.signup` endpoints. - We use dots for namespacing URLs (eg `/things.add`). diff --git a/docs/docs/api/websocket.md b/docs/docs/api/websocket.md index 1c16f48a73..287ac1d3be 100644 --- a/docs/docs/api/websocket.md +++ b/docs/docs/api/websocket.md @@ -11,6 +11,9 @@ uses the [STOMP](https://en.wikipedia.org/wiki/Streaming_Text_Oriented_Messaging_Protocol) protocol endpoint at `/ws.communication`. +To execute the handshake with `/ws.communicaiton` you need to set an `Authorization` header where the +value is the authorization token obtained [from the API](http.md#authentication). + ## Outbound Queues Outbound queues follow the pattern `/queue/:event_type[/:action}]` and From 91212d4518e5b5a38f764476bbcd7b825ef2b32c Mon Sep 17 00:00:00 2001 From: Paulo Diniz Date: Thu, 21 Jan 2021 09:02:07 +0100 Subject: [PATCH 29/56] =?UTF-8?q?[#622]=20Affect=20only=20deployments=20wi?= =?UTF-8?q?th=20a=20particular=E2=80=A6=20(#688)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../apps/charts/sources-chatplugin/templates/deployment.yaml | 1 + .../sources-facebook-connector/templates/deployment.yaml | 1 + .../sources-facebook-events-router/templates/deployment.yaml | 1 + .../charts/sources-google-connector/templates/deployment.yaml | 1 + .../sources-google-events-router/templates/deployment.yaml | 1 + .../charts/sources-twilio-connector/templates/deployment.yaml | 1 + .../sources-twilio-events-router/templates/deployment.yaml | 1 + .../helm-chart/charts/controller/templates/deployment.yaml | 3 +++ 8 files changed, 10 insertions(+) diff --git a/infrastructure/helm-chart/charts/apps/charts/sources-chatplugin/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/sources-chatplugin/templates/deployment.yaml index e0aab7b08e..41eb556183 100644 --- a/infrastructure/helm-chart/charts/apps/charts/sources-chatplugin/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/sources-chatplugin/templates/deployment.yaml @@ -6,6 +6,7 @@ metadata: labels: app: sources-chatplugin type: sources-chatplugin + core.airy.co/managed: "true" spec: replicas: 0 selector: diff --git a/infrastructure/helm-chart/charts/apps/charts/sources-facebook-connector/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/sources-facebook-connector/templates/deployment.yaml index 8230e0edf4..7d551079b0 100644 --- a/infrastructure/helm-chart/charts/apps/charts/sources-facebook-connector/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/sources-facebook-connector/templates/deployment.yaml @@ -6,6 +6,7 @@ metadata: labels: app: sources-facebook-connector type: sources-facebook + core.airy.co/managed: "true" spec: replicas: 0 selector: diff --git a/infrastructure/helm-chart/charts/apps/charts/sources-facebook-events-router/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/sources-facebook-events-router/templates/deployment.yaml index 4e7356cdef..4430f199ed 100644 --- a/infrastructure/helm-chart/charts/apps/charts/sources-facebook-events-router/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/sources-facebook-events-router/templates/deployment.yaml @@ -6,6 +6,7 @@ metadata: labels: app: sources-facebook-events-router type: sources-facebook + core.airy.co/managed: "true" spec: replicas: 0 selector: diff --git a/infrastructure/helm-chart/charts/apps/charts/sources-google-connector/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/sources-google-connector/templates/deployment.yaml index a0051a9555..c92caaee2a 100644 --- a/infrastructure/helm-chart/charts/apps/charts/sources-google-connector/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/sources-google-connector/templates/deployment.yaml @@ -6,6 +6,7 @@ metadata: labels: app: sources-google-connector type: sources-google + core.airy.co/managed: "true" spec: replicas: 0 selector: diff --git a/infrastructure/helm-chart/charts/apps/charts/sources-google-events-router/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/sources-google-events-router/templates/deployment.yaml index bf366ca3c3..2e872a4496 100644 --- a/infrastructure/helm-chart/charts/apps/charts/sources-google-events-router/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/sources-google-events-router/templates/deployment.yaml @@ -6,6 +6,7 @@ metadata: labels: app: sources-google-events-router type: sources-google + core.airy.co/managed: "true" spec: replicas: 0 selector: diff --git a/infrastructure/helm-chart/charts/apps/charts/sources-twilio-connector/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/sources-twilio-connector/templates/deployment.yaml index 1fbb942ecf..dfddbfa752 100644 --- a/infrastructure/helm-chart/charts/apps/charts/sources-twilio-connector/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/sources-twilio-connector/templates/deployment.yaml @@ -6,6 +6,7 @@ metadata: labels: app: sources-twilio-connector type: sources-twilio + core.airy.co/managed: "true" spec: replicas: 0 selector: diff --git a/infrastructure/helm-chart/charts/apps/charts/sources-twilio-events-router/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/sources-twilio-events-router/templates/deployment.yaml index ad099f29c4..c0245ab674 100644 --- a/infrastructure/helm-chart/charts/apps/charts/sources-twilio-events-router/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/sources-twilio-events-router/templates/deployment.yaml @@ -6,6 +6,7 @@ metadata: labels: app: sources-twilio-events-router type: sources-twilio + core.airy.co/managed: "true" spec: replicas: 0 selector: diff --git a/infrastructure/helm-chart/charts/controller/templates/deployment.yaml b/infrastructure/helm-chart/charts/controller/templates/deployment.yaml index cbceda4924..1b7d39f419 100644 --- a/infrastructure/helm-chart/charts/controller/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/controller/templates/deployment.yaml @@ -26,3 +26,6 @@ spec: - name: app image: "{{ .Values.global.containerRegistry}}/{{ .Values.image }}:{{ .Values.global.appImageTag }}" imagePullPolicy: Always + env: + - name: LABEL_SELECTOR + value: "core.airy.co/managed=true" From 73839173e9f5dd46783d55e3be88245a9e1aabe8 Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Thu, 21 Jan 2021 09:33:29 +0100 Subject: [PATCH 30/56] [#667] Support Google suggestion responses (#690) * [#667] Support Google suggestion responses * Fix tests --- .../airy/core/sources/google/EventInfo.java | 20 +++++++ .../core/sources/google/EventsRouter.java | 5 +- .../core/sources/google/InfoExtractor.java | 1 + .../core/sources/google/WebhookEvent.java | 5 +- .../core/sources/google/EventsRouterTest.java | 12 ++-- docs/docs/api/webhook.md | 4 +- .../mapping/sources/google/GoogleMapper.java | 16 +++++ .../test/java/co/airy/mapping/GoogleTest.java | 60 +++++++------------ .../resources/google/suggestionResponse.json | 15 +++++ .../src/test/resources/google/text.json | 14 +++++ 10 files changed, 103 insertions(+), 49 deletions(-) create mode 100644 lib/java/mapping/src/test/resources/google/suggestionResponse.json create mode 100644 lib/java/mapping/src/test/resources/google/text.json diff --git a/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/EventInfo.java b/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/EventInfo.java index 894379ac13..0fabc1fe7b 100644 --- a/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/EventInfo.java +++ b/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/EventInfo.java @@ -1,12 +1,16 @@ package co.airy.core.sources.google; import co.airy.avro.communication.Channel; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.JsonNode; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; @Data @AllArgsConstructor @@ -19,4 +23,20 @@ public class EventInfo implements Serializable { private Channel channel; private Long timestamp; private boolean isMessage; + + @JsonIgnore + public Map getMessageHeaders() { + final Map headers = new HashMap<>(); + + final JsonNode suggestionResponse = event.getSuggestionResponse(); + if (suggestionResponse != null) { + if (suggestionResponse.get("postbackData") != null) { + headers.put("postback.payload", suggestionResponse.get("postbackData").textValue()); + } else { + headers.put("postback.payload", "__empty__"); + } + } + + return headers; + } } diff --git a/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/EventsRouter.java b/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/EventsRouter.java index fc5e4f9e73..6f8c969aa9 100644 --- a/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/EventsRouter.java +++ b/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/EventsRouter.java @@ -29,7 +29,6 @@ import java.time.Instant; import java.util.ArrayList; import java.util.List; -import java.util.Map; import static co.airy.model.metadata.MetadataRepository.getId; import static co.airy.core.sources.google.InfoExtractor.getMetadataFromContext; @@ -112,8 +111,8 @@ private void startStream() { final String conversationId = UUIDv5.fromNamespaceAndName(channel.getId(), sourceConversationId).toString(); final List> records = new ArrayList<>(); + final String messageId = UUIDv5.fromNamespaceAndName(channel.getId(), payload).toString(); if (webhookEvent.hasMessage()) { - final String messageId = UUIDv5.fromNamespaceAndName(channel.getId(), payload).toString(); records.add(KeyValue.pair(messageId, Message.newBuilder() .setSource(channel.getSource()) @@ -124,7 +123,7 @@ private void startStream() { .setSenderType(SenderType.SOURCE_CONTACT) .setContent(payload) .setSenderId(sourceConversationId) - .setHeaders(Map.of()) + .setHeaders(event.getMessageHeaders()) .setSentAt(event.getTimestamp()) .setUpdatedAt(null) .build() diff --git a/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/InfoExtractor.java b/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/InfoExtractor.java index e1ddab2987..33d111d144 100644 --- a/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/InfoExtractor.java +++ b/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/InfoExtractor.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; diff --git a/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/WebhookEvent.java b/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/WebhookEvent.java index 40abcba871..65aa480567 100644 --- a/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/WebhookEvent.java +++ b/backend/sources/google/events-router/src/main/java/co/airy/core/sources/google/WebhookEvent.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.JsonNode; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -10,6 +11,7 @@ @Data @NoArgsConstructor +@AllArgsConstructor public class WebhookEvent { private String agent; @@ -36,7 +38,8 @@ public JsonNode getPayload() { @JsonIgnore public boolean hasMessage() { - return this.message != null; + // since suggestion responses can be rendered, we consider them messages + return this.message != null || this.suggestionResponse != null; } @JsonIgnore diff --git a/backend/sources/google/events-router/src/test/java/co/airy/core/sources/google/EventsRouterTest.java b/backend/sources/google/events-router/src/test/java/co/airy/core/sources/google/EventsRouterTest.java index 04b303838c..bb62059cc6 100644 --- a/backend/sources/google/events-router/src/test/java/co/airy/core/sources/google/EventsRouterTest.java +++ b/backend/sources/google/events-router/src/test/java/co/airy/core/sources/google/EventsRouterTest.java @@ -118,12 +118,12 @@ void canRouteGoogleMetadata() throws Exception { final String singleName = "hal9000"; // Two different event types that both carry context - final String messagePayload = "{ \"agent\": \"brands/somebrand/agents/%s\", \"conversationId\": \"CONVERSATION_ID\"," + - " \"customAgentId\": \"CUSTOM_AGENT_ID\", \"message\": { \"messageId\": \"MESSAGE_ID\", \"name\": \"conversations/CONVERSATION_ID/messages/MESSAGE_ID\", \"text\": \"MESSAGE_TEXT\", \"createTime\": \"MESSAGE_CREATE_TIME\" }," + - " \"context\": { \"userInfo\": { \"displayName\": \"%s\" } }, \"sendTime\": \"2014-10-02T15:01:23.045123456Z\" }"; - final String userStatusPayload = "{ \"agent\": \"brands/somebrand/agents/%s\", \"conversationId\": \"CONVERSATION_ID\", " + - "\"customAgentId\": \"CUSTOM_AGENT_ID\", \"userStatus\": { \"isTyping\": true }, " + - "\"context\": { \"userInfo\": { \"displayName\": \"%s\" } }, \"sendTime\": \"2014-10-02T15:01:23.045123456Z\" }"; + final String messagePayload = "{\"agent\":\"brands/somebrand/agents/%s\",\"conversationId\":\"CONVERSATION_ID\"," + + "\"customAgentId\":\"CUSTOM_AGENT_ID\",\"message\":{\"messageId\":\"MESSAGE_ID\",\"name\":\"conversations/CONVERSATION_ID/messages/MESSAGE_ID\",\"text\":\"MESSAGE_TEXT\",\"createTime\":\"MESSAGE_CREATE_TIME\"}," + + "\"context\":{\"userInfo\":{\"displayName\":\"%s\"}},\"sendTime\":\"2014-10-02T15:01:23.045123456Z\"}"; + final String userStatusPayload = "{\"agent\":\"brands/somebrand/agents/%s\",\"conversationId\":\"CONVERSATION_ID\"," + + "\"customAgentId\":\"CUSTOM_AGENT_ID\",\"userStatus\":{\"isTyping\":true}," + + "\"context\":{\"userInfo\":{\"displayName\":\"%s\"}},\"sendTime\":\"2014-10-02T15:01:23.045123456Z\"}"; List> events = List.of( new ProducerRecord<>(sourceGoogleEvents.name(), UUID.randomUUID().toString(), String.format(messagePayload, agentId, displayName)), diff --git a/docs/docs/api/webhook.md b/docs/docs/api/webhook.md index c986850f32..519c11e2c4 100644 --- a/docs/docs/api/webhook.md +++ b/docs/docs/api/webhook.md @@ -80,7 +80,7 @@ Subscribes the webhook for the first time or update its parameters. ## Event Payload -After [subscribing](#subscribing-to-a-webhook) to an Airy webhook, you will +After [subscribing](#subscribing) to an Airy webhook, you will start receiving events on your URL of choice. The event will _always_ be a POST request with the following structure: @@ -92,7 +92,7 @@ request with the following structure: "id": "adac9220-fe7b-40a8-98e5-2fcfaf4a53b5", "type": "source_contact" }, - "source": "FACEBOOK", + "source": "facebook", "sent_at": "2020-07-20T14:18:08.584Z", "text": "Message to be sent" } diff --git a/lib/java/mapping/src/main/java/co/airy/mapping/sources/google/GoogleMapper.java b/lib/java/mapping/src/main/java/co/airy/mapping/sources/google/GoogleMapper.java index 0b252c8233..6ce335b784 100644 --- a/lib/java/mapping/src/main/java/co/airy/mapping/sources/google/GoogleMapper.java +++ b/lib/java/mapping/src/main/java/co/airy/mapping/sources/google/GoogleMapper.java @@ -32,7 +32,18 @@ public List getIdentifiers() { public List render(String payload) throws Exception { final JsonNode jsonNode = objectMapper.readTree(payload); final JsonNode messageNode = jsonNode.get("message"); + if (messageNode != null) { + return renderMessage(messageNode); + } + final JsonNode suggestionResponseNode = jsonNode.get("suggestionResponse"); + if (suggestionResponseNode != null) { + return renderSuggestionResponse(suggestionResponseNode); + } + + throw new Exception("google mapper only supports `message` and `suggestionResponse`"); + } + private List renderMessage(JsonNode messageNode) { final String messageNodeValue = messageNode.get("text").textValue(); if (isGoogleStorageUrl(messageNodeValue)) { return List.of(new Image(messageNodeValue)); @@ -41,6 +52,11 @@ public List render(String payload) throws Exception { } } + private List renderSuggestionResponse(JsonNode suggestionResponseNode) { + final String textContent = suggestionResponseNode.get("text").textValue(); + return List.of(new Text(textContent)); + } + private boolean isGoogleStorageUrl(final String url) { URI uri; try { diff --git a/lib/java/mapping/src/test/java/co/airy/mapping/GoogleTest.java b/lib/java/mapping/src/test/java/co/airy/mapping/GoogleTest.java index 2a11d8e72c..f33159bbaa 100644 --- a/lib/java/mapping/src/test/java/co/airy/mapping/GoogleTest.java +++ b/lib/java/mapping/src/test/java/co/airy/mapping/GoogleTest.java @@ -4,6 +4,9 @@ import co.airy.mapping.model.Text; import co.airy.mapping.sources.google.GoogleMapper; import org.junit.jupiter.api.Test; +import org.springframework.util.StreamUtils; + +import java.nio.charset.StandardCharsets; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -13,48 +16,31 @@ public class GoogleTest { @Test void canRenderText() throws Exception { - final String content = "{\n" + - " \"message\": {\n" + - " \"name\": \"conversations/9cec28cc-8dbe-40d0-ad68-edd0f440c743/messages/3A25E132-20D6-4A5D-8602-7DF4979F181B\",\n" + - " \"text\": \"Yes confirmed\",\n" + - " \"createTime\": \"2020-05-14T12:45:54.531828Z\",\n" + - " \"messageId\": \"3A25E132-20D6-4A5D-8602-7DF4979F181B\"\n" + - " },\n" + - " \"context\": {},\n" + - " \"sendTime\": \"2020-05-14T12:45:55.302Z\",\n" + - " \"conversationId\": \"9cec28cc-8dbe-40d0-ad68-edd0f440c743\",\n" + - " \"customAgentId\": \"5b43b04d-aa75-4b7b-bdca-28e90a344db1\",\n" + - " \"requestId\": \"3A25E132-20D6-4A5D-8602-7DF4979F181B\",\n" + - " \"agent\": \"brands/af0ef816-cef8-479e-b4b6-650d5e8b90b1/agents/31a8d3e0-490f-4ecc-887b-42df4dd1952e\"\n" + - "}"; - - final Text message = (Text) mapper.render(content).get(0); - assertThat(message.getText(), equalTo("Yes confirmed")); + final String textContent = "Hello World"; + final String sourceContent = String.format(StreamUtils.copyToString(getClass().getClassLoader() + .getResourceAsStream("google/text.json"), StandardCharsets.UTF_8), textContent); + + final Text message = (Text) mapper.render(sourceContent).get(0); + assertThat(message.getText(), equalTo(textContent)); } @Test void canRenderImage() throws Exception { final String signedImageUrl = "https://storage.googleapis.com/business-messages-us/936640919331/jzsu6cdguNGsBhmGJGuLs1DS?x-goog-algorithm\u003dGOOG4-RSA-SHA256\u0026x-goog-credential\u003duranium%40rcs-uranium.iam.gserviceaccount.com%2F20190826%2Fauto%2Fstorage%2Fgoog4_request\u0026x-goog-date\u003d20190826T201038Z\u0026x-goog-expires\u003d604800\u0026x-goog-signedheaders\u003dhost\u0026x-goog-signature\u003d89dbf7a74d21ab42ad25be071b37840a544a43d68e67270382054e1442d375b0b53d15496dbba12896b9d88a6501cac03b5cfca45d789da3e0cae75b050a89d8f54c1ffb27e467bd6ba1d146b7d42e30504c295c5c372a46e44728f554ba74b7b99bd9c6d3ed45f18588ed1b04522af1a47330cff73a711a6a8c65bb15e3289f480486f6695127e1014727cac949e284a7f74afd8220840159c589d48dddef1cc97b248dfc34802570448242eac4d7190b1b10a008404a330b4ff6f9656fa84e87f9a18ab59dc9b91e54ad11ffdc0ad1dc9d1ccc7855c0d263d93fce6f999971ec79879f922b582cf3bb196a1fedc3eefa226bb412e49af7dfd91cc072608e98"; - final String content = "{\n" + - " \"agent\": \"brands/BRAND_ID/agents/AGENT_ID\",\n" + - " \"conversationId\": \"CONVERSATION_ID\",\n" + - " \"customAgentId\": \"CUSTOM_AGENT_ID\",\n" + - " \"requestId\": \"REQUEST_ID\",\n" + - " \"message\": {\n" + - " \"messageId\": \"MESSAGE_ID\",\n" + - " \"name\": \"conversations/CONVERSATION_ID/messages/MESSAGE_ID\",\n" + - " \"text\": \"" + signedImageUrl + "\",\n" + - " \"createTime\": \"MESSAGE_CREATE_TIME\"\n" + - " },\n" + - " \"context\": {},\n" + - " \"sendTime\": \"2020-05-14T12:45:55.302Z\",\n" + - " \"conversationId\": \"9cec28cc-8dbe-40d0-ad68-edd0f440c743\",\n" + - " \"customAgentId\": \"5b43b04d-aa75-4b7b-bdca-28e90a344db1\",\n" + - " \"requestId\": \"3A25E132-20D6-4A5D-8602-7DF4979F181B\",\n" + - " \"agent\": \"brands/af0ef816-cef8-479e-b4b6-650d5e8b90b1/agents/31a8d3e0-490f-4ecc-887b-42df4dd1952e\"\n" + - "}\n"; - - final Image message = (Image) mapper.render(content).get(0); + final String sourceContent = String.format(StreamUtils.copyToString(getClass().getClassLoader() + .getResourceAsStream("google/text.json"), StandardCharsets.UTF_8), signedImageUrl); + + final Image message = (Image) mapper.render(sourceContent).get(0); assertThat(message.getUrl(), equalTo(signedImageUrl)); } + + @Test + void canRenderSuggestionResponses() throws Exception { + final String textContent = "Hello World"; + final String sourceContent = String.format(StreamUtils.copyToString(getClass().getClassLoader() + .getResourceAsStream("google/suggestionResponse.json"), StandardCharsets.UTF_8), textContent); + + final Text message = (Text) mapper.render(sourceContent).get(0); + assertThat(message.getText(), equalTo(textContent)); + } } diff --git a/lib/java/mapping/src/test/resources/google/suggestionResponse.json b/lib/java/mapping/src/test/resources/google/suggestionResponse.json new file mode 100644 index 0000000000..5b6f1110a1 --- /dev/null +++ b/lib/java/mapping/src/test/resources/google/suggestionResponse.json @@ -0,0 +1,15 @@ +{ + "suggestionResponse": { + "message": "conversations/11ab7bf3-0410-46a6-bcce-1877ca6957b0/messages/11ab7bf3-0410-46a6-bcce-1877ca6957b0", + "postbackData": "postback-data", + "createTime": "2020-07-13T12:54:50.479632Z", + "text": "%s", + "type": "REPLY" + }, + "context": {}, + "sendTime": "2020-05-14T12:45:55.302Z", + "conversationId": "11ab7bf3-0410-46a6-bcce-1877ca6957b0", + "customAgentId": "11ab7bf3-0410-46a6-bcce-1877ca6957b0", + "requestId": "11ab7bf3-0410-46a6-bcce-1877ca6957b0", + "agent": "brands/11ab7bf3-0410-46a6-bcce-1877ca6957b0/agents/11ab7bf3-0410-46a6-bcce-1877ca6957b0" +} diff --git a/lib/java/mapping/src/test/resources/google/text.json b/lib/java/mapping/src/test/resources/google/text.json new file mode 100644 index 0000000000..fa2156c57f --- /dev/null +++ b/lib/java/mapping/src/test/resources/google/text.json @@ -0,0 +1,14 @@ +{ + "message": { + "name": "conversations/11ab7bf3-0410-46a6-bcce-1877ca6957b0/messages/11ab7bf3-0410-46a6-bcce-1877ca6957b0", + "text": "%s", + "createTime": "2020-05-14T12:45:54.531828Z", + "messageId": "11ab7bf3-0410-46a6-bcce-1877ca6957b0" + }, + "context": {}, + "sendTime": "2020-05-14T12:45:55.302Z", + "conversationId": "11ab7bf3-0410-46a6-bcce-1877ca6957b0", + "customAgentId": "11ab7bf3-0410-46a6-bcce-1877ca6957b0", + "requestId": "11ab7bf3-0410-46a6-bcce-1877ca6957b0", + "agent": "brands/11ab7bf3-0410-46a6-bcce-1877ca6957b0/agents/11ab7bf3-0410-46a6-bcce-1877ca6957b0" +} From aa5b7845516cc0a071b8b6932faad2ffeeea43d5 Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Thu, 21 Jan 2021 09:38:19 +0100 Subject: [PATCH 31/56] [#644] Facebook connector sends out templates (#683) --- .../payload/SendMessageRequestPayload.java | 11 +---- .../core/chat_plugin/ChatControllerTest.java | 4 +- backend/sources/facebook/connector/BUILD | 1 + .../airy/core/sources/facebook/api/Api.java | 5 +-- .../core/sources/facebook/api/Mapper.java | 29 ++++++++++---- .../api/model/SendMessagePayload.java | 26 ++++++------ .../sources/facebook/SendMessageTest.java | 11 ++++- backend/sources/google/connector/BUILD | 1 + .../airy/core/sources/google/Connector.java | 4 +- .../core/sources/google/services/Mapper.java | 18 +++++---- .../core/sources/google/SendMessageTest.java | 5 ++- .../airy/core/sources/twilio/Connector.java | 4 +- .../core/sources/twilio/SendMessageTest.java | 5 ++- docs/docs/sources/chat-plugin.md | 1 + docs/docs/sources/facebook.md | 40 +++++++++++++++++++ frontend/chat-plugin/BUILD | 9 ++++- .../chat-plugin/src/components/api/index.tsx | 8 +++- .../chat-plugin/src/components/chat/index.tsx | 11 ++--- .../src/components/websocket/index.ts | 5 +-- frontend/demo/BUILD | 1 + .../java/co/airy/mapping/OutboundMapper.java | 4 +- .../co/airy/mapping/ContentMapperTest.java | 8 ++-- lib/typescript/types/BUILD | 6 ++- lib/typescript/types/global.d.ts | 7 ---- lib/typescript/types/index.ts | 1 + tsconfig.json | 12 ++++++ 26 files changed, 159 insertions(+), 78 deletions(-) create mode 100644 lib/typescript/types/index.ts diff --git a/backend/sources/chat-plugin/src/main/java/co/airy/core/chat_plugin/payload/SendMessageRequestPayload.java b/backend/sources/chat-plugin/src/main/java/co/airy/core/chat_plugin/payload/SendMessageRequestPayload.java index 4252796e02..6904a5f65a 100644 --- a/backend/sources/chat-plugin/src/main/java/co/airy/core/chat_plugin/payload/SendMessageRequestPayload.java +++ b/backend/sources/chat-plugin/src/main/java/co/airy/core/chat_plugin/payload/SendMessageRequestPayload.java @@ -1,5 +1,6 @@ package co.airy.core.chat_plugin.payload; +import co.airy.mapping.model.Text; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -11,13 +12,5 @@ @NoArgsConstructor public class SendMessageRequestPayload { @NotNull - private MessagePayload message; - - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class MessagePayload { - @NotBlank - private String text; - } + private Text message; } diff --git a/backend/sources/chat-plugin/src/test/java/co/airy/core/chat_plugin/ChatControllerTest.java b/backend/sources/chat-plugin/src/test/java/co/airy/core/chat_plugin/ChatControllerTest.java index 0f454858c6..267ec3c780 100644 --- a/backend/sources/chat-plugin/src/test/java/co/airy/core/chat_plugin/ChatControllerTest.java +++ b/backend/sources/chat-plugin/src/test/java/co/airy/core/chat_plugin/ChatControllerTest.java @@ -121,7 +121,7 @@ void authenticateSendAndReceive() throws Exception { final CompletableFuture messageFuture = subscribe(token, port, MessageUpsertPayload.class, QUEUE_MESSAGE); final String messageText = "answer is 42"; - String sendMessagePayload = "{\"message\": { \"text\": \"" + messageText + "\" }}"; + String sendMessagePayload = "{\"message\": { \"text\": \"" + messageText + "\", \"type\":\"text\" }}"; retryOnException(() -> mvc.perform(post("/chatplugin.send") .headers(buildHeaders(token)) .content(sendMessagePayload)) @@ -150,7 +150,7 @@ void canResumeConversation() throws Exception { final String authToken = jsonNode.get("token").textValue(); final String messageText = "Talk to you later!"; - final String sendMessagePayload = "{\"message\": { \"text\": \"" + messageText + "\" }}"; + final String sendMessagePayload = "{\"message\": { \"text\": \"" + messageText + "\", \"type\":\"text\" }}"; mvc.perform(post("/chatplugin.send") .headers(buildHeaders(authToken)) .content(sendMessagePayload)) diff --git a/backend/sources/facebook/connector/BUILD b/backend/sources/facebook/connector/BUILD index 04704f3e58..a03e6ad0d3 100644 --- a/backend/sources/facebook/connector/BUILD +++ b/backend/sources/facebook/connector/BUILD @@ -10,6 +10,7 @@ app_deps = [ "//backend/model/metadata", "//lib/java/uuid", "//lib/java/log", + "//lib/java/mapping", "//lib/java/spring/web:spring-web", "//lib/java/spring/auth:spring-auth", "//lib/java/spring/kafka/core:spring-kafka-core", diff --git a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/api/Api.java b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/api/Api.java index 2112768cbe..9b4ddd1cc6 100644 --- a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/api/Api.java +++ b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/api/Api.java @@ -60,8 +60,7 @@ public class Api implements ApplicationListener { public Api(ObjectMapper objectMapper, RestTemplateBuilder restTemplateBuilder, @Value("${facebook.app-id}") String appId, - @Value("${facebook.app-secret}") String apiSecret - ) { + @Value("${facebook.app-secret}") String apiSecret) { httpHeaders.setContentType(MediaType.APPLICATION_JSON); this.objectMapper = objectMapper; this.restTemplateBuilder = restTemplateBuilder; @@ -71,7 +70,6 @@ public Api(ObjectMapper objectMapper, RestTemplateBuilder restTemplateBuilder, public void sendMessage(final String pageToken, SendMessagePayload sendMessagePayload) { String fbReqUrl = String.format(requestTemplate, pageToken); - restTemplate.postForEntity(fbReqUrl, new HttpEntity<>(sendMessagePayload, httpHeaders), FbSendMessageResponse.class); } @@ -94,7 +92,6 @@ public List getPagesInfo(String accessToken) throws Excepti private T apiResponse(String url, HttpMethod method, Class clazz) throws Exception { ResponseEntity responseEntity = restTemplate.exchange(url, method, null, String.class); - return objectMapper.readValue(responseEntity.getBody(), clazz); } diff --git a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/api/Mapper.java b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/api/Mapper.java index a120d7d9ff..5a41f71176 100644 --- a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/api/Mapper.java +++ b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/api/Mapper.java @@ -3,18 +3,23 @@ import co.airy.avro.communication.Message; import co.airy.core.sources.facebook.api.model.SendMessagePayload; import co.airy.core.sources.facebook.dto.SendMessageRequest; +import co.airy.mapping.ContentMapper; +import co.airy.mapping.model.Content; +import co.airy.mapping.model.SourceTemplate; +import co.airy.mapping.model.Text; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.stereotype.Service; +import java.util.List; + @Service public class Mapper { + private final ContentMapper mapper; - private final ObjectMapper objectMapper; - - Mapper(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; + Mapper(ContentMapper mapper) { + this.mapper = mapper; } public SendMessagePayload fromSendMessageRequest(SendMessageRequest sendMessageRequest) throws Exception { @@ -22,9 +27,19 @@ public SendMessagePayload fromSendMessageRequest(SendMessageRequest sendMessageR final SendMessagePayload.MessagePayload messagePayload = new SendMessagePayload.MessagePayload(); - final JsonNode messageRequest = objectMapper.readTree(message.getContent()); - - messagePayload.setText(messageRequest.get("text").textValue()); + final Content content = mapper.render(message) + .stream() + .findFirst() + .orElseThrow(() -> new Exception("Message is empty")); + + if (content instanceof Text) { + messagePayload.setText(((Text) content).getText()); + } else if (content instanceof SourceTemplate) { + messagePayload.setAttachment(SendMessagePayload.AttachmentPayload.builder() + .type("template") + .payload(((SourceTemplate) content).getPayload()) + .build()); + } SendMessagePayload.SendMessagePayloadBuilder builder = SendMessagePayload.builder() .recipient(SendMessagePayload.MessageRecipient.builder() diff --git a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/api/model/SendMessagePayload.java b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/api/model/SendMessagePayload.java index 48e8f0c850..f8a73e8f84 100644 --- a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/api/model/SendMessagePayload.java +++ b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/api/model/SendMessagePayload.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -15,36 +16,33 @@ @AllArgsConstructor @JsonInclude(JsonInclude.Include.NON_EMPTY) public class SendMessagePayload { - - @JsonProperty("messaging_type") private String messagingType; - - @JsonProperty("recipient") private MessageRecipient recipient; - - @JsonProperty("message") private MessagePayload message; - - @JsonProperty("tag") private String tag; @Data @Builder @NoArgsConstructor @AllArgsConstructor - @JsonInclude(JsonInclude.Include.NON_EMPTY) public static class MessageRecipient { - - @JsonProperty("id") private String id; } @Data @NoArgsConstructor @AllArgsConstructor - @JsonInclude(JsonInclude.Include.NON_EMPTY) public static class MessagePayload implements Serializable { - @JsonProperty("text") - public String text; + private String text; + private AttachmentPayload attachment; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class AttachmentPayload implements Serializable { + private String type; + private JsonNode payload; } } diff --git a/backend/sources/facebook/connector/src/test/java/co/airy/core/sources/facebook/SendMessageTest.java b/backend/sources/facebook/connector/src/test/java/co/airy/core/sources/facebook/SendMessageTest.java index a0a9e9b08d..6d01a9f31f 100644 --- a/backend/sources/facebook/connector/src/test/java/co/airy/core/sources/facebook/SendMessageTest.java +++ b/backend/sources/facebook/connector/src/test/java/co/airy/core/sources/facebook/SendMessageTest.java @@ -13,7 +13,11 @@ import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; import co.airy.kafka.test.KafkaTestHelper; import co.airy.kafka.test.junit.SharedKafkaTestResource; +import co.airy.mapping.model.SourceTemplate; import co.airy.spring.core.AirySpringBootApplication; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.kafka.clients.producer.ProducerRecord; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -125,6 +129,9 @@ void canSendMessageViaTheFacebookApi() throws Exception { TimeUnit.SECONDS.sleep(5); + final ObjectMapper objectMapper = new ObjectMapper(); + final JsonNode attachmentPayload = objectMapper.readTree("{\"a\":\"template payload\"}"); + kafkaTestHelper.produceRecord(new ProducerRecord<>(applicationCommunicationMessages.name(), messageId, Message.newBuilder() .setId(messageId) @@ -135,14 +142,14 @@ void canSendMessageViaTheFacebookApi() throws Exception { .setConversationId(conversationId) .setChannelId(channelId) .setSource("facebook") - .setContent("{\"text\":\"" + text + "\"}") + .setContent(objectMapper.writeValueAsString(new SourceTemplate(attachmentPayload))) .build()) ); retryOnException(() -> { final SendMessagePayload sendMessagePayload = payloadCaptor.getValue(); assertThat(sendMessagePayload.getRecipient().getId(), equalTo(sourceConversationId)); - assertThat(sendMessagePayload.getMessage().getText(), equalTo(text)); + assertThat(sendMessagePayload.getMessage().getAttachment().getPayload(), equalTo(attachmentPayload)); assertThat(tokenCaptor.getValue(), equalTo(token)); }, "Facebook API was not called"); diff --git a/backend/sources/google/connector/BUILD b/backend/sources/google/connector/BUILD index dd317369ba..228830b2e0 100644 --- a/backend/sources/google/connector/BUILD +++ b/backend/sources/google/connector/BUILD @@ -10,6 +10,7 @@ app_deps = [ "//lib/java/spring/kafka/core:spring-kafka-core", "//lib/java/spring/kafka/streams:spring-kafka-streams", "//lib/java/uuid", + "//lib/java/mapping", "//lib/java/kafka/schema:source-google-events", "//lib/java/spring/web:spring-web", "//lib/java/spring/auth:spring-auth", diff --git a/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/Connector.java b/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/Connector.java index fcaef49dbc..2c597adb38 100644 --- a/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/Connector.java +++ b/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/Connector.java @@ -40,9 +40,9 @@ public Message sendMessage(SendMessageRequest sendMessageRequest) { updateDeliveryState(message, DeliveryState.DELIVERED); return message; } catch (ApiException e) { - log.error(String.format("Failed to send a message to Google \n SendMessageRequest: %s \n Error Message: %s \n", sendMessageRequest, e.getMessage()), e); + log.error(String.format("Google Api Exception for SendMessageRequest:\n%s", sendMessageRequest), e); } catch (Exception e) { - log.error(String.format("Failed to send a message to Google \n SendMessageRequest: %s", sendMessageRequest), e); + log.error(String.format("Failed to send a message to Google \nSendMessageRequest: %s", sendMessageRequest), e); } updateDeliveryState(message, DeliveryState.FAILED); diff --git a/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/services/Mapper.java b/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/services/Mapper.java index 8e156c1722..6b206f9bc7 100644 --- a/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/services/Mapper.java +++ b/backend/sources/google/connector/src/main/java/co/airy/core/sources/google/services/Mapper.java @@ -3,25 +3,29 @@ import co.airy.avro.communication.Message; import co.airy.core.sources.google.model.SendMessagePayload; import co.airy.core.sources.google.model.SendMessageRequest; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import co.airy.mapping.ContentMapper; +import co.airy.mapping.model.Text; import org.springframework.stereotype.Service; @Service public class Mapper { - private final ObjectMapper objectMapper; - Mapper(ObjectMapper objectMapper) { - this.objectMapper = objectMapper; + private final ContentMapper mapper; + Mapper(ContentMapper mapper) { + this.mapper = mapper; } public SendMessagePayload fromSendMessageRequest(SendMessageRequest sendMessageRequest) throws Exception { final Message message = sendMessageRequest.getMessage(); - final JsonNode messageRequest = objectMapper.readTree(message.getContent()); + final Text text = (Text) mapper.render(message) + .stream() + .filter(c -> c instanceof Text) + .findFirst() + .orElseThrow(() -> new Exception("google only supports text messages")); return SendMessagePayload.builder() .messageId(message.getId()) .representative(new SendMessagePayload.Representative("HUMAN")) - .text(messageRequest.get("text").textValue()) + .text(text.getText()) .build(); } } diff --git a/backend/sources/google/connector/src/test/java/co/airy/core/sources/google/SendMessageTest.java b/backend/sources/google/connector/src/test/java/co/airy/core/sources/google/SendMessageTest.java index 5ed3139880..935066ea22 100644 --- a/backend/sources/google/connector/src/test/java/co/airy/core/sources/google/SendMessageTest.java +++ b/backend/sources/google/connector/src/test/java/co/airy/core/sources/google/SendMessageTest.java @@ -10,7 +10,9 @@ import co.airy.kafka.schema.application.ApplicationCommunicationMessages; import co.airy.kafka.test.KafkaTestHelper; import co.airy.kafka.test.junit.SharedKafkaTestResource; +import co.airy.mapping.model.Text; import co.airy.spring.core.AirySpringBootApplication; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.kafka.clients.producer.ProducerRecord; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -110,6 +112,7 @@ void canSendMessageViaGoogleApi() throws Exception { TimeUnit.SECONDS.sleep(5); + final ObjectMapper objectMapper = new ObjectMapper(); kafkaTestHelper.produceRecord(new ProducerRecord<>(applicationCommunicationMessages.name(), messageId, Message.newBuilder() .setId(messageId) @@ -120,7 +123,7 @@ void canSendMessageViaGoogleApi() throws Exception { .setConversationId(conversationId) .setChannelId(channelId) .setSource("google") - .setContent("{\"text\":\"" + text + "\"}") + .setContent(objectMapper.writeValueAsString(new Text(text))) .build()) ); diff --git a/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/Connector.java b/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/Connector.java index 69107ca5ff..563f5589de 100644 --- a/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/Connector.java +++ b/backend/sources/twilio/connector/src/main/java/co/airy/core/sources/twilio/Connector.java @@ -40,14 +40,14 @@ public Message sendMessage(SendMessageRequest sendMessageRequest) { .stream() .filter(c -> c instanceof Text) .findFirst() - .orElse(null); + .orElseThrow(() -> new Exception("twilio only supports text messages")); api.sendMessage(from, to, text.getText()); updateDeliveryState(message, DeliveryState.DELIVERED); return message; } catch (ApiException e) { - log.error(String.format("Failed to send a message to Twilio \n SendMessageRequest: %s \n Error Message: %s \n", sendMessageRequest, e.getMessage()), e); + log.error(String.format("Twilio Api Exception for SendMessageRequest:\n%s", sendMessageRequest), e); } catch (Exception e) { log.error(String.format("Failed to send a message to Twilio \n SendMessageRequest: %s", sendMessageRequest), e); } diff --git a/backend/sources/twilio/connector/src/test/java/co/airy/core/sources/twilio/SendMessageTest.java b/backend/sources/twilio/connector/src/test/java/co/airy/core/sources/twilio/SendMessageTest.java index 5e1c2919e2..68a07a6217 100644 --- a/backend/sources/twilio/connector/src/test/java/co/airy/core/sources/twilio/SendMessageTest.java +++ b/backend/sources/twilio/connector/src/test/java/co/airy/core/sources/twilio/SendMessageTest.java @@ -11,7 +11,9 @@ import co.airy.kafka.schema.application.ApplicationCommunicationMessages; import co.airy.kafka.test.KafkaTestHelper; import co.airy.kafka.test.junit.SharedKafkaTestResource; +import co.airy.mapping.model.Text; import co.airy.spring.core.AirySpringBootApplication; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.kafka.clients.producer.ProducerRecord; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -122,6 +124,7 @@ void canSendMessageViaTheTwilioApi() throws Exception { TimeUnit.SECONDS.sleep(5); + final ObjectMapper objectMapper = new ObjectMapper(); kafkaTestHelper.produceRecord(new ProducerRecord<>(applicationCommunicationMessages.name(), messageId, Message.newBuilder() .setId(messageId) @@ -132,7 +135,7 @@ void canSendMessageViaTheTwilioApi() throws Exception { .setConversationId(conversationId) .setChannelId(channelId) .setSource("twilio.sms") - .setContent("{\"text\":\"" + text + "\"}") + .setContent(objectMapper.writeValueAsString(new Text(text))) .build()) ); diff --git a/docs/docs/sources/chat-plugin.md b/docs/docs/sources/chat-plugin.md index 6b2c9d4648..f433bead39 100644 --- a/docs/docs/sources/chat-plugin.md +++ b/docs/docs/sources/chat-plugin.md @@ -179,6 +179,7 @@ header. { "message": { "text": "{String}" + "type": "text" } } ``` diff --git a/docs/docs/sources/facebook.md b/docs/docs/sources/facebook.md index ba6ae36229..e26e3bda09 100644 --- a/docs/docs/sources/facebook.md +++ b/docs/docs/sources/facebook.md @@ -170,3 +170,43 @@ the nature of the request, the response time may vary. ] } ``` + +## Send a template message + +With Facebook messenger you can send [templates](https://developers.facebook.com/docs/messenger-platform/send-messages/templates) to your contacts, +which is useful for automations, FAQs, receipts and many more use cases. To send Facebook templates using the [send message API](/api/http.md#send-a-message) you have to +first build an attachment for Facebook using their template model. Quoting from the docs: + +> The body of the request follows a standard format for all template types, with the `message.attachment.payload` property containing the type and content details that are specific to each template type: +> +> ```json5 +> { +> "recipient": { +> "id": "" +> }, +> "message": { +> "attachment": { +> "type": "template", +> "payload": { +> "template_type": "" +> // ... +> } +> } +> } +> } +> ``` + +Next take the `message.attachment.payload` field and add it to the `message` field on the API request like so: + +```json5 +{ + "conversation_id": "a688d36c-a85e-44af-bc02-4248c2c97622", + "message": { + "payload": { + "template_type": "" + // ... + }, + "type": "source.template" + } +} +``` diff --git a/frontend/chat-plugin/BUILD b/frontend/chat-plugin/BUILD index d71937cd45..2eb6590268 100644 --- a/frontend/chat-plugin/BUILD +++ b/frontend/chat-plugin/BUILD @@ -6,17 +6,23 @@ load("@rules_pkg//:pkg.bzl", "pkg_tar") load("//tools/build:container_push.bzl", "container_push") load("@io_bazel_rules_docker//container:container.bzl", "container_image") +module_deps = [ + "//lib/typescript/types", +] + web_app( name = "bundle", app_lib = ":app", entry = "frontend/chat-plugin/src/iframe.js", index = ":index.html", + module_deps = module_deps, ) web_library( name = "library", app_lib = ":app", entry = "frontend/chat-plugin/src/defaultScript.js", + module_deps = module_deps, output = { "libraryExport": "AiryWidget", "publicPath": "/", @@ -27,8 +33,7 @@ web_library( ts_library( name = "app", tsconfig = ":widget_tsconfig", - deps = [ - "//lib/typescript/types", + deps = module_deps + [ "@npm//@stomp/stompjs", "@npm//@types/node", "@npm//linkifyjs", diff --git a/frontend/chat-plugin/src/components/api/index.tsx b/frontend/chat-plugin/src/components/api/index.tsx index 566a19daa4..d63de03c9b 100644 --- a/frontend/chat-plugin/src/components/api/index.tsx +++ b/frontend/chat-plugin/src/components/api/index.tsx @@ -1,3 +1,5 @@ +import {Text} from 'types'; + declare const window: { airy: { h: string; @@ -9,10 +11,12 @@ declare const window: { const API_HOST = window.airy ? window.airy.h : 'chatplugin.airy'; const TLS_PREFIX = window.airy ? (window.airy.no_tls === true ? '' : 's') : ''; -export const sendMessage = (message: string, token: string) => { +export const sendMessage = (message: Text, token: string) => { return fetch(`http${TLS_PREFIX}://${API_HOST}/chatplugin.send`, { method: 'POST', - body: message, + body: JSON.stringify({ + message, + }), headers: { 'Content-Type': 'application/json', Authorization: token, diff --git a/frontend/chat-plugin/src/components/chat/index.tsx b/frontend/chat-plugin/src/components/chat/index.tsx index 34ee9aaec1..08015e82d0 100644 --- a/frontend/chat-plugin/src/components/chat/index.tsx +++ b/frontend/chat-plugin/src/components/chat/index.tsx @@ -78,13 +78,10 @@ const Chat = (props: Props) => { } }, sendMessage: (text: string) => { - ws.onSend( - JSON.stringify({ - message: { - text, - }, - }) - ); + ws.onSend({ + text, + type: 'text', + }); }, }; diff --git a/frontend/chat-plugin/src/components/websocket/index.ts b/frontend/chat-plugin/src/components/websocket/index.ts index a3e73a0cf4..1c3a6c53f9 100644 --- a/frontend/chat-plugin/src/components/websocket/index.ts +++ b/frontend/chat-plugin/src/components/websocket/index.ts @@ -1,6 +1,7 @@ import {Client, messageCallbackType, IFrame} from '@stomp/stompjs'; import 'regenerator-runtime/runtime'; import {start, getResumeToken, sendMessage} from '../api'; +import {Text} from 'types'; declare const window: { airy: { @@ -52,9 +53,7 @@ class WebSocket { this.client.activate(); }; - onSend = (message: string) => { - sendMessage(message, this.token); - }; + onSend = (message: Text) => sendMessage(message, this.token); start = async () => { this.token = (await start(this.channel_id, this.resume_token)).token; diff --git a/frontend/demo/BUILD b/frontend/demo/BUILD index 870ee81976..a3051e3755 100644 --- a/frontend/demo/BUILD +++ b/frontend/demo/BUILD @@ -19,6 +19,7 @@ ts_library( "@npm//@types/node", "@npm//@types/prop-types", "@npm//@types/react", + "@npm//@types/react-dom", "@npm//@types/react-redux", "@npm//emoji-mart", "@npm//lodash-es", diff --git a/lib/java/mapping/src/main/java/co/airy/mapping/OutboundMapper.java b/lib/java/mapping/src/main/java/co/airy/mapping/OutboundMapper.java index ac3eb07efd..a0b16dcf21 100644 --- a/lib/java/mapping/src/main/java/co/airy/mapping/OutboundMapper.java +++ b/lib/java/mapping/src/main/java/co/airy/mapping/OutboundMapper.java @@ -18,7 +18,7 @@ public OutboundMapper() { } public List render(String payload) throws Exception { - final JsonNode jsonNode = objectMapper.readTree(payload); - return List.of(new Text(jsonNode.get("text").textValue())); + final Content content = objectMapper.readValue(payload, Content.class); + return List.of(content); } } diff --git a/lib/java/mapping/src/test/java/co/airy/mapping/ContentMapperTest.java b/lib/java/mapping/src/test/java/co/airy/mapping/ContentMapperTest.java index 839a9c02f1..57cb265b93 100644 --- a/lib/java/mapping/src/test/java/co/airy/mapping/ContentMapperTest.java +++ b/lib/java/mapping/src/test/java/co/airy/mapping/ContentMapperTest.java @@ -35,7 +35,9 @@ public class ContentMapperTest { @Test void rendersOutbound() throws Exception { - final String text = "Hello World"; + final String textContent = "Hello World"; + final Text text = new Text(textContent); + final Message message = Message.newBuilder() .setId("other-message-id") .setSource("facebook") @@ -45,12 +47,12 @@ void rendersOutbound() throws Exception { .setDeliveryState(DeliveryState.DELIVERED) .setConversationId("conversationId") .setChannelId("channelId") - .setContent("{\"text\":\"" + text + "\"}") + .setContent((new ObjectMapper()).writeValueAsString(text)) .build(); final Text textMessage = (Text) mapper.render(message).get(0); - assertThat(textMessage.getText(), equalTo(text)); + assertThat(textMessage.getText(), equalTo(textContent)); Mockito.verify(outboundMapper).render(Mockito.anyString()); } diff --git a/lib/typescript/types/BUILD b/lib/typescript/types/BUILD index 7fed076ef6..67865228e8 100644 --- a/lib/typescript/types/BUILD +++ b/lib/typescript/types/BUILD @@ -5,6 +5,10 @@ package(default_visibility = ["//visibility:public"]) ts_library( name = "types", srcs = glob([ - "**/*.d.ts", + "**/*.ts", ]), + deps = [ + "@npm//@types/react", + "@npm//@types/react-dom", + ], ) diff --git a/lib/typescript/types/global.d.ts b/lib/typescript/types/global.d.ts index dfacfac5de..b99178d4e2 100644 --- a/lib/typescript/types/global.d.ts +++ b/lib/typescript/types/global.d.ts @@ -1,13 +1,6 @@ /// /// -interface CustomNodeModule extends NodeModule { - hot: any; -} - -// Hot Module Replacement -declare let module: CustomNodeModule; - declare module '*.gif' { const src: string; export default src; diff --git a/lib/typescript/types/index.ts b/lib/typescript/types/index.ts new file mode 100644 index 0000000000..7b367d1463 --- /dev/null +++ b/lib/typescript/types/index.ts @@ -0,0 +1 @@ +export * from './content'; diff --git a/tsconfig.json b/tsconfig.json index f9c645c902..77d800bb15 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,18 @@ "components/*": [ "./frontend/components/src/*" ], + "types": [ + "./lib/typescript/types" + ], + "types/*": [ + "./lib/typescript/types/*" + ], + "httpclient": [ + "./lib/typescript/httpclient" + ], + "httpclient/*": [ + "./lib/typescript/httpclient/*" + ], "*": [ "./*" ] From 35bedfffe8a15d5d8bbbbe59d5cafbe34b89b559 Mon Sep 17 00:00:00 2001 From: Paulo Diniz Date: Thu, 21 Jan 2021 09:39:10 +0100 Subject: [PATCH 32/56] [#620] Apply configuration based on config file (#655) --- infrastructure/cli/cmd/config/BUILD | 16 +- infrastructure/cli/cmd/config/config.go | 51 +++- infrastructure/cli/cmd/config/configmaps.go | 104 +++++++ infrastructure/cli/cmd/config/parser.go | 62 ++++ infrastructure/cli/cmd/root.go | 27 +- infrastructure/cli/go.mod | 8 + infrastructure/cli/go.sum | 271 +++++++++++++++++- infrastructure/cli/main_test.go | 4 +- .../tests/golden/{cli.yaml => airycli.yaml} | 0 .../cli/pkg/tests/golden/cli.no-args.golden | 6 +- 10 files changed, 512 insertions(+), 37 deletions(-) create mode 100644 infrastructure/cli/cmd/config/configmaps.go create mode 100644 infrastructure/cli/cmd/config/parser.go rename infrastructure/cli/pkg/tests/golden/{cli.yaml => airycli.yaml} (100%) diff --git a/infrastructure/cli/cmd/config/BUILD b/infrastructure/cli/cmd/config/BUILD index caccc11385..8c79bdd734 100644 --- a/infrastructure/cli/cmd/config/BUILD +++ b/infrastructure/cli/cmd/config/BUILD @@ -2,8 +2,20 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "config", - srcs = ["config.go"], + srcs = [ + "config.go", + "configmaps.go", + "parser.go", + ], importpath = "cli/cmd/config", visibility = ["//visibility:public"], - deps = ["@com_github_spf13_cobra//:cobra"], + deps = [ + "@com_github_mitchellh_go_homedir//:go-homedir", + "@com_github_spf13_cobra//:cobra", + "@in_gopkg_yaml_v2//:yaml_v2", + "@io_k8s_api//core/v1:core", + "@io_k8s_apimachinery//pkg/apis/meta/v1:meta", + "@io_k8s_client_go//kubernetes", + "@io_k8s_client_go//tools/clientcmd", + ], ) diff --git a/infrastructure/cli/cmd/config/config.go b/infrastructure/cli/cmd/config/config.go index bfed789265..c65ae4ed19 100644 --- a/infrastructure/cli/cmd/config/config.go +++ b/infrastructure/cli/cmd/config/config.go @@ -2,20 +2,67 @@ package config import ( "fmt" + "os" + "path" + "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" ) +var kubeConfigFile string +var configFile string + // ConfigCmd subcommand for Airy Core var ConfigCmd = &cobra.Command{ Use: "config", TraverseChildren: true, Short: "Reloads configuration based on airy.yaml", - Long: ``, + Long: `Reloads configuration based on airy.yaml`, Run: config, } func config(cmd *cobra.Command, args []string) { - fmt.Println("ConfigCmd called") + fmt.Println("config called") +} + +func applyConfig(cmd *cobra.Command, args []string) { + conf, err := parseConf(configFile) + if err != nil { + fmt.Println("error parsing configuration file: ", err) + os.Exit(1) + } + + if twilioApply(conf, kubeConfigFile) { + fmt.Println("Twilio configuration applied.") + } + + if facebookApply(conf, kubeConfigFile) { + fmt.Println("Facebook configuration applied.") + } + + if googleApply(conf, kubeConfigFile) { + fmt.Println("Google configuration applied.") + } +} + +var applyConfigCmd = &cobra.Command{ + Use: "apply", + TraverseChildren: true, + Short: "Applies configuration based on airy.yaml", + Long: `Applies configuration based on airy.yaml`, + Run: applyConfig, +} +func init() { + ConfigCmd.PersistentFlags().StringVar(&kubeConfigFile, "kube-config", "", "Kubernetes config file for the cluster where Airy is running (default \"~/.airy/kube.conf\")") + ConfigCmd.PersistentFlags().StringVar(&configFile, "config", "../airy.yaml", "Configuration file for the Airy platform") + if kubeConfigFile == "" { + home, err := homedir.Dir() + if err != nil { + fmt.Println(err) + os.Exit(1) + } + kubeConfigFile = path.Join(home, ".airy/kube.conf") + } + ConfigCmd.AddCommand(applyConfigCmd) } diff --git a/infrastructure/cli/cmd/config/configmaps.go b/infrastructure/cli/cmd/config/configmaps.go new file mode 100644 index 0000000000..65cc8ebcb1 --- /dev/null +++ b/infrastructure/cli/cmd/config/configmaps.go @@ -0,0 +1,104 @@ +package config + +import ( + "context" + "fmt" + "os" + + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +func applyConfigMap(source string, newCmData map[string]string, kubeConfigFile string, namespace string) error { + configMapName := "sources-" + source + config, kubeConfigErr := clientcmd.BuildConfigFromFlags("", kubeConfigFile) + if kubeConfigErr != nil { + return kubeConfigErr + } + + clientset, clientsetErr := kubernetes.NewForConfig(config) + if clientsetErr != nil { + return clientsetErr + } + + cm, _ := clientset.CoreV1().ConfigMaps(namespace).Get(context.TODO(), configMapName, v1.GetOptions{}) + + if cm.GetName() == "" { + _, err := clientset.CoreV1().ConfigMaps(namespace).Create(context.TODO(), + &corev1.ConfigMap{ + ObjectMeta: v1.ObjectMeta{ + Name: configMapName, + Namespace: namespace, + }, + Data: newCmData, + }, v1.CreateOptions{}) + return err + } else { + cm.Data = newCmData + _, err := clientset.CoreV1().ConfigMaps(namespace).Update(context.TODO(), cm, v1.UpdateOptions{}) + return err + } + +} + +func facebookApply(airyConf airyConf, kubeConfigFile string) bool { + facebookConfig := airyConf.Core.Apps.Sources.Facebook + if facebookConfig.AppID != "" || facebookConfig.AppSecret != "" || facebookConfig.WebhookSecret != "" { + configMapData := make(map[string]string, 0) + configMapData["FACEBOOK_APP_ID"] = facebookConfig.AppID + configMapData["FACEBOOK_APP_SECRET"] = facebookConfig.AppSecret + configMapData["FACEBOOK_WEBHOOK_SECRET"] = facebookConfig.WebhookSecret + err := applyConfigMap("facebook", configMapData, kubeConfigFile, airyConf.Global.Namespace) + + if err != nil { + fmt.Println("unable to update configMap: ", err) + os.Exit(1) + } + + return true + } + + return false +} + +func googleApply(airyConf airyConf, kubeConfigFile string) bool { + googleConfig := airyConf.Core.Apps.Sources.Google + if googleConfig.PartnerKey != "" || googleConfig.SaFile != "" { + configMapData := make(map[string]string, 0) + configMapData["GOOGLE_PARTNER_KEY"] = googleConfig.PartnerKey + configMapData["GOOGLE_SA_FILE"] = googleConfig.SaFile + + err := applyConfigMap("google", configMapData, kubeConfigFile, airyConf.Global.Namespace) + + if err != nil { + fmt.Println("unable to update configMap: ", err) + os.Exit(1) + } + + return true + } + + return false +} + +func twilioApply(airyConf airyConf, kubeConfigFile string) bool { + twilioConfig := airyConf.Core.Apps.Sources.Twilio + if twilioConfig.AccountSid != "" || twilioConfig.AuthToken != "" { + configMapData := make(map[string]string, 0) + configMapData["TWILIO_ACCOUNT_SID"] = twilioConfig.AccountSid + configMapData["TWILIO_AUTH_TOKEN"] = twilioConfig.AuthToken + + err := applyConfigMap("twilio", configMapData, kubeConfigFile, airyConf.Global.Namespace) + + if err != nil { + fmt.Println("unable to update configMap: ", err) + os.Exit(1) + } + + return true + } + + return false +} diff --git a/infrastructure/cli/cmd/config/parser.go b/infrastructure/cli/cmd/config/parser.go new file mode 100644 index 0000000000..1879f4a573 --- /dev/null +++ b/infrastructure/cli/cmd/config/parser.go @@ -0,0 +1,62 @@ +package config + +import ( + "fmt" + "io/ioutil" + "os" + + "gopkg.in/yaml.v2" +) + +type globalConf struct { + AppImageTag string `yaml:"appImageTag"` + ContainerRegistry string `yaml:"containerRegistry"` + Namespace string `yaml:"namespace"` +} + +type coreConf struct { + Apps struct { + Sources struct { + Twilio struct { + AuthToken string `yaml:"authToken"` + AccountSid string `yaml:"accountSid"` + } + Facebook struct { + AppID string `yaml:"appId"` + AppSecret string `yaml:"appSecret"` + WebhookSecret string `yaml:"webhookSecret"` + } + Google struct { + PartnerKey string `yaml:"partnerKey"` + SaFile string `yaml:"saFile"` + } + } + } +} + +type airyConf struct { + Global globalConf + Core coreConf +} + +func parseConf(configFile string) (airyConf, error) { + data, err := ioutil.ReadFile(configFile) + if err != nil { + fmt.Println("error reading configuration file: ", err) + os.Exit(1) + } + + conf := airyConf{ + Global: globalConf{ + Namespace: "default", + }, + } + + err = yaml.Unmarshal(data, &conf) + if err != nil { + fmt.Println("error parsing configuration file: ", err) + os.Exit(1) + } + + return conf, nil +} diff --git a/infrastructure/cli/cmd/root.go b/infrastructure/cli/cmd/root.go index aa28653931..777763d2bb 100644 --- a/infrastructure/cli/cmd/root.go +++ b/infrastructure/cli/cmd/root.go @@ -15,14 +15,13 @@ import ( "github.com/spf13/viper" ) -const configFileName = "cli.yaml" -const configDirName = ".airy" +const cliConfigFileName = "cli.yaml" +const cliConfigDirName = ".airy" -var configFile string +var cliConfigFile string var Version string var CommitSHA1 string -// RootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "airy", Short: "airy controls your Airy Core Platform instance", @@ -35,7 +34,6 @@ var rootCmd = &cobra.Command{ }, } -// Version command var versionCmd = &cobra.Command{ Use: "version", Short: "Prints version information", @@ -45,7 +43,6 @@ var versionCmd = &cobra.Command{ }, } -// Version command var initCmd = &cobra.Command{ Use: "init", Short: "Inits your Airy CLI configuration", @@ -57,7 +54,7 @@ var initCmd = &cobra.Command{ os.Exit(1) } - configDirPath := path.Join(home, configDirName) + configDirPath := path.Join(home, cliConfigDirName) if _, errConfigDir := os.Stat(configDirPath); os.IsNotExist(errConfigDir) { errDir := os.MkdirAll(configDirPath, 0700) @@ -67,16 +64,13 @@ var initCmd = &cobra.Command{ } } - err = viper.WriteConfigAs(path.Join(home, configDirName, configFileName)) + err = viper.WriteConfigAs(path.Join(home, cliConfigDirName, cliConfigFileName)) if err != nil { fmt.Println("cannot write config: ", err) } }, } -// Execute adds all child commands to the root command and sets flags -// appropriately. This is called by main.main(). It only needs to happen once to -// the rootCmd. func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Println(err) @@ -85,8 +79,8 @@ func Execute() { } func initConfig() { - if configFile != "" { - viper.SetConfigFile(configFile) + if cliConfigFile != "" { + viper.SetConfigFile(cliConfigFile) } else { home, err := homedir.Dir() if err != nil { @@ -94,9 +88,9 @@ func initConfig() { os.Exit(1) } - viper.AddConfigPath(path.Join(home, configDirName)) + viper.AddConfigPath(path.Join(home, cliConfigDirName)) viper.SetConfigType("yaml") - viper.SetConfigName(configFileName) + viper.SetConfigName(cliConfigFileName) } if err := viper.ReadInConfig(); err != nil { @@ -121,8 +115,7 @@ func init() { rootCmd.PersistentFlags().StringVarP(&apiJWTToken, "apiJWTToken", "", "", "apiJWTToken") rootCmd.PersistentFlags().MarkHidden("apiJWTToken") viper.BindPFlag("apiJWTToken", rootCmd.PersistentFlags().Lookup("apiJWTToken")) - - rootCmd.PersistentFlags().StringVar(&configFile, "config", "", "config file (default is $HOME/.airy/cli.yaml)") + rootCmd.PersistentFlags().StringVar(&cliConfigFile, "cli-config", "", "config file (default is $HOME/.airy/cli.yaml)") rootCmd.AddCommand(api.APICmd) rootCmd.AddCommand(config.ConfigCmd) rootCmd.AddCommand(status.StatusCmd) diff --git a/infrastructure/cli/go.mod b/infrastructure/cli/go.mod index 0880d133be..3fb491520e 100644 --- a/infrastructure/cli/go.mod +++ b/infrastructure/cli/go.mod @@ -8,7 +8,15 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/spf13/cobra v1.1.1 github.com/spf13/viper v1.7.1 + github.com/stretchr/testify v1.6.1 + github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 // indirect + github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77 // indirect goji.io v2.0.2+incompatible + gopkg.in/yaml.v2 v2.4.0 + k8s.io/api v0.20.0 + k8s.io/apimachinery v0.20.0 + k8s.io/client-go v0.20.0 + ) replace github.com/airyhq/airy/lib/go/httpclient => ../../lib/go/httpclient diff --git a/infrastructure/cli/go.sum b/infrastructure/cli/go.sum index 230424ee3b..0a4a4f41b9 100644 --- a/infrastructure/cli/go.sum +++ b/infrastructure/cli/go.sum @@ -5,29 +5,57 @@ cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6A cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= +github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= @@ -37,38 +65,92 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= @@ -93,26 +175,37 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -127,15 +220,28 @@ github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0Qu github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -143,6 +249,7 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -161,15 +268,16 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -178,18 +286,23 @@ github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= @@ -200,11 +313,21 @@ golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -214,12 +337,19 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -231,18 +361,35 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 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/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -252,17 +399,38 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd h1:5CtCZbICpIOFdgO940moixOPjc0178IU44m4EjOO5IY= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -271,23 +439,49 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 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= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -297,27 +491,82 @@ google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.20.0 h1:WwrYoZNM1W1aQEbyl8HNG+oWGzLpZQBlcerS9BQw9yI= +k8s.io/api v0.20.0/go.mod h1:HyLC5l5eoS/ygQYl1BXBgFzWNlkHiAuyNAbevIn+FKg= +k8s.io/apimachinery v0.20.0 h1:jjzbTJRXk0unNS71L7h3lxGDH/2HPxMPaQY+MjECKL8= +k8s.io/apimachinery v0.20.0/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apimachinery v0.20.2 h1:hFx6Sbt1oG0n6DZ+g4bFt5f6BoMkOjKWsQFu077M3Vg= +k8s.io/client-go v0.20.0 h1:Xlax8PKbZsjX4gFvNtt4F5MoJ1V5prDvCuoq9B7iax0= +k8s.io/client-go v0.20.0/go.mod h1:4KWh/g+Ocd8KkCwKF8vUNnmqgv+EVnQDK4MBF4oB5tY= +k8s.io/client-go v1.5.1 h1:XaX/lo2/u3/pmFau8HN+sB5C/b4dc4Dmm2eXjBH4p1E= +k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwnJo9o= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ= +k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920 h1:CbnUZsM497iRC5QMVkHwyl8s2tB3g7yaSHkYPkpgelw= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2 h1:YHQV7Dajm86OuqnIR6zAelnDWBRjo+YhYV9PmGrh1s8= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/infrastructure/cli/main_test.go b/infrastructure/cli/main_test.go index 59f6564e33..48acae1fd4 100644 --- a/infrastructure/cli/main_test.go +++ b/infrastructure/cli/main_test.go @@ -18,8 +18,8 @@ func TestCli(t *testing.T) { wantErr bool }{ {"no args", []string{}, "cli.no-args", false}, - {"login", []string{"api", "login", "--config", "pkg/tests/golden/cli.yaml"}, "cli.login", false}, - {"version", []string{"version", "--config", "pkg/tests/golden/cli.yaml"}, "cli.version", false}, + {"login", []string{"api", "login", "--cli-config", "pkg/tests/golden/airycli.yaml"}, "cli.login", false}, + {"version", []string{"version", "--cli-config", "pkg/tests/golden/airycli.yaml"}, "cli.version", false}, } go func() { diff --git a/infrastructure/cli/pkg/tests/golden/cli.yaml b/infrastructure/cli/pkg/tests/golden/airycli.yaml similarity index 100% rename from infrastructure/cli/pkg/tests/golden/cli.yaml rename to infrastructure/cli/pkg/tests/golden/airycli.yaml diff --git a/infrastructure/cli/pkg/tests/golden/cli.no-args.golden b/infrastructure/cli/pkg/tests/golden/cli.no-args.golden index 73148849a8..4f514a3b08 100644 --- a/infrastructure/cli/pkg/tests/golden/cli.no-args.golden +++ b/infrastructure/cli/pkg/tests/golden/cli.no-args.golden @@ -13,8 +13,8 @@ Available Commands: version Prints version information Flags: - --apihost string Airy Core Platform HTTP API host (default "http://api.airy") - --config string config file (default is $HOME/.airy/cli.yaml) - -h, --help help for airy + --apihost string Airy Core Platform HTTP API host (default "http://api.airy") + --cli-config string config file (default is $HOME/.airy/cli.yaml) + -h, --help help for airy Use "airy [command] --help" for more information about a command. From 2914015826e7d572d80f5441bd3f324504e469d4 Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Thu, 21 Jan 2021 11:10:03 +0100 Subject: [PATCH 33/56] [#693] Simplify display name contact API response (#696) --- .../java/co/airy/core/api/communication/Mapper.java | 3 +-- .../api/communication/lucene/DocumentMapper.java | 2 -- .../payload/ContactResponsePayload.java | 3 +-- docs/docs/api/http.md | 13 +++---------- .../demo/src/reducers/data/conversations/index.ts | 3 --- .../httpclient/mappers/conversationMapper.ts | 4 +--- lib/typescript/httpclient/model/Contact.ts | 2 -- .../httpclient/payload/ConversationPayload.ts | 3 +-- 8 files changed, 7 insertions(+), 26 deletions(-) diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/Mapper.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/Mapper.java index a178e4e3b9..705c9c6184 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/Mapper.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/Mapper.java @@ -56,8 +56,7 @@ private ContactResponsePayload getContact(Conversation conversation) { return ContactResponsePayload.builder() .avatarUrl(metadata.get(MetadataKeys.Source.Contact.AVATAR_URL)) - .firstName(displayName.getFirstName()) - .lastName(displayName.getLastName()) + .displayName(displayName.toString()) .info(getConversationInfo(metadata)) .build(); } diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/DocumentMapper.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/DocumentMapper.java index c0a0b057ec..49309457a0 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/DocumentMapper.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/DocumentMapper.java @@ -16,8 +16,6 @@ import static java.util.stream.Collectors.toMap; public class DocumentMapper { - final ObjectMapper objectMapper = new ObjectMapper(); - public Document fromConversationIndex(ConversationIndex conversation) { final Document document = new Document(); document.add(new StringField("id", conversation.getId(), Field.Store.YES)); diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/payload/ContactResponsePayload.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/payload/ContactResponsePayload.java index 25f6ed99a8..93b4d37a67 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/payload/ContactResponsePayload.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/payload/ContactResponsePayload.java @@ -13,8 +13,7 @@ @Data public class ContactResponsePayload { private String avatarUrl; - private String firstName; - private String lastName; + private String displayName; private Map info; } diff --git a/docs/docs/api/http.md b/docs/docs/api/http.md index bf5a2260e8..a451960c20 100644 --- a/docs/docs/api/http.md +++ b/docs/docs/api/http.md @@ -144,9 +144,7 @@ Find users whose name ends with "Lovelace": "contact": { // Additional data on the contact "avatar_url": "https://assets.airy.co/AirySupportIcon.jpg", - "first_name": "Airy Support", - "last_name": null, - "id": "36d07b7b-e242-4612-a82c-76832cfd1026" + "display_name": "Airy Support" }, "tags": ["f339c325-8614-43cb-a70a-e83d81bf56fc"], "last_message": { @@ -202,13 +200,8 @@ Find users whose name ends with "Lovelace": }, "created_at": "2019-01-07T09:01:44.000Z", "contact": { - "avatar_url": "https://assets.airy.co/AirySupportIcon.jpg", - // optional - "first_name": "Airy Support", - // optional - "last_name": null, - // optional - "id": "36d07b7b-e242-4612-a82c-76832cfd1026" + "avatar_url": "https://assets.airy.co/AirySupportIcon.jpg", // optional + "display_name": "Airy Support" // optional }, "tags": ["f339c325-8614-43cb-a70a-e83d81bf56fc"], "last_message": { diff --git a/frontend/demo/src/reducers/data/conversations/index.ts b/frontend/demo/src/reducers/data/conversations/index.ts index 90d9d39184..81a2406e33 100644 --- a/frontend/demo/src/reducers/data/conversations/index.ts +++ b/frontend/demo/src/reducers/data/conversations/index.ts @@ -44,9 +44,6 @@ function mergeConversations( newConversations: MergedConversation[] ): ConversationMap { newConversations.forEach((conversation: MergedConversation) => { - if (conversation.contact && !conversation.contact.displayName) { - conversation.contact.displayName = `${conversation.contact.firstName} ${conversation.contact.lastName}`; - } if (conversation.lastMessage) { conversation.lastMessage.sentAt = new Date(conversation.lastMessage.sentAt); } diff --git a/lib/typescript/httpclient/mappers/conversationMapper.ts b/lib/typescript/httpclient/mappers/conversationMapper.ts index b875149b50..066e4417dc 100644 --- a/lib/typescript/httpclient/mappers/conversationMapper.ts +++ b/lib/typescript/httpclient/mappers/conversationMapper.ts @@ -8,9 +8,7 @@ export const conversationMapper = (payload: ConversationPayload): Conversation = createdAt: payload.created_at, contact: { avatarUrl: payload.contact.avatar_url, - firstName: payload.contact.first_name, - lastName: payload.contact.last_name, - displayName: payload.contact.first_name + ' ' + payload.contact.last_name, + displayName: payload.contact.display_name, id: payload.contact.id, }, tags: payload.tags, diff --git a/lib/typescript/httpclient/model/Contact.ts b/lib/typescript/httpclient/model/Contact.ts index 1ceccbff1c..827cac6189 100644 --- a/lib/typescript/httpclient/model/Contact.ts +++ b/lib/typescript/httpclient/model/Contact.ts @@ -2,8 +2,6 @@ import {Tag} from './Tag'; export interface Contact { id: string; - firstName: string; - lastName: string; displayName: string; avatarUrl: string; tags?: Tag[]; diff --git a/lib/typescript/httpclient/payload/ConversationPayload.ts b/lib/typescript/httpclient/payload/ConversationPayload.ts index d7bd809c40..7f28245b6c 100644 --- a/lib/typescript/httpclient/payload/ConversationPayload.ts +++ b/lib/typescript/httpclient/payload/ConversationPayload.ts @@ -7,8 +7,7 @@ export interface ConversationPayload { created_at: string; contact: { avatar_url: string; - first_name: string; - last_name: string; + display_name: string; id: string; }; tags: string[]; From a6286e2015b42fa268b46444d75cd30abaa35855 Mon Sep 17 00:00:00 2001 From: Pascal Holy <54705263+pascal-airy@users.noreply.github.com> Date: Thu, 21 Jan 2021 11:12:40 +0100 Subject: [PATCH 34/56] [#695] Fix yq version incompatibility (#697) fixes #695 --- infrastructure/scripts/provision/core.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infrastructure/scripts/provision/core.sh b/infrastructure/scripts/provision/core.sh index 159e2d9bba..afcb7fad6d 100755 --- a/infrastructure/scripts/provision/core.sh +++ b/infrastructure/scripts/provision/core.sh @@ -15,7 +15,7 @@ wait-for-service-account echo "Deploying the Airy Core Platform with the ${AIRY_VERSION} image tag" if [[ -f ${INFRASTRUCTURE_PATH}/airy.yaml ]]; then - yq w -i ${INFRASTRUCTURE_PATH}/airy.yaml global.appImageTag ${AIRY_VERSION} + yq eval '.global.appImageTag="'${AIRY_VERSION}'"' -i ${INFRASTRUCTURE_PATH}/airy.yaml fi helm install core ${INFRASTRUCTURE_PATH}/helm-chart/ --set global.appImageTag=${AIRY_VERSION} --version 0.5.0 --timeout 1000s > /dev/null 2>&1 From 54e4fc92e635d3968aeae63904ca193838fbcdda Mon Sep 17 00:00:00 2001 From: lucapette Date: Thu, 21 Jan 2021 13:33:29 +0100 Subject: [PATCH 35/56] [#679] Reorganize integration tests (#694) * Move tests * Prepare the code for more scalable integration tests * Fix tests after rebase --- infrastructure/cli/BUILD | 13 +---- infrastructure/cli/integration/BUILD | 30 +++++++++++ .../cli/integration/api_login_test.go | 47 ++++++++++++++++ .../{pkg/tests => integration}/golden/BUILD | 0 .../golden/cli.login.golden | 0 .../golden/cli.no-args.golden | 0 .../golden/cli.version.golden | 0 .../golden/cli.yaml} | 2 +- .../golden/users.signup.golden} | 0 infrastructure/cli/integration/mockserver.go | 54 +++++++++++++++++++ infrastructure/cli/integration/noargs_test.go | 49 +++++++++++++++++ .../golden.go => integration/test_file.go} | 4 +- .../cli/integration/version_test.go | 40 ++++++++++++++ infrastructure/cli/main_test.go | 47 ---------------- infrastructure/cli/pkg/tests/BUILD | 17 ------ infrastructure/cli/pkg/tests/mockserver.go | 52 ------------------ lib/go/httpclient/BUILD | 12 ++++- lib/go/httpclient/http-client-test/BUILD | 11 ---- .../{http-client-test => }/users_test.go | 32 +++++------ 19 files changed, 249 insertions(+), 161 deletions(-) create mode 100644 infrastructure/cli/integration/BUILD create mode 100644 infrastructure/cli/integration/api_login_test.go rename infrastructure/cli/{pkg/tests => integration}/golden/BUILD (100%) rename infrastructure/cli/{pkg/tests => integration}/golden/cli.login.golden (100%) rename infrastructure/cli/{pkg/tests => integration}/golden/cli.no-args.golden (100%) rename infrastructure/cli/{pkg/tests => integration}/golden/cli.version.golden (100%) rename infrastructure/cli/{pkg/tests/golden/airycli.yaml => integration/golden/cli.yaml} (90%) rename infrastructure/cli/{pkg/tests/golden/api.signup.golden => integration/golden/users.signup.golden} (100%) create mode 100644 infrastructure/cli/integration/mockserver.go create mode 100644 infrastructure/cli/integration/noargs_test.go rename infrastructure/cli/{pkg/tests/golden.go => integration/test_file.go} (88%) create mode 100644 infrastructure/cli/integration/version_test.go delete mode 100644 infrastructure/cli/main_test.go delete mode 100644 infrastructure/cli/pkg/tests/BUILD delete mode 100644 infrastructure/cli/pkg/tests/mockserver.go delete mode 100644 lib/go/httpclient/http-client-test/BUILD rename lib/go/httpclient/{http-client-test => }/users_test.go (55%) diff --git a/infrastructure/cli/BUILD b/infrastructure/cli/BUILD index 0aa478f382..7d1cb16920 100644 --- a/infrastructure/cli/BUILD +++ b/infrastructure/cli/BUILD @@ -1,5 +1,5 @@ # gazelle:prefix cli -load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") go_library( name = "cli_lib", @@ -34,17 +34,6 @@ os_list = [ for os in os_list ] -go_test( - name = "cli_test", - srcs = ["main_test.go"], - data = [ - "airy", - "//infrastructure/cli/pkg/tests/golden:golden_files", - ], - embed = [":cli_lib"], - deps = ["//infrastructure/cli/pkg/tests"], -) - [ genrule( name = "airy_" + os + "_bin_rule", diff --git a/infrastructure/cli/integration/BUILD b/infrastructure/cli/integration/BUILD new file mode 100644 index 0000000000..088ab09723 --- /dev/null +++ b/infrastructure/cli/integration/BUILD @@ -0,0 +1,30 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "integration", + srcs = [ + "mockserver.go", + "test_file.go", + ], + importpath = "cli/integration", + visibility = ["//visibility:public"], + deps = [ + "@com_github_kr_pretty//:pretty", + "@io_goji//:goji.io", + "@io_goji//pat", + ], +) + +go_test( + name = "integration_test", + srcs = [ + "api_login_test.go", + "noargs_test.go", + "version_test.go", + ], + data = [ + "//infrastructure/cli:airy", + "//infrastructure/cli/integration/golden:golden_files", + ], + embed = [":integration"], +) diff --git a/infrastructure/cli/integration/api_login_test.go b/infrastructure/cli/integration/api_login_test.go new file mode 100644 index 0000000000..a07e99231b --- /dev/null +++ b/infrastructure/cli/integration/api_login_test.go @@ -0,0 +1,47 @@ +package integration + +import ( + "os/exec" + "testing" + + "reflect" +) + +func TestApiLogin(t *testing.T) { + tests := []struct { + name string + args []string + golden string + wantErr bool + }{ + {"login", []string{"api", "login", "--cli-config", "golden/cli.yaml"}, "cli.login", false}, + } + ms := NewMockServer(t) + + go func() { + ms.Serve() + }() + + for _, tt := range tests { + t.Run(tt.name, func(testing *testing.T) { + cmd := exec.Command(binaryName, append(tt.args, "--apihost", ms.Host)...) + output, err := cmd.CombinedOutput() + actual := string(output) + if (err != nil) != tt.wantErr { + if tt.wantErr { + t.Fatalf("Test %s expected to fail but did not. Error message: %v Output: %s\n", tt.name, err, actual) + } else { + t.Fatalf("Test %s expected to pass but did not. Error message: %v Output: %s\n", tt.name, err, actual) + } + } + golden := NewGoldenFile(t, tt.golden) + expected := golden.Load() + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("diff: %v", Diff(actual, expected)) + } + + }) + + } +} diff --git a/infrastructure/cli/pkg/tests/golden/BUILD b/infrastructure/cli/integration/golden/BUILD similarity index 100% rename from infrastructure/cli/pkg/tests/golden/BUILD rename to infrastructure/cli/integration/golden/BUILD diff --git a/infrastructure/cli/pkg/tests/golden/cli.login.golden b/infrastructure/cli/integration/golden/cli.login.golden similarity index 100% rename from infrastructure/cli/pkg/tests/golden/cli.login.golden rename to infrastructure/cli/integration/golden/cli.login.golden diff --git a/infrastructure/cli/pkg/tests/golden/cli.no-args.golden b/infrastructure/cli/integration/golden/cli.no-args.golden similarity index 100% rename from infrastructure/cli/pkg/tests/golden/cli.no-args.golden rename to infrastructure/cli/integration/golden/cli.no-args.golden diff --git a/infrastructure/cli/pkg/tests/golden/cli.version.golden b/infrastructure/cli/integration/golden/cli.version.golden similarity index 100% rename from infrastructure/cli/pkg/tests/golden/cli.version.golden rename to infrastructure/cli/integration/golden/cli.version.golden diff --git a/infrastructure/cli/pkg/tests/golden/airycli.yaml b/infrastructure/cli/integration/golden/cli.yaml similarity index 90% rename from infrastructure/cli/pkg/tests/golden/airycli.yaml rename to infrastructure/cli/integration/golden/cli.yaml index cb422e6be7..1fec8f3a3e 100644 --- a/infrastructure/cli/pkg/tests/golden/airycli.yaml +++ b/infrastructure/cli/integration/golden/cli.yaml @@ -1,2 +1,2 @@ -apihost: http://localhost:3001 +apihost: http://localhost:50926 apijwttoken: eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDM1MmU3NC00MjEyLTQyODItYWM3NC0wZjU4ZjgzYzJkYjUiLCJzdWIiOiIxMDM1MmU3NC00MjEyLTQyODItYWM3NC0wZjU4ZjgzYzJkYjUiLCJpYXQiOjE2MDc2MTk0ODksInVzZXJfaWQiOiIxMDM1MmU3NC00MjEyLTQyODItYWM3NC0wZjU4ZjgzYzJkYjUiLCJleHAiOjE2MDc3MDU4ODl9.ZuIv_t0D358n04gamNwz3U_tkxr4IO36gXuZyU9X3e4 diff --git a/infrastructure/cli/pkg/tests/golden/api.signup.golden b/infrastructure/cli/integration/golden/users.signup.golden similarity index 100% rename from infrastructure/cli/pkg/tests/golden/api.signup.golden rename to infrastructure/cli/integration/golden/users.signup.golden diff --git a/infrastructure/cli/integration/mockserver.go b/infrastructure/cli/integration/mockserver.go new file mode 100644 index 0000000000..109685e38d --- /dev/null +++ b/infrastructure/cli/integration/mockserver.go @@ -0,0 +1,54 @@ +package integration + +import ( + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "path" + "testing" + + "goji.io" + "goji.io/pat" +) + +type MockServer struct { + l net.Listener + Host string + mux *goji.Mux +} + +func NewMockServer(t *testing.T) *MockServer { + listener, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatal("mock server error: ", err) + } + + mux := goji.NewMux() + mux.HandleFunc(pat.Post("/users.signup"), mockEndpoint("users.signup")) + mux.HandleFunc(pat.Post("/users.login"), mockEndpoint("users.signup")) + + return &MockServer{ + l: listener, + mux: mux, + Host: fmt.Sprintf("http://localhost:%d", listener.Addr().(*net.TCPAddr).Port), + } +} + +func (ms *MockServer) Serve() { + http.Serve(ms.l, ms.mux) +} + +func mockEndpoint(endpoint string) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + data, err := ioutil.ReadFile(path.Join("golden", endpoint+".golden")) + if err != nil { + fmt.Fprint(w, err) + } + _, err = w.Write(data) + if err != nil { + log.Println(err) + } + } +} diff --git a/infrastructure/cli/integration/noargs_test.go b/infrastructure/cli/integration/noargs_test.go new file mode 100644 index 0000000000..2f92e547c0 --- /dev/null +++ b/infrastructure/cli/integration/noargs_test.go @@ -0,0 +1,49 @@ +package integration + +import ( + "os/exec" + "testing" + + "reflect" +) + +const binaryName = "../airy" + +func TestNoArgs(t *testing.T) { + tests := []struct { + name string + args []string + golden string + wantErr bool + }{ + {"no args", []string{}, "cli.no-args", false}, + } + + go func() { + ms := NewMockServer(t) + ms.Serve() + }() + + for _, tt := range tests { + t.Run(tt.name, func(testing *testing.T) { + cmd := exec.Command(binaryName, tt.args...) + output, err := cmd.CombinedOutput() + actual := string(output) + if (err != nil) != tt.wantErr { + if tt.wantErr { + t.Fatalf("Test %s expected to fail but did not. Error message: %v Output: %s\n", tt.name, err, actual) + } else { + t.Fatalf("Test %s expected to pass but did not. Error message: %v Output: %s\n", tt.name, err, actual) + } + } + golden := NewGoldenFile(t, tt.golden) + expected := golden.Load() + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("diff: %v", Diff(actual, expected)) + } + + }) + + } +} diff --git a/infrastructure/cli/pkg/tests/golden.go b/infrastructure/cli/integration/test_file.go similarity index 88% rename from infrastructure/cli/pkg/tests/golden.go rename to infrastructure/cli/integration/test_file.go index 5a1fe2be72..a4547b9f01 100644 --- a/infrastructure/cli/pkg/tests/golden.go +++ b/infrastructure/cli/integration/test_file.go @@ -1,4 +1,4 @@ -package tests +package integration import ( "io/ioutil" @@ -22,7 +22,7 @@ func Diff(expected, actual interface{}) []string { // NewGoldenFile function func NewGoldenFile(t *testing.T, name string) *TestFile { - return &TestFile{t: t, name: name + ".golden", dir: "./pkg/tests/golden/"} + return &TestFile{t: t, name: name + ".golden", dir: "./golden/"} } func (tf *TestFile) path() string { diff --git a/infrastructure/cli/integration/version_test.go b/infrastructure/cli/integration/version_test.go new file mode 100644 index 0000000000..0d95253635 --- /dev/null +++ b/infrastructure/cli/integration/version_test.go @@ -0,0 +1,40 @@ +package integration + +import ( + "os/exec" + "testing" + + "reflect" +) + +func TestVersion(t *testing.T) { + tests := []struct { + name string + args []string + golden string + wantErr bool + }{ + {"version", []string{"version", "--cli-config", "golden/cli.yaml"}, "cli.version", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(testing *testing.T) { + cmd := exec.Command(binaryName, tt.args...) + output, err := cmd.CombinedOutput() + actual := string(output) + if (err != nil) != tt.wantErr { + if tt.wantErr { + t.Fatalf("Test %s expected to fail but did not. Error message: %v Output: %s\n", tt.name, err, actual) + } else { + t.Fatalf("Test %s expected to pass but did not. Error message: %v Output: %s\n", tt.name, err, actual) + } + } + golden := NewGoldenFile(t, tt.golden) + expected := golden.Load() + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("diff: %v", Diff(actual, expected)) + } + }) + } +} diff --git a/infrastructure/cli/main_test.go b/infrastructure/cli/main_test.go deleted file mode 100644 index 48acae1fd4..0000000000 --- a/infrastructure/cli/main_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import ( - "os/exec" - "testing" - - airytests "cli/pkg/tests" - "reflect" -) - -const binaryName = "./airy" - -func TestCli(t *testing.T) { - tests := []struct { - name string - args []string - golden string - wantErr bool - }{ - {"no args", []string{}, "cli.no-args", false}, - {"login", []string{"api", "login", "--cli-config", "pkg/tests/golden/airycli.yaml"}, "cli.login", false}, - {"version", []string{"version", "--cli-config", "pkg/tests/golden/airycli.yaml"}, "cli.version", false}, - } - - go func() { - airytests.MockServer() - }() - - for _, tt := range tests { - t.Run(tt.name, func(testing *testing.T) { - cmd := exec.Command(binaryName, tt.args...) - output, err := cmd.CombinedOutput() - actual := string(output) - if (err != nil) != tt.wantErr { - t.Fatalf("Test %s expected to fail: %t. Did the test pass: %t. Error message: %v Output: %s\n", tt.name, tt.wantErr, err == nil, err, actual) - } - golden := airytests.NewGoldenFile(t, tt.golden) - expected := golden.Load() - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("diff: %v", airytests.Diff(actual, expected)) - } - - }) - - } -} diff --git a/infrastructure/cli/pkg/tests/BUILD b/infrastructure/cli/pkg/tests/BUILD deleted file mode 100644 index 98517f41f0..0000000000 --- a/infrastructure/cli/pkg/tests/BUILD +++ /dev/null @@ -1,17 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") - -package(default_visibility = ["//visibility:public"]) - -go_library( - name = "tests", - srcs = [ - "golden.go", - "mockserver.go", - ], - importpath = "cli/pkg/tests", - deps = [ - "@com_github_kr_pretty//:pretty", - "@io_goji//:goji.io", - "@io_goji//pat", - ], -) diff --git a/infrastructure/cli/pkg/tests/mockserver.go b/infrastructure/cli/pkg/tests/mockserver.go deleted file mode 100644 index 1103a5f99e..0000000000 --- a/infrastructure/cli/pkg/tests/mockserver.go +++ /dev/null @@ -1,52 +0,0 @@ -package tests - -import ( - "fmt" - "io/ioutil" - "log" - "net/http" - "time" - - "goji.io" - "goji.io/pat" -) - -// MockServer starts the local server that returns the corresponding golden files for each endpoint -func MockServer() { - mux := goji.NewMux() - mux.HandleFunc(pat.Post("/users.signup"), mockUserSignupHandler) - mux.HandleFunc(pat.Post("/users.login"), mockUserLoginHandler) - - s := &http.Server{ - Addr: ":3001", - Handler: mux, - ReadTimeout: 10 * time.Second, - WriteTimeout: 10 * time.Second, - } - err := s.ListenAndServe() - if err != nil { - log.Println(err) - } -} - -func mockUserSignupHandler(w http.ResponseWriter, r *http.Request) { - data, err := ioutil.ReadFile("pkg/tests/golden/api.signup.golden") - if err != nil { - fmt.Fprint(w, err) - } - _, err = w.Write(data) - if err != nil { - log.Println(err) - } -} - -func mockUserLoginHandler(w http.ResponseWriter, r *http.Request) { - data, err := ioutil.ReadFile("pkg/tests/golden/api.signup.golden") - if err != nil { - fmt.Fprint(w, err) - } - _, err = w.Write(data) - if err != nil { - log.Println(err) - } -} diff --git a/lib/go/httpclient/BUILD b/lib/go/httpclient/BUILD index 6ac19451f5..778fda8778 100644 --- a/lib/go/httpclient/BUILD +++ b/lib/go/httpclient/BUILD @@ -1,4 +1,4 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") # gazelle:prefix github.com/airyhq/airy/lib/go/httpclient @@ -13,3 +13,13 @@ go_library( visibility = ["//visibility:public"], deps = ["//lib/go/httpclient/payloads"], ) + +go_test( + name = "httpclient_test", + srcs = ["users_test.go"], + embed = [":httpclient"], + deps = [ + "//lib/go/httpclient/payloads", + "@com_github_stretchr_testify//assert", + ], +) diff --git a/lib/go/httpclient/http-client-test/BUILD b/lib/go/httpclient/http-client-test/BUILD deleted file mode 100644 index 3a4a8ebe15..0000000000 --- a/lib/go/httpclient/http-client-test/BUILD +++ /dev/null @@ -1,11 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_test") - -go_test( - name = "http-client-test_test", - srcs = ["users_test.go"], - deps = [ - "//lib/go/httpclient", - "//lib/go/httpclient/payloads", - "@com_github_stretchr_testify//assert", - ], -) diff --git a/lib/go/httpclient/http-client-test/users_test.go b/lib/go/httpclient/users_test.go similarity index 55% rename from lib/go/httpclient/http-client-test/users_test.go rename to lib/go/httpclient/users_test.go index fc3bea1d35..8124dab466 100644 --- a/lib/go/httpclient/http-client-test/users_test.go +++ b/lib/go/httpclient/users_test.go @@ -1,4 +1,4 @@ -package tests +package httpclient import ( "fmt" @@ -6,18 +6,20 @@ import ( "net/http/httptest" "testing" - "github.com/airyhq/airy/lib/go/httpclient" "github.com/airyhq/airy/lib/go/httpclient/payloads" - "github.com/stretchr/testify/assert" ) -func TestSignup(t *testing.T) { - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +func mockedUserResponseServer() *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "{\"id\":\"a6c413a7-8d42-4c2b-8736-d033134eec59\",\"first_name\":\"Grace\",\"last_name\":\"Hopper\",\"token\":\"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJhNmM0MTNhNy04ZDQyLTRjMmItODczNi1kMDMzMTM0ZWVjNTkiLCJzdWIiOiJhNmM0MTNhNy04ZDQyLTRjMmItODczNi1kMDMzMTM0ZWVjNTkiLCJpYXQiOjE2MDczMzY2NjMsInVzZXJfaWQiOiJhNmM0MTNhNy04ZDQyLTRjMmItODczNi1kMDMzMTM0ZWVjNTkiLCJleHAiOjE2MDc0MjMwNjN9.I4sf2j36RQCPRrirzSYyRhJ4U3bG2sUmHfxX4yBJvQA\"}") })) - c := httpclient.NewClient(ts.URL) +} + +func TestSignup(t *testing.T) { + ts := mockedUserResponseServer() + defer ts.Close() + c := NewClient(ts.URL) signupRequestPayload := payloads.SignupRequestPayload{FirstName: "Grace", LastName: "Hopper", Password: "the_answer_is_42", Email: "grace@example.com"} @@ -25,24 +27,18 @@ func TestSignup(t *testing.T) { assert.Nil(t, err, "expecting nil error") assert.NotNil(t, res, "expecting non-nil result") - - assert.NotEmpty(t, res.Token, "expecting non-empty token") - + assert.Equal(t, res.Token, "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJhNmM0MTNhNy04ZDQyLTRjMmItODczNi1kMDMzMTM0ZWVjNTkiLCJzdWIiOiJhNmM0MTNhNy04ZDQyLTRjMmItODczNi1kMDMzMTM0ZWVjNTkiLCJpYXQiOjE2MDczMzY2NjMsInVzZXJfaWQiOiJhNmM0MTNhNy04ZDQyLTRjMmItODczNi1kMDMzMTM0ZWVjNTkiLCJleHAiOjE2MDc0MjMwNjN9.I4sf2j36RQCPRrirzSYyRhJ4U3bG2sUmHfxX4yBJvQA") } func TestLogin(t *testing.T) { - - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, "{\"id\":\"a6c413a7-8d42-4c2b-8736-d033134eec59\",\"first_name\":\"Grace\",\"last_name\":\"Hopper\",\"token\":\"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJhNmM0MTNhNy04ZDQyLTRjMmItODczNi1kMDMzMTM0ZWVjNTkiLCJzdWIiOiJhNmM0MTNhNy04ZDQyLTRjMmItODczNi1kMDMzMTM0ZWVjNTkiLCJpYXQiOjE2MDczMzY2NjMsInVzZXJfaWQiOiJhNmM0MTNhNy04ZDQyLTRjMmItODczNi1kMDMzMTM0ZWVjNTkiLCJleHAiOjE2MDc0MjMwNjN9.I4sf2j36RQCPRrirzSYyRhJ4U3bG2sUmHfxX4yBJvQA\"}") - })) - c := httpclient.NewClient(ts.URL) + ts := mockedUserResponseServer() + defer ts.Close() + c := NewClient(ts.URL) loginRequestPayload := payloads.LoginRequestPayload{Password: "the_answer_is_42", Email: "grace@example.com"} res, err := c.Login(loginRequestPayload) - assert.Nil(t, err, "expecting nil error") assert.NotNil(t, res, "expecting non-nil result") - - assert.NotEmpty(t, res.Token, "expecting non-empty token") + assert.Equal(t, res.Token, "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJhNmM0MTNhNy04ZDQyLTRjMmItODczNi1kMDMzMTM0ZWVjNTkiLCJzdWIiOiJhNmM0MTNhNy04ZDQyLTRjMmItODczNi1kMDMzMTM0ZWVjNTkiLCJpYXQiOjE2MDczMzY2NjMsInVzZXJfaWQiOiJhNmM0MTNhNy04ZDQyLTRjMmItODczNi1kMDMzMTM0ZWVjNTkiLCJleHAiOjE2MDc0MjMwNjN9.I4sf2j36RQCPRrirzSYyRhJ4U3bG2sUmHfxX4yBJvQA") } From 8f9ffee0d84cad2c5f5c6bff3c5165c64050998f Mon Sep 17 00:00:00 2001 From: Paulo Diniz Date: Thu, 21 Jan 2021 16:51:38 +0100 Subject: [PATCH 36/56] [#704] Update websocket documentation (#705) --- docs/docs/api/websocket.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/api/websocket.md b/docs/docs/api/websocket.md index 287ac1d3be..9145c1b591 100644 --- a/docs/docs/api/websocket.md +++ b/docs/docs/api/websocket.md @@ -39,7 +39,7 @@ Incoming payloads notify connected clients that a message was created or updated // Determines the schema of the content }, // typed source message model - "state": "{String}", + "delivery_state": "{String}", // delivery state of message, one of PENDING, FAILED, DELIVERED "sender_type": "{string/enum}", // See glossary From 87dd67ffaaf0741a3d88908a2e63641cffcbe548 Mon Sep 17 00:00:00 2001 From: Skander Garroum Date: Thu, 21 Jan 2021 17:51:14 +0100 Subject: [PATCH 37/56] Update README.md (#700) * Update README.md * Apply suggestions from code review Co-authored-by: lucapette Co-authored-by: lucapette --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4f5bf70d0a..77db844273 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,66 @@ +

+ Airy-logo + + +

The open source, fully-featured, production ready
+
Messaging platform
+

+ + + # Airy Core Platform +[![Join the chat on Airy community](https://img.shields.io/badge/forum-join%20discussions-brightgreen.svg)](https://airy.co/community/?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Documentation Status](https://img.shields.io/badge/docs-stable-brightgreen.svg)](https://docs.airy.co/) +[![Commit Frequency](https://img.shields.io/github/commit-activity/m/airyhq/airy)](https://docs.airy.co/) +[![License](https://img.shields.io/github/license/airyhq/airy)](https://docs.airy.co/) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/airyhq/airy/projects) + + The Airy Core Platform is an open source, fully-featured, production ready -messaging platform to process conversational data from a variety of sources -(like Facebook Messenger, Google Business Messages, Website Live Chat, and -more). +messaging platform. +With Airy you can process conversational data from a variety of sources: + + - **Facebook** + - **Whatsapp** + - **Google's Business Messages** + - **SMS** + - **Website Chat Plugins** + - **Twilio** + - **Your own conversational channels** + +You can then use Airy to: + + - **Unify your messaging channels** + - **Stream your conversational data wherever you want** + - **Integrate with different NLP frameworks** + - **Mediate open requests with Agents via our messaging UI** + - **Analyze your conversations** + +Since Airy's infrastructure is built around Apache Kafka, it can process a large amount of conversations and messages simultaneously and stream the relevant conversational data to wherever you need it. + +Learn more about what we open-sourced in the +[announcement blog post](https://airy.co/blog/what-we-open-sourced). + +## About Airy + +- **What does Airy do? 🚀** + [Learn more on our Website](https://airy.co/developers) + +- **I'm new to Airy 😄** + [Get Started with Airy](https://docs.airy.co/) + +- **I'd like to read the detailed docs 📖** + [Read The Docs](https://docs.airy.co/) + +- **I'm ready to install Airy ✨** + [Installation](https://docs.airy.co/) + +- **I have a question ❓** + [The Airy Community will help](https://airy.co/community) + +- **Or continue reading the Read Me** -- [Airy Core Platform](#airy-core-platform) - [Getting started](#getting-started) - [Components](#components) - [Organization of the Repository](#organization-of-the-repository) From aec5e9f11264f8a59ff23b3fc86739f742b8e733 Mon Sep 17 00:00:00 2001 From: lucapette Date: Fri, 22 Jan 2021 09:51:47 +0100 Subject: [PATCH 38/56] Fix typo in readme (#709) --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 77db844273..1236e52d9e 100644 --- a/README.md +++ b/README.md @@ -18,13 +18,13 @@ The Airy Core Platform is an open source, fully-featured, production ready -messaging platform. +messaging platform. With Airy you can process conversational data from a variety of sources: - **Facebook** - - **Whatsapp** + - **WhatsApp** - **Google's Business Messages** - - **SMS** + - **SMS** - **Website Chat Plugins** - **Twilio** - **Your own conversational channels** @@ -37,9 +37,11 @@ You can then use Airy to: - **Mediate open requests with Agents via our messaging UI** - **Analyze your conversations** -Since Airy's infrastructure is built around Apache Kafka, it can process a large amount of conversations and messages simultaneously and stream the relevant conversational data to wherever you need it. +Since Airy's infrastructure is built around Apache Kafka, it can process a +large amount of conversations and messages simultaneously and stream the +relevant conversational data to wherever you need it. -Learn more about what we open-sourced in the +Learn more about what we open-sourced in the [announcement blog post](https://airy.co/blog/what-we-open-sourced). ## About Airy From 66df6531d2790308fe50d1d19d3beb837f6f4b19 Mon Sep 17 00:00:00 2001 From: Kazeem Adetunji Date: Fri, 22 Jan 2021 11:16:09 +0100 Subject: [PATCH 39/56] [#713] Fix bug on tags list (#714) --- .../src/pages/Inbox/Messenger/ConversationMetadata/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/demo/src/pages/Inbox/Messenger/ConversationMetadata/index.tsx b/frontend/demo/src/pages/Inbox/Messenger/ConversationMetadata/index.tsx index e11ada183d..12297b3b66 100644 --- a/frontend/demo/src/pages/Inbox/Messenger/ConversationMetadata/index.tsx +++ b/frontend/demo/src/pages/Inbox/Messenger/ConversationMetadata/index.tsx @@ -40,7 +40,7 @@ const ConversationMetadata = (props: ConversationMetadataProps) => { if (tags.length == 0) { listTags(); } - }); + }, []); const showAddTags = () => { setTagName(''); From 454f3eec98bba8ac1fe7f772aa38e32962b6cbf8 Mon Sep 17 00:00:00 2001 From: lucapette Date: Fri, 22 Jan 2021 15:02:46 +0100 Subject: [PATCH 40/56] Paginate messages for real (#715) --- .../java/co/airy/core/api/communication/MessagesController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/MessagesController.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/MessagesController.java index adecebc019..452bbaca25 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/MessagesController.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/MessagesController.java @@ -52,7 +52,7 @@ private MessageListResponsePayload fetchMessages(String conversationId, int page Page page = paginator.page(); return MessageListResponsePayload.builder() - .data(messages.stream().map(mapper::fromMessage).collect(toList())) + .data(page.getData().stream().map(mapper::fromMessage).collect(toList())) .responseMetadata(MessageListResponsePayload.ResponseMetadata.builder() .nextCursor(page.getNextCursor()) .previousCursor(cursor) From cbd310c05415c887db1fc548f132a22d19182d52 Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Fri, 22 Jan 2021 16:20:06 +0100 Subject: [PATCH 41/56] [#698] Provide endpoint configuration to the frontend (#712) * [#698] Provide endpoint configuration to the frontend * add chatplugin env config * Hostnames in a separate configMap * Use a custom nginx image Co-authored-by: ljupcovangelski --- WORKSPACE | 8 ++++---- frontend/chat-plugin/BUILD | 2 +- .../{index.html => development.html} | 0 frontend/chat-plugin/example.html | 2 +- frontend/chat-plugin/nginx.conf | 15 +++++++++++++++ frontend/demo/index.html | 9 ++++++--- frontend/demo/nginx.conf | 19 +++++++++++++++++-- frontend/demo/src/InitializeAiryApi.ts | 3 ++- frontend/demo/src/env.ts | 5 +++++ infrastructure/airy.tpl.yaml | 9 ++++++++- .../charts/airy-config/templates/api.yaml | 1 - .../airy-config/templates/frontend.yaml | 9 --------- .../charts/airy-config/templates/sources.yaml | 8 ++++++++ .../templates/deployment.yaml | 9 ++++++--- .../frontend-demo/templates/deployment.yaml | 9 ++++++--- .../templates/deployment.yaml | 2 +- .../charts/ingress/templates/configmap.yaml | 9 +++++++++ infrastructure/images/nginx/Dockerfile | 5 +++++ infrastructure/images/nginx/Makefile | 6 ++++++ 19 files changed, 100 insertions(+), 30 deletions(-) rename frontend/chat-plugin/{index.html => development.html} (100%) create mode 100644 frontend/demo/src/env.ts delete mode 100644 infrastructure/helm-chart/charts/apps/charts/airy-config/templates/frontend.yaml create mode 100644 infrastructure/helm-chart/charts/ingress/templates/configmap.yaml create mode 100644 infrastructure/images/nginx/Dockerfile create mode 100644 infrastructure/images/nginx/Makefile diff --git a/WORKSPACE b/WORKSPACE index ba708a9056..fa8ef5d216 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -188,10 +188,10 @@ container_pull( container_pull( name = "nginx_base", - digest = "sha256:662a0c5a8677063c27b0ddd42f1c801be643b9502f7b1a4e2e727cb2bc3808a8", - registry = "index.docker.io", - repository = "nginx", - tag = "stable-alpine", + digest = "sha256:0340d329672fb3f0192754e4e1ccd7518ecc83f6644e8f0c317012bbc4d06d24", + registry = "ghcr.io", + repository = "airyhq/frontend/nginx-lua", + tag = "1.0.0", ) load( diff --git a/frontend/chat-plugin/BUILD b/frontend/chat-plugin/BUILD index 2eb6590268..790af5355d 100644 --- a/frontend/chat-plugin/BUILD +++ b/frontend/chat-plugin/BUILD @@ -14,7 +14,7 @@ web_app( name = "bundle", app_lib = ":app", entry = "frontend/chat-plugin/src/iframe.js", - index = ":index.html", + index = ":development.html", module_deps = module_deps, ) diff --git a/frontend/chat-plugin/index.html b/frontend/chat-plugin/development.html similarity index 100% rename from frontend/chat-plugin/index.html rename to frontend/chat-plugin/development.html diff --git a/frontend/chat-plugin/example.html b/frontend/chat-plugin/example.html index bb822ae59c..c0f2f7f4f6 100644 --- a/frontend/chat-plugin/example.html +++ b/frontend/chat-plugin/example.html @@ -207,7 +207,7 @@ (function (w, d, s, n) { w[n] = w[n] || {}; w[n].cid = search.get("channel_id"); - w[n].h = "chatplugin.airy"; + w[n].h = "{{API_HOST}}"; // Only to be used for local installations w[n].no_tls = true; var f = d.getElementsByTagName(s)[0], diff --git a/frontend/chat-plugin/nginx.conf b/frontend/chat-plugin/nginx.conf index 1541330a74..2ecf87d540 100644 --- a/frontend/chat-plugin/nginx.conf +++ b/frontend/chat-plugin/nginx.conf @@ -8,6 +8,9 @@ events { worker_connections 1024; } +# By default nginx does not make any env variables accessible to lua +# http://nginx.org/en/docs/ngx_core_module.html#env +env API_HOST; http { include /etc/nginx/mime.types; @@ -32,6 +35,18 @@ http { location / { } + location /example.html { + default_type text/html; + + content_by_lua_block { + local template = require("resty.template") + local template_string = ngx.location.capture("/example.html") + template.render(template_string.body, { + API_HOST = os.getenv("API_HOST") + }) + } + } + location /health { access_log off; return 200 "healthy\n"; diff --git a/frontend/demo/index.html b/frontend/demo/index.html index e9bda5d610..389880c5a8 100644 --- a/frontend/demo/index.html +++ b/frontend/demo/index.html @@ -1,6 +1,7 @@ + Airy UI - - Airy Inbox Demo + -
diff --git a/frontend/demo/nginx.conf b/frontend/demo/nginx.conf index fbbe8ae352..bcf8949727 100644 --- a/frontend/demo/nginx.conf +++ b/frontend/demo/nginx.conf @@ -8,6 +8,9 @@ events { worker_connections 1024; } +# By default nginx does not make any env variables accessible to lua +# http://nginx.org/en/docs/ngx_core_module.html#env +env API_HOST; http { include /etc/nginx/mime.types; @@ -30,11 +33,23 @@ http { root /usr/share/nginx/html; location / { - try_files $uri @rewrites; + try_files $uri @rewrites @lua_index; } location @rewrites { - rewrite ^(.+)$ /index.html last; + rewrite ^(.+)$ @lua_index last; + } + + location @lua_index { + default_type text/html; + + content_by_lua_block { + local template = require("resty.template") + local template_string = ngx.location.capture("/index.html") + template.render(template_string.body, { + API_HOST = os.getenv("API_HOST") + }) + } } location /health { diff --git a/frontend/demo/src/InitializeAiryApi.ts b/frontend/demo/src/InitializeAiryApi.ts index 19edf6d76d..bbe464f6ec 100644 --- a/frontend/demo/src/InitializeAiryApi.ts +++ b/frontend/demo/src/InitializeAiryApi.ts @@ -1,6 +1,7 @@ import {HttpClient} from 'httpclient'; import {getAuthToken} from './cookies'; +import {env} from './env'; const authToken = getAuthToken(); -export const HttpClientInstance = new HttpClient(authToken); +export const HttpClientInstance = new HttpClient(authToken, `//${env.apiHost}`); diff --git a/frontend/demo/src/env.ts b/frontend/demo/src/env.ts new file mode 100644 index 0000000000..f7137910cc --- /dev/null +++ b/frontend/demo/src/env.ts @@ -0,0 +1,5 @@ +export interface Env { + apiHost?: string; +} + +export const env = (window as any).airy || {}; diff --git a/infrastructure/airy.tpl.yaml b/infrastructure/airy.tpl.yaml index 06e0b5446f..9a36f174b5 100644 --- a/infrastructure/airy.tpl.yaml +++ b/infrastructure/airy.tpl.yaml @@ -18,7 +18,7 @@ core: postgresql: endpoint: "postgres:5432" dbName: "admin" - username: "postgresadmin" + username: "postgresadmin" password: "changeme" # Specific configurations for sources sources: @@ -33,8 +33,11 @@ core: twilio: authToken: "changeme" accountSid: "changeme" + chatPlugin: + host: "http://chatplugin.airy" # Specific configuration for the API apps api: + host: "http://api.airy" mailFrom: "changeme" mailPort: 587 mailUrl: "changeme" @@ -49,3 +52,7 @@ core: bucket: "changeme" region: "changeme" path: "path" +ingress: + apiHost: api.airy + uiHost: demo.airy + chatpluginHost: chatplugin.airy diff --git a/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/api.yaml b/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/api.yaml index d6034a0064..72473ace16 100644 --- a/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/api.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/api.yaml @@ -11,4 +11,3 @@ data: MAIL_PASSWORD: {{ .Values.api.mailPassword }} JWT_SECRET: {{ randAlphaNum 128 | quote }} ALLOWED_ORIGINS: {{ .Values.api.allowedOrigins | quote }} - \ No newline at end of file diff --git a/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/frontend.yaml b/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/frontend.yaml deleted file mode 100644 index 533db43ce5..0000000000 --- a/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/frontend.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# Dummy configmap so that frontend deployments can be started by the controller -# TODO remove once there is an annotation -apiVersion: v1 -kind: ConfigMap -metadata: - name: frontend-config - namespace: {{ .Values.global.namespace }} -data: - dummy: "blank" diff --git a/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/sources.yaml b/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/sources.yaml index b59397d601..3450fc296e 100644 --- a/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/sources.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/sources.yaml @@ -1,6 +1,14 @@ {{ $coreID := randAlphaNum 10 | lower }} apiVersion: v1 kind: ConfigMap +metadata: + name: sources-chatplugin + namespace: {{ .Values.global.namespace }} +data: + JWT_SECRET: {{ randAlphaNum 128 | quote }} +--- +apiVersion: v1 +kind: ConfigMap metadata: name: sources-facebook namespace: {{ .Values.global.namespace }} diff --git a/infrastructure/helm-chart/charts/apps/charts/frontend-chat-plugin/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/frontend-chat-plugin/templates/deployment.yaml index ddf58398fe..3c89f8e1c4 100644 --- a/infrastructure/helm-chart/charts/apps/charts/frontend-chat-plugin/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/frontend-chat-plugin/templates/deployment.yaml @@ -25,9 +25,12 @@ spec: - name: app image: "{{ .Values.global.containerRegistry}}/{{ .Values.image }}:{{ .Values.global.appImageTag }}" imagePullPolicy: Always - envFrom: - - configMapRef: - name: frontend-config + env: + - name: API_HOST + valueFrom: + configMapKeyRef: + name: hostnames + key: CHATPLUGIN_HOST livenessProbe: httpGet: path: /health diff --git a/infrastructure/helm-chart/charts/apps/charts/frontend-demo/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/frontend-demo/templates/deployment.yaml index ddd62e7a55..cca768d4e8 100644 --- a/infrastructure/helm-chart/charts/apps/charts/frontend-demo/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/frontend-demo/templates/deployment.yaml @@ -25,9 +25,12 @@ spec: - name: app image: "{{ .Values.global.containerRegistry}}/{{ .Values.image }}:{{ .Values.global.appImageTag }}" imagePullPolicy: Always - envFrom: - - configMapRef: - name: frontend-config + env: + - name: API_HOST + valueFrom: + configMapKeyRef: + name: hostnames + key: API_HOST livenessProbe: httpGet: path: /health diff --git a/infrastructure/helm-chart/charts/apps/charts/sources-chatplugin/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/sources-chatplugin/templates/deployment.yaml index 41eb556183..f9b2ef1b99 100644 --- a/infrastructure/helm-chart/charts/apps/charts/sources-chatplugin/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/sources-chatplugin/templates/deployment.yaml @@ -35,7 +35,7 @@ spec: - name: JWT_SECRET valueFrom: configMapKeyRef: - name: api-config + name: sources-chatplugin key: JWT_SECRET - name: KAFKA_BROKERS valueFrom: diff --git a/infrastructure/helm-chart/charts/ingress/templates/configmap.yaml b/infrastructure/helm-chart/charts/ingress/templates/configmap.yaml new file mode 100644 index 0000000000..297ad1c5b9 --- /dev/null +++ b/infrastructure/helm-chart/charts/ingress/templates/configmap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: hostnames + namespace: {{ .Values.global.namespace }} +data: + API_HOST: {{ .Values.apiHost }} + UI_HOST: {{ .Values.uiHost }} + CHATPLUGIN_HOST: {{ .Values.chatpluginHost }} diff --git a/infrastructure/images/nginx/Dockerfile b/infrastructure/images/nginx/Dockerfile new file mode 100644 index 0000000000..c3870d3e11 --- /dev/null +++ b/infrastructure/images/nginx/Dockerfile @@ -0,0 +1,5 @@ +FROM fabiocicerchia/nginx-lua:1.19.6-alpine3.13.0 + +RUN apk add --no-cache lua5.1-dev luarocks5.1 +RUN luarocks-5.1 install lua-resty-template +RUN apk del lua5.1-dev luarocks5.1 diff --git a/infrastructure/images/nginx/Makefile b/infrastructure/images/nginx/Makefile new file mode 100644 index 0000000000..91419b5941 --- /dev/null +++ b/infrastructure/images/nginx/Makefile @@ -0,0 +1,6 @@ +build: + docker build -t nginx-lua . + +release: build + docker tag nginx-lua ghcr.io/airyhq/frontend/nginx-lua:1.0.0 + docker push ghcr.io/airyhq/frontend/nginx-lua:1.0.0 From 3968393755959c1e57bce99db42fa13012b8ba3e Mon Sep 17 00:00:00 2001 From: Pascal Holy <54705263+pascal-airy@users.noreply.github.com> Date: Fri, 22 Jan 2021 17:24:13 +0100 Subject: [PATCH 42/56] [#698] Rm extra bracket (#717) --- frontend/demo/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/demo/index.html b/frontend/demo/index.html index 389880c5a8..93a117d42c 100644 --- a/frontend/demo/index.html +++ b/frontend/demo/index.html @@ -17,7 +17,7 @@ From 6a428f836e32fdfa5859daa9df69f66c6687d382 Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Mon, 25 Jan 2021 12:59:16 +0100 Subject: [PATCH 43/56] [#698] Use host config coming from the env (#721) * [#698] Use host config coming from the env * Try to decouple templating from window.airy Co-authored-by: lucapette --- frontend/demo/index.html | 2 +- frontend/demo/src/InitializeAiryApi.ts | 2 +- frontend/demo/src/env.ts | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/demo/index.html b/frontend/demo/index.html index 93a117d42c..f85c9a66c6 100644 --- a/frontend/demo/index.html +++ b/frontend/demo/index.html @@ -16,7 +16,7 @@ --> diff --git a/frontend/demo/src/InitializeAiryApi.ts b/frontend/demo/src/InitializeAiryApi.ts index bbe464f6ec..53a92df676 100644 --- a/frontend/demo/src/InitializeAiryApi.ts +++ b/frontend/demo/src/InitializeAiryApi.ts @@ -4,4 +4,4 @@ import {env} from './env'; const authToken = getAuthToken(); -export const HttpClientInstance = new HttpClient(authToken, `//${env.apiHost}`); +export const HttpClientInstance = new HttpClient(authToken, `//${env.API_HOST}`); diff --git a/frontend/demo/src/env.ts b/frontend/demo/src/env.ts index f7137910cc..d53a86c6f4 100644 --- a/frontend/demo/src/env.ts +++ b/frontend/demo/src/env.ts @@ -1,5 +1,9 @@ export interface Env { - apiHost?: string; + API_HOST?: string; } -export const env = (window as any).airy || {}; +const templatedState: Env = (window as any).AIRY_TEMPLATED_STATE || {}; + +export const env: Env = { + API_HOST: templatedState.API_HOST || 'api.airy', +}; From 302b65ce9adb10435141c9926b1dba774ef2e559 Mon Sep 17 00:00:00 2001 From: lucapette Date: Mon, 25 Jan 2021 13:31:57 +0100 Subject: [PATCH 44/56] [#711] Add CI status badge (#724) Fixes #711 --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1236e52d9e..4841d2f2af 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ [![Join the chat on Airy community](https://img.shields.io/badge/forum-join%20discussions-brightgreen.svg)](https://airy.co/community/?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Documentation Status](https://img.shields.io/badge/docs-stable-brightgreen.svg)](https://docs.airy.co/) +[![CI](https://github.com/airyhq/airy/workflows/CI/badge.svg)](https://github.com/airyhq/airy/actions?query=workflow%3ACI) [![Commit Frequency](https://img.shields.io/github/commit-activity/m/airyhq/airy)](https://docs.airy.co/) [![License](https://img.shields.io/github/license/airyhq/airy)](https://docs.airy.co/) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/airyhq/airy/projects) From e2b1f01307a5b5ff8b1d4a375e0e95c26fda23a2 Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Mon, 25 Jan 2021 14:24:36 +0100 Subject: [PATCH 45/56] Fix infinite recursion in chatplugin nginx location capture (#725) --- frontend/chat-plugin/nginx.conf | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frontend/chat-plugin/nginx.conf b/frontend/chat-plugin/nginx.conf index 2ecf87d540..e5859392ac 100644 --- a/frontend/chat-plugin/nginx.conf +++ b/frontend/chat-plugin/nginx.conf @@ -32,10 +32,7 @@ http { listen 80; root /usr/share/nginx/html; - location / { - } - - location /example.html { + location = /example { default_type text/html; content_by_lua_block { From 4ef07533236f7ef874981a520540aa6786ef93f9 Mon Sep 17 00:00:00 2001 From: lucapette Date: Mon, 25 Jan 2021 15:04:32 +0100 Subject: [PATCH 46/56] Improve config tests and introduce integration tests runner func (#726) --- infrastructure/cli/cmd/config/config.go | 15 ++------ infrastructure/cli/integration/BUILD | 1 + .../cli/integration/api_login_test.go | 36 ++---------------- .../cli/integration/golden/cli.no-args.golden | 2 +- infrastructure/cli/integration/noargs_test.go | 35 +----------------- infrastructure/cli/integration/runner.go | 37 +++++++++++++++++++ .../cli/integration/version_test.go | 31 +--------------- 7 files changed, 51 insertions(+), 106 deletions(-) create mode 100644 infrastructure/cli/integration/runner.go diff --git a/infrastructure/cli/cmd/config/config.go b/infrastructure/cli/cmd/config/config.go index c65ae4ed19..904ca38881 100644 --- a/infrastructure/cli/cmd/config/config.go +++ b/infrastructure/cli/cmd/config/config.go @@ -16,13 +16,7 @@ var configFile string var ConfigCmd = &cobra.Command{ Use: "config", TraverseChildren: true, - Short: "Reloads configuration based on airy.yaml", - Long: `Reloads configuration based on airy.yaml`, - Run: config, -} - -func config(cmd *cobra.Command, args []string) { - fmt.Println("config called") + Short: "Manages your Airy Core Platform instance via airy.yaml", } func applyConfig(cmd *cobra.Command, args []string) { @@ -48,14 +42,13 @@ func applyConfig(cmd *cobra.Command, args []string) { var applyConfigCmd = &cobra.Command{ Use: "apply", TraverseChildren: true, - Short: "Applies configuration based on airy.yaml", - Long: `Applies configuration based on airy.yaml`, + Short: "Applies configuration values from airy.yaml configuration to the Airy Core Platform", Run: applyConfig, } func init() { - ConfigCmd.PersistentFlags().StringVar(&kubeConfigFile, "kube-config", "", "Kubernetes config file for the cluster where Airy is running (default \"~/.airy/kube.conf\")") - ConfigCmd.PersistentFlags().StringVar(&configFile, "config", "../airy.yaml", "Configuration file for the Airy platform") + ConfigCmd.PersistentFlags().StringVar(&kubeConfigFile, "kube-config", "", "Kubernetes config file for the cluster where the Airy Core Platform is running (default \"~/.airy/kube.conf\")") + ConfigCmd.PersistentFlags().StringVar(&configFile, "config", "./airy.yaml", "Configuration file for the Airy Core Platform") if kubeConfigFile == "" { home, err := homedir.Dir() if err != nil { diff --git a/infrastructure/cli/integration/BUILD b/infrastructure/cli/integration/BUILD index 088ab09723..39ed65dabe 100644 --- a/infrastructure/cli/integration/BUILD +++ b/infrastructure/cli/integration/BUILD @@ -4,6 +4,7 @@ go_library( name = "integration", srcs = [ "mockserver.go", + "runner.go", "test_file.go", ], importpath = "cli/integration", diff --git a/infrastructure/cli/integration/api_login_test.go b/infrastructure/cli/integration/api_login_test.go index a07e99231b..31ea6c5a7e 100644 --- a/infrastructure/cli/integration/api_login_test.go +++ b/infrastructure/cli/integration/api_login_test.go @@ -1,47 +1,19 @@ package integration import ( - "os/exec" "testing" - - "reflect" ) func TestApiLogin(t *testing.T) { - tests := []struct { - name string - args []string - golden string - wantErr bool - }{ - {"login", []string{"api", "login", "--cli-config", "golden/cli.yaml"}, "cli.login", false}, - } ms := NewMockServer(t) go func() { ms.Serve() }() - for _, tt := range tests { - t.Run(tt.name, func(testing *testing.T) { - cmd := exec.Command(binaryName, append(tt.args, "--apihost", ms.Host)...) - output, err := cmd.CombinedOutput() - actual := string(output) - if (err != nil) != tt.wantErr { - if tt.wantErr { - t.Fatalf("Test %s expected to fail but did not. Error message: %v Output: %s\n", tt.name, err, actual) - } else { - t.Fatalf("Test %s expected to pass but did not. Error message: %v Output: %s\n", tt.name, err, actual) - } - } - golden := NewGoldenFile(t, tt.golden) - expected := golden.Load() - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("diff: %v", Diff(actual, expected)) - } - - }) - + tests := []test{ + {"login", []string{"api", "login", "--apihost", ms.Host, "--cli-config", "golden/cli.yaml"}, "cli.login", false}, } + + runner(t, tests) } diff --git a/infrastructure/cli/integration/golden/cli.no-args.golden b/infrastructure/cli/integration/golden/cli.no-args.golden index 4f514a3b08..340de9711e 100644 --- a/infrastructure/cli/integration/golden/cli.no-args.golden +++ b/infrastructure/cli/integration/golden/cli.no-args.golden @@ -5,7 +5,7 @@ Usage: Available Commands: api Interacts with the Airy Core Platform HTTP API - config Reloads configuration based on airy.yaml + config Manages your Airy Core Platform instance via airy.yaml help Help about any command init Inits your Airy CLI configuration status Reports the status of your Airy Core Platform diff --git a/infrastructure/cli/integration/noargs_test.go b/infrastructure/cli/integration/noargs_test.go index 2f92e547c0..81d33b5f72 100644 --- a/infrastructure/cli/integration/noargs_test.go +++ b/infrastructure/cli/integration/noargs_test.go @@ -1,21 +1,11 @@ package integration import ( - "os/exec" "testing" - - "reflect" ) -const binaryName = "../airy" - func TestNoArgs(t *testing.T) { - tests := []struct { - name string - args []string - golden string - wantErr bool - }{ + tests := []test{ {"no args", []string{}, "cli.no-args", false}, } @@ -24,26 +14,5 @@ func TestNoArgs(t *testing.T) { ms.Serve() }() - for _, tt := range tests { - t.Run(tt.name, func(testing *testing.T) { - cmd := exec.Command(binaryName, tt.args...) - output, err := cmd.CombinedOutput() - actual := string(output) - if (err != nil) != tt.wantErr { - if tt.wantErr { - t.Fatalf("Test %s expected to fail but did not. Error message: %v Output: %s\n", tt.name, err, actual) - } else { - t.Fatalf("Test %s expected to pass but did not. Error message: %v Output: %s\n", tt.name, err, actual) - } - } - golden := NewGoldenFile(t, tt.golden) - expected := golden.Load() - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("diff: %v", Diff(actual, expected)) - } - - }) - - } + runner(t, tests) } diff --git a/infrastructure/cli/integration/runner.go b/infrastructure/cli/integration/runner.go new file mode 100644 index 0000000000..442dfbe771 --- /dev/null +++ b/infrastructure/cli/integration/runner.go @@ -0,0 +1,37 @@ +package integration + +import ( + "os/exec" + "reflect" + "testing" +) + +type test struct { + name string + args []string + golden string + wantErr bool +} + +func runner(t *testing.T, tests []test) { + for _, tt := range tests { + t.Run(tt.name, func(testing *testing.T) { + cmd := exec.Command("../airy", tt.args...) + output, err := cmd.CombinedOutput() + actual := string(output) + if (err != nil) != tt.wantErr { + if tt.wantErr { + t.Fatalf("Test %s expected to fail but did not. Error message: %v Output: %s\n", tt.name, err, actual) + } else { + t.Fatalf("Test %s expected to pass but did not. Error message: %v Output: %s\n", tt.name, err, actual) + } + } + golden := NewGoldenFile(t, tt.golden) + expected := golden.Load() + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("diff: %v", Diff(actual, expected)) + } + }) + } +} diff --git a/infrastructure/cli/integration/version_test.go b/infrastructure/cli/integration/version_test.go index 0d95253635..e2db0ae45e 100644 --- a/infrastructure/cli/integration/version_test.go +++ b/infrastructure/cli/integration/version_test.go @@ -1,40 +1,13 @@ package integration import ( - "os/exec" "testing" - - "reflect" ) func TestVersion(t *testing.T) { - tests := []struct { - name string - args []string - golden string - wantErr bool - }{ + tests := []test{ {"version", []string{"version", "--cli-config", "golden/cli.yaml"}, "cli.version", false}, } - for _, tt := range tests { - t.Run(tt.name, func(testing *testing.T) { - cmd := exec.Command(binaryName, tt.args...) - output, err := cmd.CombinedOutput() - actual := string(output) - if (err != nil) != tt.wantErr { - if tt.wantErr { - t.Fatalf("Test %s expected to fail but did not. Error message: %v Output: %s\n", tt.name, err, actual) - } else { - t.Fatalf("Test %s expected to pass but did not. Error message: %v Output: %s\n", tt.name, err, actual) - } - } - golden := NewGoldenFile(t, tt.golden) - expected := golden.Load() - - if !reflect.DeepEqual(actual, expected) { - t.Fatalf("diff: %v", Diff(actual, expected)) - } - }) - } + runner(t, tests) } From df16a02e5478676c6617099d4fd621d21c0ce44b Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Mon, 25 Jan 2021 15:31:16 +0100 Subject: [PATCH 47/56] Remove suppression (#702) * Make topology immutable * remove suppression --- .../co/airy/core/sources/facebook/Stores.java | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/Stores.java b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/Stores.java index 9baa3de7ba..ea6cb8e91e 100644 --- a/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/Stores.java +++ b/backend/sources/facebook/connector/src/main/java/co/airy/core/sources/facebook/Stores.java @@ -12,6 +12,7 @@ import co.airy.kafka.schema.application.ApplicationCommunicationMessages; import co.airy.kafka.schema.application.ApplicationCommunicationMetadata; import co.airy.kafka.streams.KafkaStreamsWrapper; +import co.airy.log.AiryLoggerFactory; import org.apache.avro.specific.SpecificRecordBase; import org.apache.kafka.clients.producer.KafkaProducer; import org.apache.kafka.clients.producer.ProducerRecord; @@ -23,6 +24,7 @@ import org.apache.kafka.streams.kstream.Materialized; import org.apache.kafka.streams.kstream.Suppressed; import org.apache.kafka.streams.state.ReadOnlyKeyValueStore; +import org.slf4j.Logger; import org.springframework.beans.factory.DisposableBean; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; @@ -41,6 +43,7 @@ @Service public class Stores implements ApplicationListener, DisposableBean, HealthIndicator { + private static final Logger log = AiryLoggerFactory.getLogger(Stores.class); private static final String appId = "sources.facebook.ConnectorStores"; private final KafkaStreamsWrapper streams; @@ -89,18 +92,21 @@ public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) final KTable conversationTable = messageStream .groupByKey() .aggregate(Conversation::new, - (conversationId, message, aggregate) -> { + (conversationId, message, conversation) -> { + final Conversation.ConversationBuilder conversationBuilder = conversation.toBuilder(); if (SenderType.SOURCE_CONTACT.equals(message.getSenderType())) { - aggregate.setSourceConversationId(message.getSenderId()); + conversationBuilder.sourceConversationId(message.getSenderId()); } + conversationBuilder.channelId(message.getChannelId()); - aggregate.setChannelId(message.getChannelId()); - - return aggregate; + return conversationBuilder.build(); }) - .join(channelsTable, Conversation::getChannelId, (aggregate, channel) -> { - aggregate.setChannel(channel); - return aggregate; + .join(channelsTable, Conversation::getChannelId, (conversation, channel) -> { + return conversation.toBuilder() + .channelId(conversation.getChannelId()) + .channel(channel) + .sourceConversationId(conversation.getSourceConversationId()) + .build(); }); // Send outbound messages @@ -111,9 +117,6 @@ public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) // Fetch missing metadata conversationTable - // To avoid any redundant fetch contact operations the suppression interval should - // be higher than the timeout of the Facebook API - .suppress(Suppressed.untilTimeLimit(Duration.ofMillis(streams.getSuppressIntervalInMs()), Suppressed.BufferConfig.unbounded())) .toStream() .leftJoin(metadataTable, (conversation, metadataMap) -> conversation .toBuilder() From 8d91a4a5caa4591a28e8e402cc852586ad3d67ce Mon Sep 17 00:00:00 2001 From: lucapette Date: Mon, 25 Jan 2021 16:28:34 +0100 Subject: [PATCH 48/56] Return messages last to first so pagination makes sense (#728) --- .../co/airy/core/api/communication/dto/MessagesTreeSet.java | 2 +- .../java/co/airy/core/api/communication/MessagesTest.java | 4 +++- docs/docs/guides/airy-core-in-test-env.md | 3 ++- docs/docs/index.md | 2 +- docs/docs/sources/chat-plugin.md | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/dto/MessagesTreeSet.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/dto/MessagesTreeSet.java index 658a15b875..582d0f5340 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/dto/MessagesTreeSet.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/dto/MessagesTreeSet.java @@ -9,6 +9,6 @@ public class MessagesTreeSet extends TreeSet { @JsonCreator public MessagesTreeSet() { - super(Comparator.comparing(Message::getSentAt)); + super(Comparator.comparing(Message::getSentAt).reversed()); } } diff --git a/backend/api/communication/src/test/java/co/airy/core/api/communication/MessagesTest.java b/backend/api/communication/src/test/java/co/airy/core/api/communication/MessagesTest.java index 6763071239..0fb81ccfd0 100644 --- a/backend/api/communication/src/test/java/co/airy/core/api/communication/MessagesTest.java +++ b/backend/api/communication/src/test/java/co/airy/core/api/communication/MessagesTest.java @@ -23,12 +23,14 @@ import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit.jupiter.SpringExtension; +import java.util.Comparator; import java.util.List; import java.util.UUID; import static co.airy.core.api.communication.util.Topics.getTopics; import static co.airy.core.api.communication.util.Topics.applicationCommunicationChannels; import static co.airy.test.Timing.retryOnException; +import static java.util.Comparator.reverseOrder; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.hasSize; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -94,7 +96,7 @@ void canFetchMessages() throws Exception { records.stream() .map((record) -> ((Message) record.value()).getSentAt()) .map(DateFormat::isoFromMillis) - .sorted().toArray()))), + .sorted(reverseOrder()).toArray()))), "/messages.list endpoint error"); } diff --git a/docs/docs/guides/airy-core-in-test-env.md b/docs/docs/guides/airy-core-in-test-env.md index 4f5a377955..88f4314d03 100644 --- a/docs/docs/guides/airy-core-in-test-env.md +++ b/docs/docs/guides/airy-core-in-test-env.md @@ -98,7 +98,8 @@ local machine. You can see an example request to the API by running the The frontend UI for the demo app can be accessed through http://demo.airy. -The frontend UI for the Airy chat plugin can be accessed through http://chatplugin.airy/example.html. +The frontend UI for the Airy chat plugin can be accessed through +http://chatplugin.airy/example. ## Public webhooks diff --git a/docs/docs/index.md b/docs/docs/index.md index d654af9b53..042fd1d2ed 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -91,7 +91,7 @@ browser. This authenticates the chat plugin and enables you to send messages immediately: ``` -http://chatplugin.airy/example.html?channel_id= +http://chatplugin.airy/example?channel_id= ``` You can now type a message in the text box and send it 🎉 diff --git a/docs/docs/sources/chat-plugin.md b/docs/docs/sources/chat-plugin.md index f433bead39..b58220dd99 100644 --- a/docs/docs/sources/chat-plugin.md +++ b/docs/docs/sources/chat-plugin.md @@ -83,7 +83,7 @@ of your Chat Plugin server. When using the local vagrant environment ::: To test the setup, replace the `CHANNEL_ID` in the URL -`http://chatplugin.airy/example.html?channel_id=CHANNEL_ID` and open it in your +`http://chatplugin.airy/example?channel_id=CHANNEL_ID` and open it in your browser. ## HTTP API From f9823db9fe9cb64c9893fe4842d842307a97587c Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Tue, 26 Jan 2021 08:54:42 +0100 Subject: [PATCH 49/56] Fix numeric range queries (#730) * WIP * WIP * Subclass the query parser to allow for numeric queries Co-authored-by: lucapette --- .../ConversationsController.java | 14 +++-- .../airy/core/api/communication/Mapper.java | 2 +- .../airy/core/api/communication/Stores.java | 8 ++- .../api/communication/dto/Conversation.java | 2 +- .../communication/dto/ConversationIndex.java | 5 +- .../communication/lucene/DocumentMapper.java | 18 +++---- .../lucene/ExtendedQueryParser.java | 51 +++++++++++++++++++ .../communication/ConversationsListTest.java | 14 +++-- 8 files changed, 86 insertions(+), 28 deletions(-) create mode 100644 backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/ExtendedQueryParser.java diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/ConversationsController.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/ConversationsController.java index a283d04cbe..a5c63448f5 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/ConversationsController.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/ConversationsController.java @@ -1,6 +1,7 @@ package co.airy.core.api.communication; import co.airy.avro.communication.Metadata; +import co.airy.core.api.communication.lucene.ExtendedQueryParser; import co.airy.model.metadata.MetadataKeys; import co.airy.model.metadata.Subject; import co.airy.avro.communication.ReadReceipt; @@ -34,6 +35,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.List; +import java.util.Set; import static co.airy.model.metadata.MetadataRepository.newConversationTag; import static java.util.Comparator.comparing; @@ -43,10 +45,16 @@ public class ConversationsController { private final Stores stores; private final Mapper mapper; + private final ExtendedQueryParser queryParser; ConversationsController(Stores stores, Mapper mapper) { this.stores = stores; this.mapper = mapper; + this.queryParser = new ExtendedQueryParser(Set.of("unread_message_count"), + Set.of("created_at"), + "id", + new WhitespaceAnalyzer()); + this.queryParser.setAllowLeadingWildcard(true); } @PostMapping("/conversations.list") @@ -63,13 +71,9 @@ private ResponseEntity queryConversations(ConversationListRequestPayload requ final ReadOnlyLuceneStore conversationLuceneStore = stores.getConversationLuceneStore(); final ReadOnlyKeyValueStore conversationsStore = stores.getConversationsStore(); - final QueryParser simpleQueryParser = new QueryParser("id", new WhitespaceAnalyzer()); - // TODO Index display names more efficiently - simpleQueryParser.setAllowLeadingWildcard(true); - final Query query; try { - query = simpleQueryParser.parse(requestPayload.getFilters()); + query = queryParser.parse(requestPayload.getFilters()); } catch (ParseException e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(new RequestErrorResponsePayload("Failed to parse Lucene query: " + e.getMessage())); diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/Mapper.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/Mapper.java index 705c9c6184..ec914f15bd 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/Mapper.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/Mapper.java @@ -36,7 +36,7 @@ public ConversationResponsePayload fromConversation(Conversation conversation) { .source(conversation.getChannel().getSource()) .build()) .id(conversation.getId()) - .unreadMessageCount(conversation.getUnreadCount()) + .unreadMessageCount(conversation.getUnreadMessageCount()) .tags( MetadataRepository.filterPrefix(metadata, MetadataKeys.TAGS) .keySet() diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/Stores.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/Stores.java index f96d48e0cd..15c56269ca 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/Stores.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/Stores.java @@ -155,10 +155,8 @@ private void startStream() { return aggregate; }) - .join(channelTable, Conversation::getChannelId, (conversation, channel) -> { - conversation.setChannel(channel); - return conversation; - }) + .join(channelTable, Conversation::getChannelId, + (conversation, channel) -> conversation.toBuilder().channel(channel).build()) .leftJoin(metadataTable, (conversation, metadataMap) -> { if (metadataMap != null) { return conversation.toBuilder() @@ -169,7 +167,7 @@ private void startStream() { }) .leftJoin(unreadCountTable, (conversation, unreadCountState) -> { if (unreadCountState != null) { - conversation.setUnreadCount(unreadCountState.getUnreadCount()); + return conversation.toBuilder().unreadMessageCount(unreadCountState.getUnreadCount()).build(); } return conversation; }, Materialized.as(conversationsStore)) diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/dto/Conversation.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/dto/Conversation.java index 31f85edadf..4316714eda 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/dto/Conversation.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/dto/Conversation.java @@ -25,7 +25,7 @@ public class Conversation implements Serializable { private String sourceConversationId; private Channel channel; - private Integer unreadCount; + private Integer unreadMessageCount; @Builder.Default private Map metadata = new HashMap<>(); diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/dto/ConversationIndex.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/dto/ConversationIndex.java index d766746c81..48f8133520 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/dto/ConversationIndex.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/dto/ConversationIndex.java @@ -7,6 +7,7 @@ import java.io.Serializable; import java.util.HashMap; +import java.util.List; import java.util.Map; @Data @@ -19,7 +20,7 @@ public class ConversationIndex implements Serializable { private String channelId; private String source; private Long createdAt; - private Integer unreadCount; + private Integer unreadMessageCount; @Builder.Default private Map metadata = new HashMap<>(); @@ -32,7 +33,7 @@ public static ConversationIndex fromConversation(Conversation conversation) { .displayName(conversation.getDisplayNameOrDefault().toString()) .metadata(new HashMap<>(conversation.getMetadata())) .createdAt(conversation.getCreatedAt()) - .unreadCount(conversation.getUnreadCount()) + .unreadMessageCount(conversation.getUnreadMessageCount()) .build(); } } diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/DocumentMapper.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/DocumentMapper.java index 49309457a0..ac2d50e0d0 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/DocumentMapper.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/DocumentMapper.java @@ -1,7 +1,6 @@ package co.airy.core.api.communication.lucene; import co.airy.core.api.communication.dto.ConversationIndex; -import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.IntPoint; @@ -25,10 +24,10 @@ public Document fromConversationIndex(ConversationIndex conversation) { document.add(new TextField("display_name", conversation.getDisplayName(), Field.Store.YES)); } - document.add(new LongPoint("createdAt", conversation.getCreatedAt())); - document.add(new StoredField("createdAt", conversation.getCreatedAt())); - document.add(new IntPoint("unreadCount", conversation.getUnreadCount())); - document.add(new StoredField("unreadCount", conversation.getUnreadCount())); + document.add(new LongPoint("created_at", conversation.getCreatedAt())); + document.add(new StoredField("created_at", conversation.getCreatedAt())); + document.add(new IntPoint("unread_message_count", conversation.getUnreadMessageCount())); + document.add(new StoredField("unread_message_count", conversation.getUnreadMessageCount())); for (Map.Entry entry : conversation.getMetadata().entrySet()) { document.add(new TextField("metadata." + entry.getKey(), entry.getValue(), Field.Store.YES)); @@ -38,9 +37,8 @@ public Document fromConversationIndex(ConversationIndex conversation) { } public ConversationIndex fromDocument(Document document) { - - final Long createdAt = document.getField("createdAt").numericValue().longValue(); - final Integer unreadCount = document.getField("unreadCount").numericValue().intValue(); + final Long createdAt = document.getField("created_at").numericValue().longValue(); + final Integer unreadCount = document.getField("unread_message_count").numericValue().intValue(); final Map metadata = document.getFields().stream() .filter((field) -> field.name().startsWith("metadata")) @@ -51,10 +49,10 @@ public ConversationIndex fromDocument(Document document) { return ConversationIndex.builder() .id(document.get("id")) - .unreadCount(unreadCount) + .unreadMessageCount(unreadCount) .createdAt(createdAt) .metadata(metadata) - .displayName(document.get("displayName")) + .displayName(document.get("display_name")) .build(); } } diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/ExtendedQueryParser.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/ExtendedQueryParser.java new file mode 100644 index 0000000000..4ed39bfc8a --- /dev/null +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/ExtendedQueryParser.java @@ -0,0 +1,51 @@ +package co.airy.core.api.communication.lucene; + +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.document.IntPoint; +import org.apache.lucene.document.LongPoint; +import org.apache.lucene.queryparser.classic.QueryParser; +import org.apache.lucene.search.Query; + +import java.util.Set; + +public class ExtendedQueryParser extends QueryParser { + private final Set intFields; + private final Set longFields; + + public ExtendedQueryParser(Set intFields, + Set longFields, + String field, + Analyzer analyzer) { + super(field, analyzer); + this.intFields = intFields; + this.longFields = longFields; + } + + protected Query newRangeQuery(String field, String part1, String part2, boolean startInclusive, + boolean endInclusive) { + if (intFields.contains(field)) { + return IntPoint.newRangeQuery(field, Integer.parseInt(part1), getUpperIntBound(part2)); + } + if (longFields.contains(field)) { + return LongPoint.newRangeQuery(field, Long.parseLong(part1), getUpperLongBound(part2)); + } + + return super.newRangeQuery(field, part1, part2, startInclusive, endInclusive); + } + + private int getUpperIntBound(String part2) { + if (part2 == null) { + return Integer.MAX_VALUE; + } + + return Integer.parseInt(part2); + } + + private long getUpperLongBound(String part2) { + if (part2 == null) { + return Long.MAX_VALUE; + } + + return Integer.parseInt(part2); + } +} diff --git a/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsListTest.java b/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsListTest.java index 9f6a6e9bf0..e593eabf5f 100644 --- a/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsListTest.java +++ b/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsListTest.java @@ -29,8 +29,8 @@ import java.util.Map; import java.util.UUID; -import static co.airy.core.api.communication.util.Topics.getTopics; import static co.airy.core.api.communication.util.Topics.applicationCommunicationChannels; +import static co.airy.core.api.communication.util.Topics.getTopics; import static co.airy.test.Timing.retryOnException; import static java.util.Comparator.reverseOrder; import static java.util.stream.Collectors.toList; @@ -78,8 +78,8 @@ class ConversationsListTest { TestConversation.from(UUID.randomUUID().toString(), channelToFind, Map.of(MetadataKeys.Source.Contact.FIRST_NAME, firstNameToFind), 1), TestConversation.from(UUID.randomUUID().toString(), channelToFind, 1), TestConversation.from(conversationIdToFind, defaultChannel, 1), - TestConversation.from(UUID.randomUUID().toString(), defaultChannel, 1), - TestConversation.from(UUID.randomUUID().toString(), defaultChannel, 1) + TestConversation.from(UUID.randomUUID().toString(), defaultChannel, 2), + TestConversation.from(UUID.randomUUID().toString(), defaultChannel, 5) ); @@ -137,6 +137,12 @@ void canFilterByDisplayName() throws Exception { checkConversationsFound(payload, 1); } + @Test + void canFilterByUnreadMessageCount() throws Exception { + String payload = "{\"filters\": \"unread_message_count:[2 TO *]\"}"; + checkConversationsFound(payload, 2); + } + @Test void canFilterByCombinedQueries() throws Exception { final JsonNodeFactory jsonNodeFactory = JsonNodeFactory.instance; @@ -169,6 +175,6 @@ private void checkConversationsFound(String payload, int count) throws Interrupt .andExpect(jsonPath("$.data", hasSize(count))) .andExpect(jsonPath("response_metadata.filtered_total", is(count))) .andExpect(jsonPath("response_metadata.total", is(conversations.size()))), - "Expected one conversation returned"); + String.format("Expected %d conversation returned", count)); } } From cb83aa061377507274834956a198f2ac2a226439 Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Tue, 26 Jan 2021 09:29:36 +0100 Subject: [PATCH 50/56] [#623] Optional apps depend on config file (#719) --- docs/docs/guides/airy-core-in-production.md | 32 +++++---------- docs/docs/guides/airy-core-in-test-env.md | 16 ++++++-- docs/docs/index.md | 8 ++++ docs/docs/overview/architecture.md | 10 +++++ docs/docs/overview/release-process.md | 2 + infrastructure/Vagrantfile | 3 -- infrastructure/airy.tpl.yaml | 5 +-- infrastructure/cli/cmd/config/config.go | 4 ++ infrastructure/cli/cmd/config/configmaps.go | 28 ++++++++++--- infrastructure/cli/cmd/config/parser.go | 3 ++ .../charts/airy-config/templates/sources.yaml | 40 +------------------ .../templates/deployment.yaml | 2 +- .../frontend-demo/templates/deployment.yaml | 2 +- .../templates/deployment.yaml | 11 +++-- .../templates/deployment.yaml | 10 +++++ .../templates/deployment.yaml | 10 +++++ .../templates/deployment.yaml | 6 +++ .../templates/deployment.yaml | 6 +++ infrastructure/scripts/conf.sh | 37 ----------------- infrastructure/scripts/provision/core.sh | 12 +++++- infrastructure/scripts/trigger/start.sh | 7 +--- infrastructure/scripts/trigger/stop.sh | 1 + 22 files changed, 131 insertions(+), 124 deletions(-) delete mode 100755 infrastructure/scripts/conf.sh diff --git a/docs/docs/guides/airy-core-in-production.md b/docs/docs/guides/airy-core-in-production.md index 3fe2d405b8..989c8bedad 100644 --- a/docs/docs/guides/airy-core-in-production.md +++ b/docs/docs/guides/airy-core-in-production.md @@ -141,6 +141,10 @@ features. In order to proceed with deploying the apps, we assume that you have a running Kubernetes cluster, properly configured KUBECONF file and properly set context. +The Airy Core Platform ships with a Kubernetes controller, which is responsible for +starting and reloading the appropriate Airy apps based on the provided configuration. +The controller as a deployment named `airy-controller`. + ### Configuration After the [required services](#requirements) are deployed, you're ready to start @@ -196,21 +200,10 @@ We provided a Helm chart to deploy the `Airy apps`. Before you can run helm, you must configure the system via the `airy.yaml` file, then you can proceed: ```sh -cp airy.yaml ./helm-chart/charts/apps/values.yaml -helm install core ./helm-chart/charts/apps/ --timeout 1000s +helm install core ./helm-chart/charts/apps/ --values ./airy.yaml --timeout 1000s ``` -By default, the `Airy apps` deployments start with `replicas=0` so to scale them up, run: - -```sh -kubectl scale deployment -l type=api --replicas=1 -kubectl scale deployment -l type=frontend --replicas=1 -kubectl scale deployment -l type=webhook --replicas=1 -kubectl scale deployment -l type=sources-chatplugin --replicas=1 -kubectl scale deployment -l type=sources-facebook --replicas=1 -kubectl scale deployment -l type=sources-google --replicas=1 -kubectl scale deployment -l type=sources-twilio --replicas=1 -``` +The API `Airy apps`, the Frontend UI and the Frontend Chatplugin start by default, while all the other apps are optional and are started if there is provided configuration for them in the `airy.yaml` file. At this point you should have a running `Airy Core Platform` in your environment 🎉. @@ -218,18 +211,13 @@ If afterwards you need to modify or add other config parameters in the `airy.yaml` file, after editing the file run: ```sh -cp airy.yaml ./helm-chart/charts/apps/values.yaml -helm upgrade core ./helm-chart/charts/apps/ --timeout 1000s +airy config apply --config ./airy.yaml --kube-config /path/to/your/kube.conf ``` -If you deploy the Airy Core Platform with a specific version tag, you must -export the `AIRY_VERSION` variable before running `helm upgrade`: +Make sure you point the `--kube-config` flag to your Kubernetes configuration file. -```sh -cp airy.yaml ./helm-chart/charts/apps/values.yaml -export AIRY_VERSION=develop -helm upgrade core ./helm-chart/charts/apps/ --set global.appImageTag=${AIRY_VERSION} --timeout 1000s -``` +If you want to deploy the Airy Core Platform with a specific version, you must set the version in your +`airy.yaml` file, under the `global.appImageTag` configuration key. ## Network diff --git a/docs/docs/guides/airy-core-in-test-env.md b/docs/docs/guides/airy-core-in-test-env.md index 88f4314d03..7f38ee4162 100644 --- a/docs/docs/guides/airy-core-in-test-env.md +++ b/docs/docs/guides/airy-core-in-test-env.md @@ -139,7 +139,7 @@ document The bootstrap process creates a random URL which is then provisioned inside the Helm chart. To configure these URLs, you can specify them in the -`infrastructure/helm-chart/charts/apps/charts/airy-co)fig/values.yaml` document. +`infrastructure/helm-chart/charts/apps/charts/airy-config/values.yaml` document. Alternatively you can edit the `airy.yaml` file by setting the following parameter (see `airy.tpl.yaml` for more examples): @@ -165,12 +165,22 @@ configuration settings, refer to the source specific docs for details. You must provide the settings in `infrastructure/airy.yaml` configuration file. An example of the configuration can be found in `airy.tpl.yaml`. -After setting the configuration run: +After setting the configuration, you need the Airy command line binary (Airy CLI), to communicate with the core installation and apply the installation. +Building and releasing the Airy CLI is part of the regular release process of the Airy Core Platform. +You can download the Airy CLI from the releases page on Github https://github.com/airyhq/airy/releases. + +After downloading, run the following commands: ```sh -vagrant provision --provision-with=conf +airy init +airy apply config --config ./airy.yaml ``` +Make sure that the argument `` points to your `airy.yaml` configuration file. + +The Airy CLI considers that the kubernetes configuration file is located under `~/.airy/kube.conf`. +If you modified the location of the file, make sure to set the appropriate path with the `--kube-config` flag. + ## Uninstall the Airy Core Platform You can remove the Airy Core Platform Box from your machine completely running diff --git a/docs/docs/index.md b/docs/docs/index.md index 042fd1d2ed..c01ac13125 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -57,6 +57,14 @@ hosts file yourself. 192.168.50.5 chatplugin.airy ``` +After the bootstrap process finishes, it will download the Kubernetes configiration file to the local host machine under `~/.airy/kube.conf`. +That file is required for the Airy Command Line tool (Airy CLI), in order to access the Kubernetes cluster where the Airy Core Platform is running. +You can also use that configuration file with the `kubectl` utility, for example: + +```sh +kubectl --kubeconfig ~/.airy/kube.conf get pods +``` + Check out our [guide for running in test environment](guides/airy-core-in-test-env.md) for detailed information. ## Connect a Chat Plugin source diff --git a/docs/docs/overview/architecture.md b/docs/docs/overview/architecture.md index a7a6e4b20f..d68ccd1eb9 100644 --- a/docs/docs/overview/architecture.md +++ b/docs/docs/overview/architecture.md @@ -43,3 +43,13 @@ which run as part of the Airy Core Platform: - frontend-demo - Web application for viewing messages - frontend-chat-plugin - Web chat plugin + +## Airy Controller + +The Airy Core Platform ships with a Kubernetes controller, which is responsible for starting and reloading the appropriate Airy apps based on the provided configuration. +The controller as a deployment named `airy-controller`. + +## Airy CLI + +Every release features a command line binary, used to configure and fetch status information from the Airy Core Platform. +This tool is referred to as the `Airy CLI` throughout the documentation. diff --git a/docs/docs/overview/release-process.md b/docs/docs/overview/release-process.md index 3040c16b46..7f11157026 100644 --- a/docs/docs/overview/release-process.md +++ b/docs/docs/overview/release-process.md @@ -20,4 +20,6 @@ Here's an outline of the process: - We rename the current draft release to `x.y.z` and publish it - We announce the release! +As part of the release process we are also releasing a command line client - the `Airy CLI`. + You can check out existing releases on [GitHub](https://github.com/airyhq/airy/releases). diff --git a/infrastructure/Vagrantfile b/infrastructure/Vagrantfile index 91ea0dbf75..52f9e6cea1 100644 --- a/infrastructure/Vagrantfile +++ b/infrastructure/Vagrantfile @@ -20,9 +20,6 @@ Vagrant.configure("2") do |config| airy_core.vm.provision "core", type: "shell", env: {"AIRY_VERSION" => ENV['AIRY_VERSION']} do |c| c.inline = "/vagrant/scripts/provision/core.sh" end - airy_core.vm.provision "conf", type: "shell", env: {"AIRY_VERSION" => ENV['AIRY_VERSION']} do |u| - u.inline = "/vagrant/scripts/conf.sh" - end airy_core.trigger.before [:halt, :reload] do |stop| stop.name = "stop" stop.run_remote = {inline: "/vagrant/scripts/trigger/stop.sh"} diff --git a/infrastructure/airy.tpl.yaml b/infrastructure/airy.tpl.yaml index 9a36f174b5..c3d63dce1f 100644 --- a/infrastructure/airy.tpl.yaml +++ b/infrastructure/airy.tpl.yaml @@ -33,11 +33,10 @@ core: twilio: authToken: "changeme" accountSid: "changeme" - chatPlugin: - host: "http://chatplugin.airy" + webhooks: + name: "Airy-web" # Specific configuration for the API apps api: - host: "http://api.airy" mailFrom: "changeme" mailPort: 587 mailUrl: "changeme" diff --git a/infrastructure/cli/cmd/config/config.go b/infrastructure/cli/cmd/config/config.go index 904ca38881..ebf92fc2bc 100644 --- a/infrastructure/cli/cmd/config/config.go +++ b/infrastructure/cli/cmd/config/config.go @@ -37,6 +37,10 @@ func applyConfig(cmd *cobra.Command, args []string) { if googleApply(conf, kubeConfigFile) { fmt.Println("Google configuration applied.") } + + if webhooksApply(conf, kubeConfigFile) { + fmt.Println("Webhooks configuration applied.") + } } var applyConfigCmd = &cobra.Command{ diff --git a/infrastructure/cli/cmd/config/configmaps.go b/infrastructure/cli/cmd/config/configmaps.go index 65cc8ebcb1..b5a091496e 100644 --- a/infrastructure/cli/cmd/config/configmaps.go +++ b/infrastructure/cli/cmd/config/configmaps.go @@ -11,8 +11,7 @@ import ( "k8s.io/client-go/tools/clientcmd" ) -func applyConfigMap(source string, newCmData map[string]string, kubeConfigFile string, namespace string) error { - configMapName := "sources-" + source +func applyConfigMap(configMapName string, newCmData map[string]string, kubeConfigFile string, namespace string) error { config, kubeConfigErr := clientcmd.BuildConfigFromFlags("", kubeConfigFile) if kubeConfigErr != nil { return kubeConfigErr @@ -50,7 +49,7 @@ func facebookApply(airyConf airyConf, kubeConfigFile string) bool { configMapData["FACEBOOK_APP_ID"] = facebookConfig.AppID configMapData["FACEBOOK_APP_SECRET"] = facebookConfig.AppSecret configMapData["FACEBOOK_WEBHOOK_SECRET"] = facebookConfig.WebhookSecret - err := applyConfigMap("facebook", configMapData, kubeConfigFile, airyConf.Global.Namespace) + err := applyConfigMap("sources-facebook", configMapData, kubeConfigFile, airyConf.Global.Namespace) if err != nil { fmt.Println("unable to update configMap: ", err) @@ -70,7 +69,7 @@ func googleApply(airyConf airyConf, kubeConfigFile string) bool { configMapData["GOOGLE_PARTNER_KEY"] = googleConfig.PartnerKey configMapData["GOOGLE_SA_FILE"] = googleConfig.SaFile - err := applyConfigMap("google", configMapData, kubeConfigFile, airyConf.Global.Namespace) + err := applyConfigMap("sources-google", configMapData, kubeConfigFile, airyConf.Global.Namespace) if err != nil { fmt.Println("unable to update configMap: ", err) @@ -90,7 +89,26 @@ func twilioApply(airyConf airyConf, kubeConfigFile string) bool { configMapData["TWILIO_ACCOUNT_SID"] = twilioConfig.AccountSid configMapData["TWILIO_AUTH_TOKEN"] = twilioConfig.AuthToken - err := applyConfigMap("twilio", configMapData, kubeConfigFile, airyConf.Global.Namespace) + err := applyConfigMap("sources-twilio", configMapData, kubeConfigFile, airyConf.Global.Namespace) + + if err != nil { + fmt.Println("unable to update configMap: ", err) + os.Exit(1) + } + + return true + } + + return false +} + +func webhooksApply(airyConf airyConf, kubeConfigFile string) bool { + webhooksConfig := airyConf.Core.Apps.Webhooks + if webhooksConfig.Name != "" { + configMapData := make(map[string]string, 0) + configMapData["NAME"] = webhooksConfig.Name + + err := applyConfigMap("webhooks-config", configMapData, kubeConfigFile, airyConf.Global.Namespace) if err != nil { fmt.Println("unable to update configMap: ", err) diff --git a/infrastructure/cli/cmd/config/parser.go b/infrastructure/cli/cmd/config/parser.go index 1879f4a573..db95a16c8b 100644 --- a/infrastructure/cli/cmd/config/parser.go +++ b/infrastructure/cli/cmd/config/parser.go @@ -31,6 +31,9 @@ type coreConf struct { SaFile string `yaml:"saFile"` } } + Webhooks struct { + Name string `yaml:"name"` + } } } diff --git a/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/sources.yaml b/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/sources.yaml index 3450fc296e..382f90b348 100644 --- a/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/sources.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/airy-config/templates/sources.yaml @@ -1,40 +1,3 @@ -{{ $coreID := randAlphaNum 10 | lower }} -apiVersion: v1 -kind: ConfigMap -metadata: - name: sources-chatplugin - namespace: {{ .Values.global.namespace }} -data: - JWT_SECRET: {{ randAlphaNum 128 | quote }} ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: sources-facebook - namespace: {{ .Values.global.namespace }} -data: - FACEBOOK_APP_ID: {{ .Values.sources.facebook.appId | quote }} - FACEBOOK_APP_SECRET: {{ .Values.sources.facebook.appSecret | quote }} - FACEBOOK_WEBHOOK_SECRET: {{ .Values.sources.facebook.webhookSecret | quote }} ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: sources-google - namespace: {{ .Values.global.namespace }} -data: - GOOGLE_PARTNER_KEY: {{ .Values.sources.google.partnerKey }} - GOOGLE_SA_FILE: {{ .Values.sources.google.saFile }} ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: sources-twilio - namespace: {{ .Values.global.namespace }} -data: - TWILIO_AUTH_TOKEN: {{ .Values.sources.twilio.authToken }} - TWILIO_ACCOUNT_SID: {{ .Values.sources.twilio.accountSid }} ---- apiVersion: v1 kind: ConfigMap metadata: @@ -52,4 +15,5 @@ metadata: namespace: {{ .Values.global.namespace }} data: APP_IMAGE_TAG: {{ .Values.global.appImageTag }} - CORE_ID: {{ $coreID }} + CORE_ID: {{ randAlphaNum 10 | lower }} + CHATPLUGIN_JWT_SECRET: {{ randAlphaNum 128 | quote }} \ No newline at end of file diff --git a/infrastructure/helm-chart/charts/apps/charts/frontend-chat-plugin/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/frontend-chat-plugin/templates/deployment.yaml index 3c89f8e1c4..2416ffe035 100644 --- a/infrastructure/helm-chart/charts/apps/charts/frontend-chat-plugin/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/frontend-chat-plugin/templates/deployment.yaml @@ -7,7 +7,7 @@ metadata: app: frontend-chat-plugin type: frontend spec: - replicas: 0 + replicas: 1 selector: matchLabels: app: frontend-chat-plugin diff --git a/infrastructure/helm-chart/charts/apps/charts/frontend-demo/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/frontend-demo/templates/deployment.yaml index cca768d4e8..4ccf3f6022 100644 --- a/infrastructure/helm-chart/charts/apps/charts/frontend-demo/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/frontend-demo/templates/deployment.yaml @@ -7,7 +7,7 @@ metadata: app: frontend-demo type: frontend spec: - replicas: 0 + replicas: 1 selector: matchLabels: app: frontend-demo diff --git a/infrastructure/helm-chart/charts/apps/charts/sources-chatplugin/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/sources-chatplugin/templates/deployment.yaml index f9b2ef1b99..460cf26429 100644 --- a/infrastructure/helm-chart/charts/apps/charts/sources-chatplugin/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/sources-chatplugin/templates/deployment.yaml @@ -8,7 +8,7 @@ metadata: type: sources-chatplugin core.airy.co/managed: "true" spec: - replicas: 0 + replicas: 1 selector: matchLabels: app: sources-chatplugin @@ -35,8 +35,8 @@ spec: - name: JWT_SECRET valueFrom: configMapKeyRef: - name: sources-chatplugin - key: JWT_SECRET + name: core-config + key: CHATPLUGIN_JWT_SECRET - name: KAFKA_BROKERS valueFrom: configMapKeyRef: @@ -52,6 +52,11 @@ spec: configMapKeyRef: name: kafka-config key: KAFKA_COMMIT_INTERVAL_MS + - name: CHATPLUGIN_NAME + valueFrom: + configMapKeyRef: + name: sources-chatplugin + key: NAME livenessProbe: httpGet: path: /actuator/health diff --git a/infrastructure/helm-chart/charts/apps/charts/sources-google-events-router/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/sources-google-events-router/templates/deployment.yaml index 2e872a4496..94360c1d7d 100644 --- a/infrastructure/helm-chart/charts/apps/charts/sources-google-events-router/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/sources-google-events-router/templates/deployment.yaml @@ -42,6 +42,16 @@ spec: configMapKeyRef: name: kafka-config key: KAFKA_COMMIT_INTERVAL_MS + - name: GOOGLE_SA_FILE + valueFrom: + configMapKeyRef: + name: sources-google + key: GOOGLE_SA_FILE + - name: GOOGLE_PARTNER_KEY + valueFrom: + configMapKeyRef: + name: sources-google + key: GOOGLE_PARTNER_KEY livenessProbe: tcpSocket: port: 6000 diff --git a/infrastructure/helm-chart/charts/apps/charts/sources-twilio-events-router/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/sources-twilio-events-router/templates/deployment.yaml index c0245ab674..0e4f3be848 100644 --- a/infrastructure/helm-chart/charts/apps/charts/sources-twilio-events-router/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/sources-twilio-events-router/templates/deployment.yaml @@ -42,6 +42,16 @@ spec: configMapKeyRef: name: kafka-config key: KAFKA_COMMIT_INTERVAL_MS + - name: TWILIO_AUTH_TOKEN + valueFrom: + configMapKeyRef: + name: sources-twilio + key: TWILIO_AUTH_TOKEN + - name: TWILIO_ACCOUNT_SID + valueFrom: + configMapKeyRef: + name: sources-twilio + key: TWILIO_ACCOUNT_SID livenessProbe: tcpSocket: port: 6000 diff --git a/infrastructure/helm-chart/charts/apps/charts/webhook-consumer/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/webhook-consumer/templates/deployment.yaml index 53a468057b..ccc6ddc098 100644 --- a/infrastructure/helm-chart/charts/apps/charts/webhook-consumer/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/webhook-consumer/templates/deployment.yaml @@ -6,6 +6,7 @@ metadata: labels: app: webhook-consumer type: webhook + core.airy.co/managed: "true" spec: replicas: 0 selector: @@ -53,6 +54,11 @@ spec: configMapKeyRef: name: redis-config key: REDIS_PORT + - name: WEBHOOK_NAME + valueFrom: + configMapKeyRef: + name: webhooks-config + key: NAME livenessProbe: httpGet: path: /health diff --git a/infrastructure/helm-chart/charts/apps/charts/webhook-publisher/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/webhook-publisher/templates/deployment.yaml index d19f5e3069..f32870f51e 100644 --- a/infrastructure/helm-chart/charts/apps/charts/webhook-publisher/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/webhook-publisher/templates/deployment.yaml @@ -6,6 +6,7 @@ metadata: labels: app: webhook-publisher type: webhook + core.airy.co/managed: "true" spec: replicas: 0 selector: @@ -53,6 +54,11 @@ spec: configMapKeyRef: name: redis-config key: REDIS_PORT + - name: WEBHOOK_NAME + valueFrom: + configMapKeyRef: + name: webhooks-config + key: NAME livenessProbe: tcpSocket: port: 6000 diff --git a/infrastructure/scripts/conf.sh b/infrastructure/scripts/conf.sh deleted file mode 100755 index b9f977c6bd..0000000000 --- a/infrastructure/scripts/conf.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -set -eo pipefail -IFS=$'\n\t' - -SCRIPT_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P) -INFRASTRUCTURE_PATH=$(cd ${SCRIPT_PATH}/../; pwd -P) - -if [[ ! -f ${INFRASTRUCTURE_PATH}/airy.yaml ]]; then - echo "No airy.yaml config file found" - exit 0 -fi - -source ${INFRASTRUCTURE_PATH}/scripts/lib/k8s.sh - - -kubectl delete pod startup-helper --force 2>/dev/null || true -kubectl run startup-helper --image busybox --command -- /bin/sh -c "tail -f /dev/null" - -helm upgrade core ${INFRASTRUCTURE_PATH}/helm-chart/ --values ${INFRASTRUCTURE_PATH}/airy.yaml --timeout 1000s > /dev/null 2>&1 - -kubectl scale deployment schema-registry --replicas=1 - -wait-for-running-pod startup-helper -wait-for-service startup-helper schema-registry 8081 15 "Schema registry" - -kubectl scale deployment -l type=api --replicas=1 -kubectl scale deployment -l type=sources-chatplugin --replicas=1 -kubectl scale deployment -l type=frontend --replicas=1 - -wait-for-service startup-helper api-auth 80 10 api-auth - -kubectl scale deployment -l type=sources-twilio --replicas=1 -kubectl scale deployment -l type=sources-google --replicas=1 -kubectl scale deployment -l type=sources-facebook --replicas=1 -kubectl scale deployment -l type=webhook --replicas=1 - -kubectl delete pod startup-helper --force 2>/dev/null diff --git a/infrastructure/scripts/provision/core.sh b/infrastructure/scripts/provision/core.sh index afcb7fad6d..6a64859f18 100755 --- a/infrastructure/scripts/provision/core.sh +++ b/infrastructure/scripts/provision/core.sh @@ -2,6 +2,7 @@ set -euo pipefail IFS=$'\n\t' +AIRY_VERSION=${AIRY_VERSION} SCRIPT_PATH=$(cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P) INFRASTRUCTURE_PATH=$(cd ${SCRIPT_PATH}/../../; pwd -P) @@ -16,10 +17,17 @@ echo "Deploying the Airy Core Platform with the ${AIRY_VERSION} image tag" if [[ -f ${INFRASTRUCTURE_PATH}/airy.yaml ]]; then yq eval '.global.appImageTag="'${AIRY_VERSION}'"' -i ${INFRASTRUCTURE_PATH}/airy.yaml + helm install core ${INFRASTRUCTURE_PATH}/helm-chart/ --values ${INFRASTRUCTURE_PATH}/airy.yaml --timeout 1000s > /dev/null 2>&1 + wget -qnv https://airy-core-binaries.s3.amazonaws.com/alpine/airy.gz + gunzip airy.gz + chmod +x airy + mv airy /usr/local/bin/ + airy init + airy config apply --kube-config /etc/rancher/k3s/k3s.yaml --config ${INFRASTRUCTURE_PATH}/airy.yaml +else + helm install core ${INFRASTRUCTURE_PATH}/helm-chart/ --set global.appImageTag=${AIRY_VERSION} --timeout 1000s > /dev/null 2>&1 fi -helm install core ${INFRASTRUCTURE_PATH}/helm-chart/ --set global.appImageTag=${AIRY_VERSION} --version 0.5.0 --timeout 1000s > /dev/null 2>&1 - kubectl run startup-helper --image busybox --command -- /bin/sh -c "tail -f /dev/null" wait-for-running-pod startup-helper diff --git a/infrastructure/scripts/trigger/start.sh b/infrastructure/scripts/trigger/start.sh index 7956bad5f0..fa715e4447 100755 --- a/infrastructure/scripts/trigger/start.sh +++ b/infrastructure/scripts/trigger/start.sh @@ -24,15 +24,10 @@ wait-for-service startup-helper schema-registry 8081 15 "Schema registry" echo "Starting up Airy Core Platform appplications" kubectl scale deployment -l type=api --replicas=1 -kubectl scale deployment -l type=sources-chatplugin --replicas=1 -kubectl scale deployment -l type=frontend --replicas=1 wait-for-service startup-helper api-auth 80 10 api-auth -kubectl scale deployment -l type=sources-twilio --replicas=1 -kubectl scale deployment -l type=sources-google --replicas=1 -kubectl scale deployment -l type=sources-facebook --replicas=1 -kubectl scale deployment -l type=webhook --replicas=1 +kubectl scale deployment -l app=airy-controller --replicas=1 kubectl delete pod startup-helper --force 2>/dev/null chmod o+r /etc/rancher/k3s/k3s.yaml diff --git a/infrastructure/scripts/trigger/stop.sh b/infrastructure/scripts/trigger/stop.sh index 08bea932bb..587f8bd514 100755 --- a/infrastructure/scripts/trigger/stop.sh +++ b/infrastructure/scripts/trigger/stop.sh @@ -3,6 +3,7 @@ set -euo pipefail IFS=$'\n\t' echo "Scaling down Airy Core platform applications" +kubectl scale deployment -l app=airy-controller --replicas=0 kubectl scale deployment -l type=frontend --replicas=0 kubectl scale deployment -l type=sources-twilio --replicas=0 kubectl scale deployment -l type=sources-google --replicas=0 From ae8858352cbb84ea1e906b7f895073561aa2aac1 Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Tue, 26 Jan 2021 09:57:49 +0100 Subject: [PATCH 51/56] [#623] Fix manifest for the chatpluign (#732) --- .../apps/charts/sources-chatplugin/templates/deployment.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/infrastructure/helm-chart/charts/apps/charts/sources-chatplugin/templates/deployment.yaml b/infrastructure/helm-chart/charts/apps/charts/sources-chatplugin/templates/deployment.yaml index 460cf26429..7364879de5 100644 --- a/infrastructure/helm-chart/charts/apps/charts/sources-chatplugin/templates/deployment.yaml +++ b/infrastructure/helm-chart/charts/apps/charts/sources-chatplugin/templates/deployment.yaml @@ -52,11 +52,6 @@ spec: configMapKeyRef: name: kafka-config key: KAFKA_COMMIT_INTERVAL_MS - - name: CHATPLUGIN_NAME - valueFrom: - configMapKeyRef: - name: sources-chatplugin - key: NAME livenessProbe: httpGet: path: /actuator/health From 300c25c126096a494ba26ec0cae2c59578b634f1 Mon Sep 17 00:00:00 2001 From: Christoph Proeschel Date: Tue, 26 Jan 2021 10:36:54 +0100 Subject: [PATCH 52/56] [#692] Enable exact numeric queries (#734) Fixes #692 --- .../communication/lucene/ExtendedQueryParser.java | 12 ++++++++++++ .../api/communication/ConversationsListTest.java | 8 +++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/ExtendedQueryParser.java b/backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/ExtendedQueryParser.java index 4ed39bfc8a..b73c980025 100644 --- a/backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/ExtendedQueryParser.java +++ b/backend/api/communication/src/main/java/co/airy/core/api/communication/lucene/ExtendedQueryParser.java @@ -3,6 +3,7 @@ import org.apache.lucene.analysis.Analyzer; import org.apache.lucene.document.IntPoint; import org.apache.lucene.document.LongPoint; +import org.apache.lucene.queryparser.classic.ParseException; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.Query; @@ -21,6 +22,17 @@ public ExtendedQueryParser(Set intFields, this.longFields = longFields; } + protected Query getFieldQuery(String field, String queryText, boolean quoted) throws ParseException { + if (intFields.contains(field)) { + return IntPoint.newExactQuery(field, Integer.parseInt(queryText)); + } + if (longFields.contains(field)) { + return LongPoint.newExactQuery(field, Long.parseLong(queryText)); + } + + return super.getFieldQuery(field, queryText, quoted); + } + protected Query newRangeQuery(String field, String part1, String part2, boolean startInclusive, boolean endInclusive) { if (intFields.contains(field)) { diff --git a/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsListTest.java b/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsListTest.java index e593eabf5f..f315b51f3d 100644 --- a/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsListTest.java +++ b/backend/api/communication/src/test/java/co/airy/core/api/communication/ConversationsListTest.java @@ -138,11 +138,17 @@ void canFilterByDisplayName() throws Exception { } @Test - void canFilterByUnreadMessageCount() throws Exception { + void canFilterByUnreadMessageCountRange() throws Exception { String payload = "{\"filters\": \"unread_message_count:[2 TO *]\"}"; checkConversationsFound(payload, 2); } + @Test + void canFilterByUnreadMessageCount() throws Exception { + String payload = "{\"filters\": \"unread_message_count:2\"}"; + checkConversationsFound(payload, 1); + } + @Test void canFilterByCombinedQueries() throws Exception { final JsonNodeFactory jsonNodeFactory = JsonNodeFactory.instance; From cd820c94d147d7bb3b53980843114883ae315735 Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Tue, 26 Jan 2021 10:43:43 +0100 Subject: [PATCH 53/56] [#623] Fix Google safile sample config (#736) --- infrastructure/airy.tpl.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/infrastructure/airy.tpl.yaml b/infrastructure/airy.tpl.yaml index c3d63dce1f..b5efdf06cf 100644 --- a/infrastructure/airy.tpl.yaml +++ b/infrastructure/airy.tpl.yaml @@ -28,8 +28,7 @@ core: webhookSecret: "changeme" google: partnerKey: "changeme" - saFile: > - '{"type":"service_account","project_id":"airy","private_key_id":"no","private_key":"nokey","client_email":"no","client_id":"no","auth_uri":"no","token_uri":"no","no":"no","client_x509_cert_url":"no"}' + saFile: '{"type":"service_account","project_id":"airy","private_key_id":"no","private_key":"nokey","client_email":"no","client_id":"no","auth_uri":"no","token_uri":"no","no":"no","client_x509_cert_url":"no"}' twilio: authToken: "changeme" accountSid: "changeme" From 4d058a16f1afd1716c8b392091b725af794332fd Mon Sep 17 00:00:00 2001 From: Pascal Holy Date: Tue, 26 Jan 2021 12:08:04 +0100 Subject: [PATCH 54/56] Fixes #737 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 2f35653bc3..a918a2aa18 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.0.alpha +0.6.0 From fbe00dd145497c707929ba2e61d70b64b6e21b9d Mon Sep 17 00:00:00 2001 From: Pascal Holy Date: Tue, 26 Jan 2021 12:24:53 +0100 Subject: [PATCH 55/56] Fix version path --- scripts/push-images.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/push-images.sh b/scripts/push-images.sh index d236688b06..fad4a83c7e 100755 --- a/scripts/push-images.sh +++ b/scripts/push-images.sh @@ -13,7 +13,7 @@ case ${BRANCH_TARGET} in ;; main|release) - tag=`(cat ../VERSION)` + tag=$(cat ./VERSION) ;; esac From edd7b5b533ed71c9746714d5570ec305072da0b1 Mon Sep 17 00:00:00 2001 From: Pascal Holy Date: Tue, 26 Jan 2021 13:24:41 +0100 Subject: [PATCH 56/56] [#737] Fix release tag --- scripts/push-images.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/push-images.sh b/scripts/push-images.sh index fad4a83c7e..d170851184 100755 --- a/scripts/push-images.sh +++ b/scripts/push-images.sh @@ -13,7 +13,7 @@ case ${BRANCH_TARGET} in ;; main|release) - tag=$(cat ./VERSION) + tag="release" ;; esac