From 7c12413979ab74a4ceb9aa87b64c43179f8743f8 Mon Sep 17 00:00:00 2001 From: ljupcovangelski Date: Thu, 6 Jul 2023 20:41:36 +0200 Subject: [PATCH 01/37] Bump version to 0.55.0-alpha --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 524456c77..b93005464 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.54.0 +0.55.0-alpha From 71fd9f5068786c7571c83e5a16dbf1629630919e Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Thu, 13 Jul 2023 12:51:37 +0200 Subject: [PATCH 02/37] [#4108] Fix schemas and topics screens (#4109) * [#4108] Start Rest Karapace by default * Update docs for custom partitions * Add Kafka proxy to the airy-controller * Fix lint * Fix Helm test --- .../docs/getting-started/installation/helm.md | 10 ++++ infrastructure/controller/pkg/endpoints/BUILD | 1 + .../controller/pkg/endpoints/proxy.go | 58 +++++++++++++++++++ .../controller/pkg/endpoints/server.go | 6 ++ .../schema-registry/templates/service.yaml | 4 ++ .../kafka/charts/schema-registry/values.yaml | 2 +- .../templates/components/ingress.yaml | 2 +- 7 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 infrastructure/controller/pkg/endpoints/proxy.go diff --git a/docs/docs/getting-started/installation/helm.md b/docs/docs/getting-started/installation/helm.md index 03dcc7038..e47ed9b44 100644 --- a/docs/docs/getting-started/installation/helm.md +++ b/docs/docs/getting-started/installation/helm.md @@ -290,6 +290,16 @@ Run the following command to create the `Airy` platform without the bundled inst helm install airy airy/airy --timeout 10m --set prerequisites.kafka.enabled=false --values ./airy.yaml ``` +### Kafka partitions per topic + +Currently all the default topics in the Airy instance are created with 10 partitions. To create these topics with a different number of partitions, add the following to your `airy.yaml` file before running `helm install` (before the initial creation of the topics): + +``` +provisioning: + kafka: + partitions: 2 +``` + ### Beanstalkd The default installation creates its own [Beanstalkd](https://beanstalkd.github.io/) deployment, as it is a prerequisite for using the `integration/webhook` component. diff --git a/infrastructure/controller/pkg/endpoints/BUILD b/infrastructure/controller/pkg/endpoints/BUILD index fdcbf7203..c8705df73 100644 --- a/infrastructure/controller/pkg/endpoints/BUILD +++ b/infrastructure/controller/pkg/endpoints/BUILD @@ -12,6 +12,7 @@ go_library( "components_list.go", "components_update.go", "cors.go", + "proxy.go", "server.go", "services.go", ], diff --git a/infrastructure/controller/pkg/endpoints/proxy.go b/infrastructure/controller/pkg/endpoints/proxy.go new file mode 100644 index 000000000..8dc0f90af --- /dev/null +++ b/infrastructure/controller/pkg/endpoints/proxy.go @@ -0,0 +1,58 @@ +package endpoints + +import ( + "log" + "net/http" + "net/http/httputil" + "net/url" + "strings" + + "k8s.io/client-go/kubernetes" + "k8s.io/helm/cmd/helm/search" +) + +type proxyTarget struct { + name string + url string + stripUri string +} + +type KafkaSubjects struct { + ClientSet *kubernetes.Clientset + Namespace string + Index *search.Index +} + +type KafkaTopics struct { + ClientSet *kubernetes.Clientset + Namespace string + Index *search.Index +} + +func (s *KafkaSubjects) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var proxyUpstream = proxyTarget{"subjects", "http://schema-registry:8081/subjects", "/kafka/subjects"} + proxyRequest(w, r, proxyUpstream) +} + +func (s *KafkaTopics) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var proxyUpstream = proxyTarget{"subjects", "http://schema-registry:8082/topics", "/kafka/topics"} + proxyRequest(w, r, proxyUpstream) +} + +func proxyRequest(w http.ResponseWriter, r *http.Request, proxyUpstream proxyTarget) { + target, err := url.Parse(proxyUpstream.url) + if err != nil { + log.Printf("Error parsing target URL: %v\n", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + proxy := httputil.NewSingleHostReverseProxy(target) + r.URL.Host = target.Host + r.URL.Scheme = target.Scheme + r.Header.Set("X-Forwarded-Host", r.Header.Get("Host")) + r.Host = target.Host + r.URL.Path = strings.TrimPrefix(r.URL.Path, proxyUpstream.stripUri) + + proxy.ServeHTTP(w, r) +} diff --git a/infrastructure/controller/pkg/endpoints/server.go b/infrastructure/controller/pkg/endpoints/server.go index 27e24c426..b3c68443f 100644 --- a/infrastructure/controller/pkg/endpoints/server.go +++ b/infrastructure/controller/pkg/endpoints/server.go @@ -70,6 +70,12 @@ func Serve(clientSet *kubernetes.Clientset, namespace string, kubeConfig *rest.C componentsList := ComponentsList{ClientSet: clientSet, Namespace: namespace, Index: helmIndex} r.Handle("/components.list", &componentsList) + kafkaSubjects := KafkaSubjects{ClientSet: clientSet, Namespace: namespace, Index: helmIndex} + r.Handle("/kafka/subjects", &kafkaSubjects) + + kafkaTopics := KafkaTopics{ClientSet: clientSet, Namespace: namespace, Index: helmIndex} + r.Handle("/kafka/topics", &kafkaTopics) + log.Fatal(http.ListenAndServe(":8080", r)) } diff --git a/infrastructure/helm-chart/charts/prerequisites/charts/kafka/charts/schema-registry/templates/service.yaml b/infrastructure/helm-chart/charts/prerequisites/charts/kafka/charts/schema-registry/templates/service.yaml index 7d17a5bd9..52c14795e 100644 --- a/infrastructure/helm-chart/charts/prerequisites/charts/kafka/charts/schema-registry/templates/service.yaml +++ b/infrastructure/helm-chart/charts/prerequisites/charts/kafka/charts/schema-registry/templates/service.yaml @@ -9,6 +9,10 @@ spec: ports: - name: schema-registry port: {{ .Values.servicePort }} +{{- if .Values.restEnabled }} + - name: rest-api + port: {{ .Values.restPort }} +{{- end}} selector: app: schema-registry release: {{ .Release.Name }} diff --git a/infrastructure/helm-chart/charts/prerequisites/charts/kafka/charts/schema-registry/values.yaml b/infrastructure/helm-chart/charts/prerequisites/charts/kafka/charts/schema-registry/values.yaml index 3f89f02c9..d95dfcad3 100644 --- a/infrastructure/helm-chart/charts/prerequisites/charts/kafka/charts/schema-registry/values.yaml +++ b/infrastructure/helm-chart/charts/prerequisites/charts/kafka/charts/schema-registry/values.yaml @@ -9,4 +9,4 @@ kafka: bootstrapServers: kafka-headless:9092 minBrokers: 1 resources: {} -restEnabled: false \ No newline at end of file +restEnabled: true \ No newline at end of file diff --git a/infrastructure/helm-chart/templates/components/ingress.yaml b/infrastructure/helm-chart/templates/components/ingress.yaml index b4ddcdb3d..5a65593cd 100644 --- a/infrastructure/helm-chart/templates/components/ingress.yaml +++ b/infrastructure/helm-chart/templates/components/ingress.yaml @@ -82,7 +82,7 @@ spec: pathType: Prefix backend: service: - name: api-admin + name: airy-controller port: number: 80 - path: /kafka From 4cb61ff9c4e83a6d05c39d0d7d5b3b83a559f661 Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Fri, 25 Aug 2023 01:14:47 +0200 Subject: [PATCH 03/37] Docs - Remove Solutions link (#4115) --- docs/docusaurus.config.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 1f1fbae64..84987c390 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -66,12 +66,6 @@ module.exports = { position: 'left', to: 'https://airy.co/docs/enterprise/', }, - { - target: '_self', - label: 'Solutions', - position: 'left', - href: 'https://airy.co/solutions', - }, { target: '_self', label: 'Customer Stories', From f5ffce380c62c88a11738bb97071b8051a3d0c3e Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Mon, 11 Sep 2023 11:53:51 +0200 Subject: [PATCH 04/37] Update icons for LLM components (#4120) Co-authored-by: Aitor Algorta --- .../src/components/ChannelAvatar/index.tsx | 34 ++++ lib/typescript/assets/images/icons/chroma.svg | 44 +++++ lib/typescript/assets/images/icons/gmail.svg | 7 + lib/typescript/assets/images/icons/meta.svg | 1 + lib/typescript/assets/images/icons/mosaic.svg | 44 +++++ lib/typescript/assets/images/icons/openai.svg | 2 + .../assets/images/icons/pinecone.svg | 162 ++++++++++++++++++ .../assets/images/icons/weaviate.svg | 44 +++++ lib/typescript/model/Connectors.ts | 1 + lib/typescript/model/Source.ts | 13 +- 10 files changed, 350 insertions(+), 2 deletions(-) create mode 100644 lib/typescript/assets/images/icons/chroma.svg create mode 100644 lib/typescript/assets/images/icons/gmail.svg create mode 100644 lib/typescript/assets/images/icons/meta.svg create mode 100644 lib/typescript/assets/images/icons/mosaic.svg create mode 100644 lib/typescript/assets/images/icons/openai.svg create mode 100644 lib/typescript/assets/images/icons/pinecone.svg create mode 100644 lib/typescript/assets/images/icons/weaviate.svg diff --git a/frontend/control-center/src/components/ChannelAvatar/index.tsx b/frontend/control-center/src/components/ChannelAvatar/index.tsx index 7a26ed24f..16b05c6da 100644 --- a/frontend/control-center/src/components/ChannelAvatar/index.tsx +++ b/frontend/control-center/src/components/ChannelAvatar/index.tsx @@ -19,6 +19,13 @@ import {ReactComponent as IbmWatsonAssistantAvatar} from 'assets/images/icons/ib import {ReactComponent as RedisAvatar} from 'assets/images/icons/redisLogo.svg'; import {ReactComponent as PostgresAvatar} from 'assets/images/icons/postgresLogo.svg'; import {ReactComponent as FeastAvatar} from 'assets/images/icons/feastLogo.svg'; +import {ReactComponent as MetaAvatar} from 'assets/images/icons/meta.svg'; +import {ReactComponent as OpenaiAvatar} from 'assets/images/icons/openai.svg'; +import {ReactComponent as PineconeAvatar} from 'assets/images/icons/pinecone.svg'; +import {ReactComponent as ChromaAvatar} from 'assets/images/icons/chroma.svg'; +import {ReactComponent as MosaicAvatar} from 'assets/images/icons/mosaic.svg'; +import {ReactComponent as WeaviateAvatar} from 'assets/images/icons/weaviate.svg'; +import {ReactComponent as GmailAvatar} from 'assets/images/icons/gmail.svg'; import {Channel, Source} from 'model'; import styles from './index.module.scss'; @@ -98,6 +105,33 @@ export const getChannelAvatar = (source: string) => { case Source.feast: case 'Feast': return ; + case Source.faiss: + case 'FAISS': + return ; + case Source.faissConnector: + case 'FAISS connector': + return ; + case Source.llama2: + case 'LLama2': + return ; + case Source.openaiConnector: + case 'OpenAI connector': + return ; + case Source.pineconeConnector: + case 'Pinecone': + return ; + case Source.chroma: + case 'Chroma': + return ; + case Source.mosaic: + case 'Mosaic': + return ; + case Source.weaviate: + case 'Weaviate': + return ; + case Source.gmail: + case 'GMail connector': + return ; default: return ; diff --git a/lib/typescript/assets/images/icons/chroma.svg b/lib/typescript/assets/images/icons/chroma.svg new file mode 100644 index 000000000..c35b980d0 --- /dev/null +++ b/lib/typescript/assets/images/icons/chroma.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/typescript/assets/images/icons/gmail.svg b/lib/typescript/assets/images/icons/gmail.svg new file mode 100644 index 000000000..40b7175c1 --- /dev/null +++ b/lib/typescript/assets/images/icons/gmail.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/lib/typescript/assets/images/icons/meta.svg b/lib/typescript/assets/images/icons/meta.svg new file mode 100644 index 000000000..72316de7c --- /dev/null +++ b/lib/typescript/assets/images/icons/meta.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/lib/typescript/assets/images/icons/mosaic.svg b/lib/typescript/assets/images/icons/mosaic.svg new file mode 100644 index 000000000..0d1b99509 --- /dev/null +++ b/lib/typescript/assets/images/icons/mosaic.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/typescript/assets/images/icons/openai.svg b/lib/typescript/assets/images/icons/openai.svg new file mode 100644 index 000000000..3b4eff961 --- /dev/null +++ b/lib/typescript/assets/images/icons/openai.svg @@ -0,0 +1,2 @@ + +OpenAI icon \ No newline at end of file diff --git a/lib/typescript/assets/images/icons/pinecone.svg b/lib/typescript/assets/images/icons/pinecone.svg new file mode 100644 index 000000000..2b61be24e --- /dev/null +++ b/lib/typescript/assets/images/icons/pinecone.svg @@ -0,0 +1,162 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/typescript/assets/images/icons/weaviate.svg b/lib/typescript/assets/images/icons/weaviate.svg new file mode 100644 index 000000000..7eac0ba99 --- /dev/null +++ b/lib/typescript/assets/images/icons/weaviate.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/typescript/model/Connectors.ts b/lib/typescript/model/Connectors.ts index 68bb055a5..1c9b3d8e9 100644 --- a/lib/typescript/model/Connectors.ts +++ b/lib/typescript/model/Connectors.ts @@ -24,6 +24,7 @@ export enum ConnectorName { sourcesViber = 'sources-viber', rasaConnector = 'rasa-connector', zendenkConnector = 'zendesk-connector', + faissConnector = 'faiss-connector', } export enum InstallationStatus { diff --git a/lib/typescript/model/Source.ts b/lib/typescript/model/Source.ts index 2945297ff..dab71c881 100644 --- a/lib/typescript/model/Source.ts +++ b/lib/typescript/model/Source.ts @@ -22,6 +22,15 @@ export enum Source { redis = 'redis', postgresql = 'postgresql', feast = 'feast', + faiss = 'faiss', + faissConnector = 'faissConnector', + llama2 = 'llama2', + openaiConnector = 'openaiConnector', + pineconeConnector = 'pineconeConnector', + chroma = 'chroma', + mosaic = 'mosaic', + weaviate = 'weaviate', + gmail = 'gmail', amazons3 = 'amazons3', amazonLexV2 = 'amazonLexV2', integrationSourceApi = 'integrationSourceApi', @@ -30,14 +39,14 @@ export enum Source { export enum SourceApps { redis = 'redis', postgresql = 'postgresql', - feast = 'feast', + faiss = 'faiss', } export const isApp = (source: string): boolean => { switch (source) { case SourceApps.postgresql: case SourceApps.redis: - case SourceApps.feast: + case SourceApps.faiss: return true; } return false; From 06dd4021ca18ef170a6b24cc1391fc42d6ebfb63 Mon Sep 17 00:00:00 2001 From: Aitor Algorta Date: Mon, 11 Sep 2023 14:59:24 +0200 Subject: [PATCH 05/37] Fix symbol in stream fields (#4122) --- frontend/control-center/src/pages/Streams/Creation/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/control-center/src/pages/Streams/Creation/index.tsx b/frontend/control-center/src/pages/Streams/Creation/index.tsx index d929c8f36..62ac8849d 100644 --- a/frontend/control-center/src/pages/Streams/Creation/index.tsx +++ b/frontend/control-center/src/pages/Streams/Creation/index.tsx @@ -112,7 +112,7 @@ const Creation = (props: ListModeProps) => { fields.forEach(field => { formatedFields.push({ name: field['name'], - newName: field['name'] + '-' + topic, + newName: field['name'] + '_' + topic, }); }); return formatedFields; From e4baba2e55c69a38913fe0d59a930a2b6ddee58a Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Mon, 11 Sep 2023 18:28:21 +0200 Subject: [PATCH 06/37] Fix selecting fields in streams (#4123) --- .github/workflows/main.yml | 5 ----- .../control-center/src/pages/Streams/Creation/index.tsx | 6 ++++++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 11d1fbb6f..9348a3f58 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -64,11 +64,6 @@ jobs: echo ${{ secrets.PAT }} | docker login ghcr.io -u airydevci --password-stdin ./scripts/push-images.sh - - name: Install aws cli - uses: chrislennon/action-aws-cli@v1.1 - env: - ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' - - name: Upload airy binary to S3 if: ${{ github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/heads/release') || github.ref == 'refs/heads/main' }} run: | diff --git a/frontend/control-center/src/pages/Streams/Creation/index.tsx b/frontend/control-center/src/pages/Streams/Creation/index.tsx index 62ac8849d..378a0bc8d 100644 --- a/frontend/control-center/src/pages/Streams/Creation/index.tsx +++ b/frontend/control-center/src/pages/Streams/Creation/index.tsx @@ -203,6 +203,7 @@ const Creation = (props: ListModeProps) => { backgroundColor: 'transparent', border: '1px solid gray', borderRadius: '10px', + pointerEvents: 'none', }} /> @@ -232,6 +233,7 @@ const Creation = (props: ListModeProps) => { backgroundColor: 'transparent', border: '1px solid gray', borderRadius: '10px', + pointerEvents: 'none', }} /> @@ -281,6 +283,7 @@ const Creation = (props: ListModeProps) => { backgroundColor: 'transparent', border: '1px solid gray', borderRadius: '10px', + pointerEvents: 'none', }} /> @@ -312,6 +315,7 @@ const Creation = (props: ListModeProps) => { backgroundColor: 'transparent', border: '1px solid gray', borderRadius: '10px', + pointerEvents: 'none', }} /> @@ -394,6 +398,7 @@ const Creation = (props: ListModeProps) => { backgroundColor: 'transparent', border: '1px solid gray', borderRadius: '10px', + pointerEvents: 'none', }} /> @@ -437,6 +442,7 @@ const Creation = (props: ListModeProps) => { backgroundColor: 'transparent', border: '1px solid gray', borderRadius: '10px', + pointerEvents: 'none', }} /> From 65e098c94b5c5a523ffc35c10d2c8dacd8a7ddbb Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Wed, 20 Sep 2023 11:51:40 +0200 Subject: [PATCH 07/37] Add LLM filter in Catalog (#4125) --- .../components/general/FilterBar/FilterDropdown/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/typescript/components/general/FilterBar/FilterDropdown/index.tsx b/lib/typescript/components/general/FilterBar/FilterDropdown/index.tsx index c81007e33..376fc885e 100644 --- a/lib/typescript/components/general/FilterBar/FilterDropdown/index.tsx +++ b/lib/typescript/components/general/FilterBar/FilterDropdown/index.tsx @@ -10,6 +10,7 @@ const filterAttributes = [ 'Customer Service', 'Machine Learning', 'Conversational AI', + 'LLM', 'Databases', 'Marketing', 'Storage', From 839690e5e41670649f3cca229401e481a43e28ec Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Wed, 20 Sep 2023 16:34:40 +0200 Subject: [PATCH 08/37] Add description for components (#4126) --- lib/typescript/translations/translations.ts | 40 +++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/lib/typescript/translations/translations.ts b/lib/typescript/translations/translations.ts index b3d5271e5..b3743bf1f 100644 --- a/lib/typescript/translations/translations.ts +++ b/lib/typescript/translations/translations.ts @@ -4,6 +4,16 @@ import {initReactI18next} from 'react-i18next'; const resources = { en: { translation: { + //Components + chromaDescription: 'Chroma is a vector database that helps LLM applications to have long term memory.', + faissDescription: 'FAISS is a vector database that allows developers to quickly group and search embeddings of documents that are similar to each other.', + llama2Description: 'LLama2 is a large language model that can be downloaded and deployed in a self-hosted environment.', + mosaicDescription: 'Mosaic is an AI tool to easily train and deploy generative AI models on your data, in your secure environment.', + pineconeDescription: 'Pinecone is a fully managed vector database solution, used to store and search vector embeddings.', + weaviateDescription: 'Weaviate is an open-source vector database that allows storing object and vector embeddings from various ML-models.', + openaiDescription: 'The OpenAI connector is a bidirectional connector that connects Airy to the OpenAI LLM.', + gmailDescription: 'The GMail connector is a bidirectional connector for sending and receiving e-mail messages through the Google Mail API.', + //Input Component fieldCannotBeEmpty: 'This field cannot be empty.', invalidURL: 'The URL is invalid', @@ -596,6 +606,16 @@ const resources = { }, de: { translation: { + //Components + chromaDescription: 'Chroma ist eine Vektordatenbank, die LLM-Anwendungen dabei hilft, über ein Langzeitgedächtnis zu verfügen.', + faissDescription: 'FAISS ist eine Vektordatenbank, die es Entwicklern ermöglicht, Einbettungen von einander ähnlichen Dokumenten schnell zu gruppieren und zu durchsuchen.', + llama2Description: 'LLama2 ist ein großes Sprachmodell, das heruntergeladen und in einer selbst gehosteten Umgebung bereitgestellt werden kann.', + mosaicDescription: 'Mosaik ist ein KI-Tool zum einfachen Trainieren und Bereitstellen generativer KI-Modelle auf Ihren Daten in Ihrer sicheren Umgebung.', + pineconeDescription: 'Pinecone ist eine vollständig verwaltete Vektordatenbanklösung, die zum Speichern und Durchsuchen von Vektoreinbettungen verwendet wird.', + weaviateDescription: 'Weaviate ist eine Open-Source-Vektordatenbank, die das Speichern von Objekt- und Vektoreinbettungen aus verschiedenen ML-Modellen ermöglicht.', + openaiDescription: 'Der OpenAI-Connector ist ein bidirektionaler Connector, der Airy mit dem OpenAI LLM verbindet.', + gmailDescription: 'Der GMail-Connector ist ein bidirektionaler Connector zum Senden und Empfangen von E-Mail-Nachrichten über die Google Mail-API.', + //Input Component fieldCannotBeEmpty: 'Dieses Feld kann nicht leer sein.', invalidURL: 'Die URL ist ungültig', @@ -1199,6 +1219,16 @@ const resources = { }, fr: { translation: { + //Components + chromaDescription: 'Chroma est une base de données vectorielle qui aide les applications LLM à disposer d\'une mémoire à long terme.', + faissDescription: 'FAISS est une base de données vectorielle qui permet aux développeurs de regrouper et de rechercher rapidement des intégrations de documents similaires les uns aux autres.', + llama2Description: 'LLama2 est un grand modèle de langage qui peut être téléchargé et déployé dans un environnement auto-hébergé.', + mosaicDescription: 'Mosaic est un outil d\'IA permettant de former et de déployer facilement des modèles d\'IA génératifs sur vos données, dans votre environnement sécurisé.', + pineconeDescription: 'Pinecone est une solution de base de données vectorielle entièrement gérée, utilisée pour stocker et rechercher des intégrations vectorielles.', + weaviateDescription: 'Weaviate est une base de données vectorielles open source qui permet de stocker des intégrations d\'objets et de vecteurs à partir de divers modèles ML.', + openaiDescription: 'Le connecteur OpenAI est un connecteur bidirectionnel qui connecte Airy au OpenAI LLM.', + gmailDescription: 'Le connecteur GMail est un connecteur bidirectionnel permettant d\'envoyer et de recevoir des messages électroniques via l\'API Google Mail.', + //Input Component fieldCannotBeEmpty: 'Ce champ ne peut pas être vide.', invalidURL: 'URL non valide', @@ -1796,6 +1826,16 @@ const resources = { }, es: { translation: { + //Components + chromaDescription: 'Chroma es una base de datos vectorial que ayuda a las aplicaciones LLM a tener memoria a largo plazo.', + faissDescription: 'FAISS es una base de datos vectorial que permite a los desarrolladores agrupar y buscar rápidamente incrustaciones de documentos que son similares entre sí.', + llama2Description: 'LLama2 es un modelo de lenguaje grande que se puede descargar e implementar en un entorno autohospedado.', + mosaicDescription: 'Mosaic es una herramienta de IA para entrenar e implementar fácilmente modelos de IA generativos en sus datos, en su entorno seguro.', + pineconeDescription: 'Pinecone es una solución de base de datos de vectores totalmente administrada, que se utiliza para almacenar y buscar incrustaciones de vectores.', + weaviateDescription: 'Weaviate es una base de datos vectorial de código abierto que permite almacenar incrustaciones de objetos y vectores de varios modelos ML.', + openaiDescription: 'El conector OpenAI es un conector bidireccional que conecta Airy con OpenAI LLM.', + gmailDescription: 'El conector GMail es un conector bidireccional para enviar y recibir mensajes de correo electrónico a través de la API de Google Mail.', + //Input Component fieldCannotBeEmpty: 'El campo de texto no puede estar vacío.', invalidURL: 'La URL no es válida', From 4b72cf2cfdac195d884c8977cf11d6db13575bef Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Thu, 21 Sep 2023 12:50:09 +0200 Subject: [PATCH 09/37] Change name for Pinecone connector (#4127) --- frontend/control-center/src/components/ChannelAvatar/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/control-center/src/components/ChannelAvatar/index.tsx b/frontend/control-center/src/components/ChannelAvatar/index.tsx index 16b05c6da..7a981bb04 100644 --- a/frontend/control-center/src/components/ChannelAvatar/index.tsx +++ b/frontend/control-center/src/components/ChannelAvatar/index.tsx @@ -118,7 +118,7 @@ export const getChannelAvatar = (source: string) => { case 'OpenAI connector': return ; case Source.pineconeConnector: - case 'Pinecone': + case 'Pinecone connector': return ; case Source.chroma: case 'Chroma': From aea7497176e3a69670ec981184a61821b58cebc0 Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Tue, 26 Sep 2023 17:09:34 +0200 Subject: [PATCH 10/37] Improve docs (#4128) --- .../conversational-ai/introduction.md | 2 +- docs/docs/getting-started/components.md | 4 +- docs/docs/getting-started/glossary.md | 26 +++++- lib/typescript/translations/translations.ts | 84 ++++++++++++------- 4 files changed, 81 insertions(+), 35 deletions(-) diff --git a/docs/docs/connectors/conversational-ai/introduction.md b/docs/docs/connectors/conversational-ai/introduction.md index df111a622..151d33b06 100644 --- a/docs/docs/connectors/conversational-ai/introduction.md +++ b/docs/docs/connectors/conversational-ai/introduction.md @@ -9,7 +9,7 @@ import ButtonBox from "@site/src/components/ButtonBox"; Level up your channels' communication with Airy Core's conversational AI [connectors](/concepts/architecture#components). -Airy Core features conversational AI [connectors](/concepts/architecture#components) that you can easily install and configure on your instance. +Airy Core features conversational AI [connectors](/concepts/architecture#components) that you can easily install and configure on your instance. For all of the LLM connectors that Airy supports, please refer to our [Enterprise Docs](https://airy.co/docs/enterprise/). } iconInvertible={true} title='WebSockets to power real-time applications' - description="A WebSocket server that allows clients to receive near real-time updates about data flowing through the system." + description="A WebSocket server that allows clients to receive near real-time updates about data flowing through the system. Particularly useful in combination with our LLM connectors and apps, that can send real-time data to enrich the interaction your customers." link='/api/websocket' /> } iconInvertible={true} title='UI: From a control center to dashboards' - description="No-code interfaces to manage and control Airy, your connectors and your streams." + description="No-code interfaces to manage and control Airy, your connectors, your LLM integrations and your streams. " link='/ui/inbox/introduction' /> diff --git a/docs/docs/getting-started/glossary.md b/docs/docs/getting-started/glossary.md index d6060e5d3..f99512014 100644 --- a/docs/docs/getting-started/glossary.md +++ b/docs/docs/getting-started/glossary.md @@ -118,21 +118,39 @@ A tag is a special use case of metadata, which is used to tag common, Airy Core provides specialized endpoints and filters for tagging conversations. +## AI & ML + +## Large language model + +A type of artificial intelligence model designed to understand and generate human-like text based on vast amounts of data. It's trained on diverse internet text to predict the next word in a sequence, enabling it to answer questions, generate content, and assist with various tasks. Airy allows a plug-able interface into different LLMs. + +## Vector database + +A high-dimensional database store which is suitable for persistent storage for natural language processing or images. The data is represented as vectors and retrieval is based on similarity, allowing for efficient similarity searches and context creation. Vector databases are very convenient for storing vector representations of streaming data that can be queried and add context to questions that are sent to LLMs, in real time. + +## Automation + +The ability of a an Airy component to react and simulate human-like conversations and automate specific tasks, in real time. It aims to provide users with immediate, consistent responses, reducing the need for human intervention in customer support, inquiries, and other conversational scenarios. + ## Source A source represents a system that generates messaging data that a user wants to process with Airy Core. -## Stream - -The whole Airy platform is based on Kafka and real-time streaming of messages. In the context of `streams` feature that Airy supports, a `stream` is the process of joining two or multiple Kafka topics, combining the data and creating an outout topic where the result of the streaming operation will be stored. It is based on KSQL. - ### Provider Source providers are API platforms that allow Airy Core to connect to one or more of their sources typically via a webhook. E.g. Twilio is a source provider for the Twilio SMS and WhatsApp sources. +## App + +Third party open-source packages that can be installed alongside Airy, in the same Kubernetes cluster, to provide a more robust and powerful application development environment. These `Apps` can vary from databases (ex. PostgreSQL or Redis) to LLM implementations and vector databases (ex. Llama2 or FAISS). + +## Stream + +The whole Airy platform is based on Kafka and real-time streaming of messages. In the context of `streams` feature that Airy supports, a `stream` is the process of joining two or multiple Kafka topics, combining the data and creating an outout topic where the result of the streaming operation will be stored. It is based on KSQL. + ## User A user represents one authorized agent in Airy Core, which is different from a Contact diff --git a/lib/typescript/translations/translations.ts b/lib/typescript/translations/translations.ts index b3743bf1f..89529fc01 100644 --- a/lib/typescript/translations/translations.ts +++ b/lib/typescript/translations/translations.ts @@ -6,13 +6,19 @@ const resources = { translation: { //Components chromaDescription: 'Chroma is a vector database that helps LLM applications to have long term memory.', - faissDescription: 'FAISS is a vector database that allows developers to quickly group and search embeddings of documents that are similar to each other.', - llama2Description: 'LLama2 is a large language model that can be downloaded and deployed in a self-hosted environment.', - mosaicDescription: 'Mosaic is an AI tool to easily train and deploy generative AI models on your data, in your secure environment.', - pineconeDescription: 'Pinecone is a fully managed vector database solution, used to store and search vector embeddings.', - weaviateDescription: 'Weaviate is an open-source vector database that allows storing object and vector embeddings from various ML-models.', + faissDescription: + 'FAISS is a vector database that allows developers to quickly group and search embeddings of documents that are similar to each other.', + llama2Description: + 'LLama2 is a large language model that can be downloaded and deployed in a self-hosted environment.', + mosaicDescription: + 'Mosaic is an AI tool to easily train and deploy generative AI models on your data, in your secure environment.', + pineconeDescription: + 'Pinecone is a fully managed vector database solution, used to store and search vector embeddings.', + weaviateDescription: + 'Weaviate is an open-source vector database that allows storing object and vector embeddings from various ML-models.', openaiDescription: 'The OpenAI connector is a bidirectional connector that connects Airy to the OpenAI LLM.', - gmailDescription: 'The GMail connector is a bidirectional connector for sending and receiving e-mail messages through the Google Mail API.', + gmailDescription: + 'The GMail connector is a bidirectional connector for sending and receiving e-mail messages through the Google Mail API.', //Input Component fieldCannotBeEmpty: 'This field cannot be empty.', @@ -607,14 +613,22 @@ const resources = { de: { translation: { //Components - chromaDescription: 'Chroma ist eine Vektordatenbank, die LLM-Anwendungen dabei hilft, über ein Langzeitgedächtnis zu verfügen.', - faissDescription: 'FAISS ist eine Vektordatenbank, die es Entwicklern ermöglicht, Einbettungen von einander ähnlichen Dokumenten schnell zu gruppieren und zu durchsuchen.', - llama2Description: 'LLama2 ist ein großes Sprachmodell, das heruntergeladen und in einer selbst gehosteten Umgebung bereitgestellt werden kann.', - mosaicDescription: 'Mosaik ist ein KI-Tool zum einfachen Trainieren und Bereitstellen generativer KI-Modelle auf Ihren Daten in Ihrer sicheren Umgebung.', - pineconeDescription: 'Pinecone ist eine vollständig verwaltete Vektordatenbanklösung, die zum Speichern und Durchsuchen von Vektoreinbettungen verwendet wird.', - weaviateDescription: 'Weaviate ist eine Open-Source-Vektordatenbank, die das Speichern von Objekt- und Vektoreinbettungen aus verschiedenen ML-Modellen ermöglicht.', - openaiDescription: 'Der OpenAI-Connector ist ein bidirektionaler Connector, der Airy mit dem OpenAI LLM verbindet.', - gmailDescription: 'Der GMail-Connector ist ein bidirektionaler Connector zum Senden und Empfangen von E-Mail-Nachrichten über die Google Mail-API.', + chromaDescription: + 'Chroma ist eine Vektordatenbank, die LLM-Anwendungen dabei hilft, über ein Langzeitgedächtnis zu verfügen.', + faissDescription: + 'FAISS ist eine Vektordatenbank, die es Entwicklern ermöglicht, Einbettungen von einander ähnlichen Dokumenten schnell zu gruppieren und zu durchsuchen.', + llama2Description: + 'LLama2 ist ein großes Sprachmodell, das heruntergeladen und in einer selbst gehosteten Umgebung bereitgestellt werden kann.', + mosaicDescription: + 'Mosaik ist ein KI-Tool zum einfachen Trainieren und Bereitstellen generativer KI-Modelle auf Ihren Daten in Ihrer sicheren Umgebung.', + pineconeDescription: + 'Pinecone ist eine vollständig verwaltete Vektordatenbanklösung, die zum Speichern und Durchsuchen von Vektoreinbettungen verwendet wird.', + weaviateDescription: + 'Weaviate ist eine Open-Source-Vektordatenbank, die das Speichern von Objekt- und Vektoreinbettungen aus verschiedenen ML-Modellen ermöglicht.', + openaiDescription: + 'Der OpenAI-Connector ist ein bidirektionaler Connector, der Airy mit dem OpenAI LLM verbindet.', + gmailDescription: + 'Der GMail-Connector ist ein bidirektionaler Connector zum Senden und Empfangen von E-Mail-Nachrichten über die Google Mail-API.', //Input Component fieldCannotBeEmpty: 'Dieses Feld kann nicht leer sein.', @@ -1220,14 +1234,21 @@ const resources = { fr: { translation: { //Components - chromaDescription: 'Chroma est une base de données vectorielle qui aide les applications LLM à disposer d\'une mémoire à long terme.', - faissDescription: 'FAISS est une base de données vectorielle qui permet aux développeurs de regrouper et de rechercher rapidement des intégrations de documents similaires les uns aux autres.', - llama2Description: 'LLama2 est un grand modèle de langage qui peut être téléchargé et déployé dans un environnement auto-hébergé.', - mosaicDescription: 'Mosaic est un outil d\'IA permettant de former et de déployer facilement des modèles d\'IA génératifs sur vos données, dans votre environnement sécurisé.', - pineconeDescription: 'Pinecone est une solution de base de données vectorielle entièrement gérée, utilisée pour stocker et rechercher des intégrations vectorielles.', - weaviateDescription: 'Weaviate est une base de données vectorielles open source qui permet de stocker des intégrations d\'objets et de vecteurs à partir de divers modèles ML.', + chromaDescription: + "Chroma est une base de données vectorielle qui aide les applications LLM à disposer d'une mémoire à long terme.", + faissDescription: + 'FAISS est une base de données vectorielle qui permet aux développeurs de regrouper et de rechercher rapidement des intégrations de documents similaires les uns aux autres.', + llama2Description: + 'LLama2 est un grand modèle de langage qui peut être téléchargé et déployé dans un environnement auto-hébergé.', + mosaicDescription: + "Mosaic est un outil d'IA permettant de former et de déployer facilement des modèles d'IA génératifs sur vos données, dans votre environnement sécurisé.", + pineconeDescription: + 'Pinecone est une solution de base de données vectorielle entièrement gérée, utilisée pour stocker et rechercher des intégrations vectorielles.', + weaviateDescription: + "Weaviate est une base de données vectorielles open source qui permet de stocker des intégrations d'objets et de vecteurs à partir de divers modèles ML.", openaiDescription: 'Le connecteur OpenAI est un connecteur bidirectionnel qui connecte Airy au OpenAI LLM.', - gmailDescription: 'Le connecteur GMail est un connecteur bidirectionnel permettant d\'envoyer et de recevoir des messages électroniques via l\'API Google Mail.', + gmailDescription: + "Le connecteur GMail est un connecteur bidirectionnel permettant d'envoyer et de recevoir des messages électroniques via l'API Google Mail.", //Input Component fieldCannotBeEmpty: 'Ce champ ne peut pas être vide.', @@ -1827,14 +1848,21 @@ const resources = { es: { translation: { //Components - chromaDescription: 'Chroma es una base de datos vectorial que ayuda a las aplicaciones LLM a tener memoria a largo plazo.', - faissDescription: 'FAISS es una base de datos vectorial que permite a los desarrolladores agrupar y buscar rápidamente incrustaciones de documentos que son similares entre sí.', - llama2Description: 'LLama2 es un modelo de lenguaje grande que se puede descargar e implementar en un entorno autohospedado.', - mosaicDescription: 'Mosaic es una herramienta de IA para entrenar e implementar fácilmente modelos de IA generativos en sus datos, en su entorno seguro.', - pineconeDescription: 'Pinecone es una solución de base de datos de vectores totalmente administrada, que se utiliza para almacenar y buscar incrustaciones de vectores.', - weaviateDescription: 'Weaviate es una base de datos vectorial de código abierto que permite almacenar incrustaciones de objetos y vectores de varios modelos ML.', + chromaDescription: + 'Chroma es una base de datos vectorial que ayuda a las aplicaciones LLM a tener memoria a largo plazo.', + faissDescription: + 'FAISS es una base de datos vectorial que permite a los desarrolladores agrupar y buscar rápidamente incrustaciones de documentos que son similares entre sí.', + llama2Description: + 'LLama2 es un modelo de lenguaje grande que se puede descargar e implementar en un entorno autohospedado.', + mosaicDescription: + 'Mosaic es una herramienta de IA para entrenar e implementar fácilmente modelos de IA generativos en sus datos, en su entorno seguro.', + pineconeDescription: + 'Pinecone es una solución de base de datos de vectores totalmente administrada, que se utiliza para almacenar y buscar incrustaciones de vectores.', + weaviateDescription: + 'Weaviate es una base de datos vectorial de código abierto que permite almacenar incrustaciones de objetos y vectores de varios modelos ML.', openaiDescription: 'El conector OpenAI es un conector bidireccional que conecta Airy con OpenAI LLM.', - gmailDescription: 'El conector GMail es un conector bidireccional para enviar y recibir mensajes de correo electrónico a través de la API de Google Mail.', + gmailDescription: + 'El conector GMail es un conector bidireccional para enviar y recibir mensajes de correo electrónico a través de la API de Google Mail.', //Input Component fieldCannotBeEmpty: 'El campo de texto no puede estar vacío.', From 8776c1a301f6d1aae193df36ce056edb87f94903 Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Thu, 19 Oct 2023 18:03:54 +0200 Subject: [PATCH 11/37] Improve docs (#4131) --- docs/docs/getting-started/installation/minikube.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/docs/getting-started/installation/minikube.md b/docs/docs/getting-started/installation/minikube.md index bf9a7912d..9feddc0fc 100644 --- a/docs/docs/getting-started/installation/minikube.md +++ b/docs/docs/getting-started/installation/minikube.md @@ -14,6 +14,11 @@ Run Airy on minikube with one command. The goal of this document is to provide an overview of how to run Airy Core on your local machine using [minikube](https://minikube.sigs.k8s.io/). +## Requirements + +- [Terraform](https://learn.hashicorp.com/tutorials/terraform/install-cli) v1.2.0+ +- [Kubectl](https://kubernetes.io/docs/tasks/tools/) + ## Install :::note From 14563ba9a93c56e13f4d2847d8b98ea3d4f4f10e Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Fri, 27 Oct 2023 19:19:03 +0200 Subject: [PATCH 12/37] [#4134] Update main diagram (#4135) --- docs/docs/getting-started/introduction.md | 2 +- .../img/getting-started/introduction.png | Bin 0 -> 197623 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 docs/static/img/getting-started/introduction.png diff --git a/docs/docs/getting-started/introduction.md b/docs/docs/getting-started/introduction.md index ea4ecebc3..52e28db5f 100644 --- a/docs/docs/getting-started/introduction.md +++ b/docs/docs/getting-started/introduction.md @@ -23,7 +23,7 @@ Airy Core is an is an **open-source** **streaming** **app framework** to train M - +

Get Airy up and running with one command

diff --git a/docs/static/img/getting-started/introduction.png b/docs/static/img/getting-started/introduction.png new file mode 100644 index 0000000000000000000000000000000000000000..2391349407e2974f4ab5b9e25f55c3c8f41e6ffa GIT binary patch literal 197623 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¢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 RdI0c4T3kV_T*NT&zX1KZ!hrw) literal 0 HcmV?d00001 From 70890f81b00de9f0bd2482be6e1d52adf39c74ff Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Fri, 27 Oct 2023 19:24:14 +0200 Subject: [PATCH 13/37] [#4134] Update the diagram in the README file (#4136) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92d5b4b5b..2d80c6760 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ --- -![Airy_Explainer_Highlevel_Readme](https://airy.co/docs/core/img/getting-started/introduction-light.png) +![Airy_Explainer_Highlevel_Readme](https://airy.co/docs/core/img/getting-started/introduction.png) Airy Core is an is an open-source streaming app framework to train ML models and supply them with historical and real-time data. With Airy you can process data from a variety of sources: From f9d7c2f4881e9f2bd9f6ee1ac91435de01fdb0a9 Mon Sep 17 00:00:00 2001 From: Aitor Algorta Date: Thu, 11 Jan 2024 10:28:54 +0100 Subject: [PATCH 14/37] added source to libs and apps (#4133) --- .../Topics/TopicItem/TopicDescription/TopicDescription.tsx | 2 -- frontend/inbox/src/pages/Inbox/MessageInput/index.tsx | 3 +++ lib/typescript/model/Source.ts | 2 ++ lib/typescript/render/outbound/index.ts | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/control-center/src/pages/Topics/TopicItem/TopicDescription/TopicDescription.tsx b/frontend/control-center/src/pages/Topics/TopicItem/TopicDescription/TopicDescription.tsx index eeec77ad2..26d4d42d4 100644 --- a/frontend/control-center/src/pages/Topics/TopicItem/TopicDescription/TopicDescription.tsx +++ b/frontend/control-center/src/pages/Topics/TopicItem/TopicDescription/TopicDescription.tsx @@ -35,9 +35,7 @@ const TopicDescription = (props: TopicDescriptionProps) => { if (!_expanded) { wrapperSection.current.style.height = `${basicHeight}px`; } else { - console.log('in'); if (wrapperSection && wrapperSection.current) { - console.log('in in'); wrapperSection.current.style.height = `${calculateHeightOfCodeString(code) + 100 + headerHeight}px`; } else { wrapperSection.current.style.height = `${basicHeight}px`; diff --git a/frontend/inbox/src/pages/Inbox/MessageInput/index.tsx b/frontend/inbox/src/pages/Inbox/MessageInput/index.tsx index f17606e7b..4dba3375c 100644 --- a/frontend/inbox/src/pages/Inbox/MessageInput/index.tsx +++ b/frontend/inbox/src/pages/Inbox/MessageInput/index.tsx @@ -286,6 +286,9 @@ const MessageInput = (props: Props) => { case Source.whatsapp: message.message = outboundMapper.getTextPayload(input); break; + case Source.airyCopilot: + message.message = outboundMapper.getTextPayload(input); + break; } sendMessages(message).then(() => { diff --git a/lib/typescript/model/Source.ts b/lib/typescript/model/Source.ts index dab71c881..fffb775a0 100644 --- a/lib/typescript/model/Source.ts +++ b/lib/typescript/model/Source.ts @@ -34,6 +34,7 @@ export enum Source { amazons3 = 'amazons3', amazonLexV2 = 'amazonLexV2', integrationSourceApi = 'integrationSourceApi', + airyCopilot = 'copilot', } export enum SourceApps { @@ -57,6 +58,7 @@ export const isAiryComponent = (source: string): boolean => { case Source.airyContacts: case Source.airyMobile: case Source.airyWebhooks: + case Source.airyCopilot: case Source.integrationSourceApi: return true; } diff --git a/lib/typescript/render/outbound/index.ts b/lib/typescript/render/outbound/index.ts index d23c11016..97d87bd22 100644 --- a/lib/typescript/render/outbound/index.ts +++ b/lib/typescript/render/outbound/index.ts @@ -13,6 +13,7 @@ export const getOutboundMapper = (source: string) => { case 'google': return new GoogleMapper(); case 'chatplugin': + case 'copilot': return new ChatpluginMapper(); case 'twilio.sms': case 'twilio.whatsapp': From 8a6d8195dd783ac9b104a7f9a96014469adda182 Mon Sep 17 00:00:00 2001 From: Aitor Algorta Date: Thu, 11 Jan 2024 10:44:52 +0100 Subject: [PATCH 15/37] [#4137] Improve Kafka Sections (#4138) * add section * improvement of schemas enrichment * some fixes * some fixes * some fixes --- frontend/control-center/src/App.tsx | 6 + .../src/actions/streams/index.ts | 25 +- .../src/components/Sidebar/index.tsx | 48 ++- .../ConnectorWrapper/index.module.scss | 3 + .../LLMConsumers/EmptyState/index.module.scss | 52 +++ .../pages/LLMConsumers/EmptyState/index.tsx | 30 ++ .../LLMConsumerItem/index.module.scss | 47 +++ .../LLMConsumers/LLMConsumerItem/index.tsx | 38 +++ .../src/pages/LLMConsumers/index.module.scss | 136 ++++++++ .../src/pages/LLMConsumers/index.tsx | 216 +++++++++++++ .../pages/LLMs/EmptyState/index.module.scss | 52 +++ .../src/pages/LLMs/EmptyState/index.tsx | 30 ++ .../pages/LLMs/LLMInfoItem/index.module.scss | 21 ++ .../src/pages/LLMs/LLMInfoItem/index.tsx | 18 ++ .../src/pages/LLMs/index.module.scss | 106 +++++++ .../control-center/src/pages/LLMs/index.tsx | 92 ++++++ .../EnrichedSchemaSection/index.tsx | 296 ++++++++++++++++++ .../SchemaDescription/SchemaDescription.tsx | 45 ++- .../SchemaDescription/SchemaSection/index.tsx | 97 +++++- .../SchemaDescription/index.module.scss | 67 ++++ .../src/pages/Schemas/SchemaItem/index.tsx | 7 + .../TopicDescription/TopicDescription.tsx | 13 +- .../src/pages/Topics/index.module.scss | 2 +- .../src/reducers/data/streams/index.ts | 11 + frontend/control-center/src/routes/routes.ts | 4 + .../control-center/src/services/format.ts | 2 +- .../SettingsModal/ModalHeader.module.scss | 2 +- lib/typescript/httpclient/src/client.ts | 28 ++ .../httpclient/src/endpoints/index.ts | 6 + .../src/endpoints/llmConsumersCreate.ts | 11 + .../src/endpoints/llmConsumersDelete.ts | 6 + .../src/endpoints/llmConsumersList.ts | 6 + .../httpclient/src/endpoints/llmInfo.ts | 6 + .../httpclient/src/endpoints/llmQuery.ts | 6 + .../httpclient/src/endpoints/llmStats.ts | 6 + .../LLMConsumerCreateRequestPayload.ts | 6 + .../LLMConsumerCreateResponsePayload.ts | 3 + .../src/payload/LLMConsumerListPayload.ts | 10 + .../src/payload/LLMConsumersDeletePayload.ts | 3 + .../httpclient/src/payload/LLMInfoPayload.ts | 8 + .../src/payload/LLMQueryRequestPayload.ts | 3 + .../src/payload/LLMQueryResponsePayload.ts | 6 + .../httpclient/src/payload/LLMStatsPayload.ts | 3 + .../httpclient/src/payload/index.ts | 8 + lib/typescript/model/Streams.ts | 3 + lib/typescript/translations/translations.ts | 40 +++ 46 files changed, 1586 insertions(+), 48 deletions(-) create mode 100644 frontend/control-center/src/pages/LLMConsumers/EmptyState/index.module.scss create mode 100644 frontend/control-center/src/pages/LLMConsumers/EmptyState/index.tsx create mode 100644 frontend/control-center/src/pages/LLMConsumers/LLMConsumerItem/index.module.scss create mode 100644 frontend/control-center/src/pages/LLMConsumers/LLMConsumerItem/index.tsx create mode 100644 frontend/control-center/src/pages/LLMConsumers/index.module.scss create mode 100644 frontend/control-center/src/pages/LLMConsumers/index.tsx create mode 100644 frontend/control-center/src/pages/LLMs/EmptyState/index.module.scss create mode 100644 frontend/control-center/src/pages/LLMs/EmptyState/index.tsx create mode 100644 frontend/control-center/src/pages/LLMs/LLMInfoItem/index.module.scss create mode 100644 frontend/control-center/src/pages/LLMs/LLMInfoItem/index.tsx create mode 100644 frontend/control-center/src/pages/LLMs/index.module.scss create mode 100644 frontend/control-center/src/pages/LLMs/index.tsx create mode 100644 frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/EnrichedSchemaSection/index.tsx create mode 100644 lib/typescript/httpclient/src/endpoints/llmConsumersCreate.ts create mode 100644 lib/typescript/httpclient/src/endpoints/llmConsumersDelete.ts create mode 100644 lib/typescript/httpclient/src/endpoints/llmConsumersList.ts create mode 100644 lib/typescript/httpclient/src/endpoints/llmInfo.ts create mode 100644 lib/typescript/httpclient/src/endpoints/llmQuery.ts create mode 100644 lib/typescript/httpclient/src/endpoints/llmStats.ts create mode 100644 lib/typescript/httpclient/src/payload/LLMConsumerCreateRequestPayload.ts create mode 100644 lib/typescript/httpclient/src/payload/LLMConsumerCreateResponsePayload.ts create mode 100644 lib/typescript/httpclient/src/payload/LLMConsumerListPayload.ts create mode 100644 lib/typescript/httpclient/src/payload/LLMConsumersDeletePayload.ts create mode 100644 lib/typescript/httpclient/src/payload/LLMInfoPayload.ts create mode 100644 lib/typescript/httpclient/src/payload/LLMQueryRequestPayload.ts create mode 100644 lib/typescript/httpclient/src/payload/LLMQueryResponsePayload.ts create mode 100644 lib/typescript/httpclient/src/payload/LLMStatsPayload.ts diff --git a/frontend/control-center/src/App.tsx b/frontend/control-center/src/App.tsx index 294aabaae..4788b33db 100644 --- a/frontend/control-center/src/App.tsx +++ b/frontend/control-center/src/App.tsx @@ -17,6 +17,8 @@ import { STREAMS_ROUTE, TOPICS_ROUTE, SCHEMAS_ROUTE, + LLMS_ROUTE, + LLM_CONSUMERS_ROUTE, } from './routes/routes'; import NotFound from './pages/NotFound'; import ConnectorsOutlet from './pages/Connectors/ConnectorsOutlet'; @@ -36,6 +38,8 @@ import {getAppExternalURL} from './services/getAppExternalURL'; import Streams from './pages/Streams'; import Topics from './pages/Topics'; import Schemas from './pages/Schemas'; +import LLMs from './pages/LLMs'; +import LLMConsumers from './pages/LLMConsumers'; const mapDispatchToProps = { getClientConfig, @@ -111,6 +115,8 @@ const App = (props: ConnectedProps) => { } /> } /> + } /> + } /> } /> diff --git a/frontend/control-center/src/actions/streams/index.ts b/frontend/control-center/src/actions/streams/index.ts index 3d47b61e0..26619e2e6 100644 --- a/frontend/control-center/src/actions/streams/index.ts +++ b/frontend/control-center/src/actions/streams/index.ts @@ -10,6 +10,7 @@ const SET_TOPIC_INFO = '@@metadata/SET_TOPIC_INFO'; const SET_TOPIC_SCHEMAS = '@@metadata/SET_TOPIC_SCHEMAS'; const SET_STREAMS = '@@metadata/SET_STREAMS'; const SET_SCHEMAS_INFO = '@@metadata/SET_SCHEMAS_INFO'; +const SET_SCHEMAS_VERSIONS = '@@metadata/SET_SCHEMAS_VERSIONS'; const SET_STREAM_INFO = '@@metadata/SET_STREAM_INFO'; const SET_LAST_MESSAGE = '@@metadata/SET_LAST_MESSAGRE'; @@ -74,8 +75,23 @@ export const getSchemas = () => async (dispatch: Dispatch) => { }); }; -export const getSchemaInfo = (topicName: string) => async (dispatch: Dispatch) => { - return getData(`subjects/${topicName}/versions/latest`).then(response => { +export const getSchemaVersions = (topicName: string) => async (dispatch: Dispatch) => { + return getData(`subjects/${topicName}/versions`).then(response => { + if (response.error_code && response.error_code.toString().includes('404') && !topicName.includes('-value')) { + return Promise.reject('404 Not Found'); + } else { + dispatch(setCurrentSchemaVersionsAction({name: topicName, versions: response})); + } + return Promise.resolve(true); + }); +}; + +export const getSchemaInfo = (topicName: string, version?: string) => async (dispatch: Dispatch) => { + let v = 'latest'; + if (version) { + v = version; + } + return getData(`subjects/${topicName}/versions/${v}`).then(response => { if (response.error_code && response.error_code.toString().includes('404') && !topicName.includes('-value')) { return Promise.reject('404 Not Found'); } else { @@ -197,6 +213,11 @@ export const setStreamsAction = createAction(SET_STREAMS, (streams: Stream[]) => export const setCurrentSchemaInfoAction = createAction(SET_SCHEMAS_INFO, (topicInfo: Schema) => topicInfo)(); +export const setCurrentSchemaVersionsAction = createAction( + SET_SCHEMAS_VERSIONS, + (topicInfo: {name: string; versions: []}) => topicInfo +)<{name: string; versions: []}>(); + export const setCurrentStreamInfoAction = createAction( SET_STREAM_INFO, (streamInfo: StreamInfo) => streamInfo diff --git a/frontend/control-center/src/components/Sidebar/index.tsx b/frontend/control-center/src/components/Sidebar/index.tsx index 043513f45..95285c91e 100644 --- a/frontend/control-center/src/components/Sidebar/index.tsx +++ b/frontend/control-center/src/components/Sidebar/index.tsx @@ -15,6 +15,8 @@ import { STREAMS_ROUTE, TOPICS_ROUTE, SCHEMAS_ROUTE, + LLMS_ROUTE, + LLM_CONSUMERS_ROUTE, } from '../../routes/routes'; import {ReactComponent as ConnectorsIcon} from 'assets/images/icons/gitMerge.svg'; @@ -31,18 +33,18 @@ type SideBarProps = {} & ConnectedProps; const mapStateToProps = (state: StateModel) => ({ version: state.data.config.clusterVersion, components: state.data.config.components, + connectors: state.data.catalog, }); const connector = connect(mapStateToProps); const Sidebar = (props: SideBarProps) => { - const {version, components} = props; + const {version, components, connectors} = props; const componentInfo = useCurrentComponentForSource(Source.airyWebhooks); - const webhooksEnabled = componentInfo.installationStatus === InstallationStatus.installed; const inboxEnabled = components[Source.frontendInbox]?.enabled || false; - const showLine = inboxEnabled || webhooksEnabled; - + const llmsEnabled = connectors['llm-controller']?.installationStatus === 'installed' || false; + const showLine = inboxEnabled || webhooksEnabled || llmsEnabled; const isActive = (route: string) => { return useMatch(`${route}/*`); }; @@ -51,6 +53,9 @@ const Sidebar = (props: SideBarProps) => { const [kafkaSectionOpen, setKafkaSectionOpen] = useState( href.includes(TOPICS_ROUTE) || href.includes(STREAMS_ROUTE) ); + const [llmSectionOpen, setLlmSectionOpen] = useState( + href.includes(LLMS_ROUTE) || href.includes(LLM_CONSUMERS_ROUTE) + ); return ( diff --git a/frontend/control-center/src/pages/Connectors/ConnectorWrapper/index.module.scss b/frontend/control-center/src/pages/Connectors/ConnectorWrapper/index.module.scss index ae262da93..91129c432 100644 --- a/frontend/control-center/src/pages/Connectors/ConnectorWrapper/index.module.scss +++ b/frontend/control-center/src/pages/Connectors/ConnectorWrapper/index.module.scss @@ -114,9 +114,12 @@ .connectorIcon { display: flex; width: 75px; + height: 75px; margin-right: 15px; svg { + width: 75px; + height: 75px; fill: var(--color-text-contrast); } } diff --git a/frontend/control-center/src/pages/LLMConsumers/EmptyState/index.module.scss b/frontend/control-center/src/pages/LLMConsumers/EmptyState/index.module.scss new file mode 100644 index 000000000..39684b4f2 --- /dev/null +++ b/frontend/control-center/src/pages/LLMConsumers/EmptyState/index.module.scss @@ -0,0 +1,52 @@ +@import 'assets/scss/colors.scss'; +@import 'assets/scss/fonts.scss'; + +.container { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: calc(100% - 88px); +} + +.contentContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + h1 { + @include font-m; + font-weight: 800; + color: var(--color-text-contrast); + margin: 31px 0; + } + + span { + @include font-base; + color: var(--color-text-gray); + } + + .subscribeButton { + color: var(--color-airy-blue); + &:hover { + cursor: pointer; + text-decoration: underline; + } + } +} + +.iconContainer { + display: flex; + justify-content: center; + align-items: center; + background: var(--color-background-gray); + height: 95px; + width: 105px; +} + +.searchIcon { + height: 45px; + width: 45px; + color: var(--color-airy-blue); +} diff --git a/frontend/control-center/src/pages/LLMConsumers/EmptyState/index.tsx b/frontend/control-center/src/pages/LLMConsumers/EmptyState/index.tsx new file mode 100644 index 000000000..1a542500b --- /dev/null +++ b/frontend/control-center/src/pages/LLMConsumers/EmptyState/index.tsx @@ -0,0 +1,30 @@ +import React, {Dispatch, SetStateAction} from 'react'; +import styles from './index.module.scss'; +import {ReactComponent as SearchIcon} from 'assets/images/icons/search.svg'; +import {useTranslation} from 'react-i18next'; + +type EmptyStateProps = { + createNewLLM: Dispatch>; +}; + +export const EmptyState = (props: EmptyStateProps) => { + const {createNewLLM} = props; + const {t} = useTranslation(); + + return ( +
+
+
+ +
+

{t('noLLMConsumers')}

+ + {t('noLLMConsumersText')} + createNewLLM(true)} className={styles.subscribeButton}> + {t('create') + ' one'} + + +
+
+ ); +}; diff --git a/frontend/control-center/src/pages/LLMConsumers/LLMConsumerItem/index.module.scss b/frontend/control-center/src/pages/LLMConsumers/LLMConsumerItem/index.module.scss new file mode 100644 index 000000000..b4f9b0d97 --- /dev/null +++ b/frontend/control-center/src/pages/LLMConsumers/LLMConsumerItem/index.module.scss @@ -0,0 +1,47 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; + +.container { + display: flex; + flex-direction: row; + height: 50px; + align-items: center; + justify-content: flex-start; + + p { + @include font-base; + color: var(--color-text-contrast); + font-weight: bold; + width: 25%; + } + + p:first-child { + width: 30%; + } + + p:nth-child(4) { + width: 15%; + } +} + +.actionButton { + width: 2%; + outline: none; + cursor: pointer; + border: none; + background: none; + padding: 0; +} + +.actionSVG { + width: 16px; + height: 18px; + path { + fill: var(--color-dark-elements-gray); + } + &:hover { + path { + fill: var(--color-airy-blue); + } + } +} diff --git a/frontend/control-center/src/pages/LLMConsumers/LLMConsumerItem/index.tsx b/frontend/control-center/src/pages/LLMConsumers/LLMConsumerItem/index.tsx new file mode 100644 index 000000000..b59ce205b --- /dev/null +++ b/frontend/control-center/src/pages/LLMConsumers/LLMConsumerItem/index.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import {ReactComponent as TrashIcon} from 'assets/images/icons/trash.svg'; +import {useTranslation} from 'react-i18next'; +import {HttpClientInstance} from '../../../httpClient'; +import styles from './index.module.scss'; +import {NotificationModel} from 'model'; + +type EmptyStateProps = { + item: {name: string; topic: string; status: string; lag: number}; + setNotification: (object: NotificationModel) => void; +}; + +export const LLMConsumerItem = (props: EmptyStateProps) => { + const {item, setNotification} = props; + const {t} = useTranslation(); + + const deleteConsumer = () => { + HttpClientInstance.deleteLLMConsumer({name: item.name}) + .then(() => { + setNotification({show: true, successful: true, text: 'Consumer Deleted'}); + }) + .catch(() => { + setNotification({show: true, successful: false, text: t('errorOccurred')}); + }); + }; + + return ( +
+

{item.name}

+

{item.topic}

+

{item.status}

+

{item.lag}

+ +
+ ); +}; diff --git a/frontend/control-center/src/pages/LLMConsumers/index.module.scss b/frontend/control-center/src/pages/LLMConsumers/index.module.scss new file mode 100644 index 000000000..8eb971f0d --- /dev/null +++ b/frontend/control-center/src/pages/LLMConsumers/index.module.scss @@ -0,0 +1,136 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; +@import 'assets/scss/animations.scss'; + +.llmsWrapper { + background: var(--color-background-white); + border-top-right-radius: 10px; + border-top-left-radius: 10px; + padding: 32px; + margin: 88px 1.5em 0 191px; + height: calc(100vh - 88px); + overflow-y: scroll; + overflow-x: hidden; + width: 100%; +} + +.headlineContainer { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; +} + +.llmsHeadline { + @include font-xl; + font-weight: 900; + letter-spacing: 0; + display: flex; + justify-content: space-between; + color: var(--color-text-contrast); + margin-bottom: 14px; +} + +.llmsHeadlineText { + @include font-xl; + font-weight: 900; +} + +.wrapper { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.listHeader { + display: flex; + flex-direction: row; + height: 50px; + align-items: center; + justify-content: flex-start; + + h2 { + @include font-base; + color: var(--color-text-gray); + font-weight: bold; + width: 25%; + } + + h2:first-child { + width: 30%; + } + + h2:last-child { + width: 10%; + } +} + +.successfullySubscribed { + @include font-base; + color: white; +} + +@keyframes translateYIn { + 0% { + transform: translateY(-50px); + opacity: 0; + } + + 50% { + transform: translateY(16px); + opacity: 1; + } + + 100% { + transform: translateY(-50px); + opacity: 0; + } +} + +.translateYAnimIn { + animation: translateYIn 4s ease-in-out; +} + +.animateIn { + animation: fadeInTranslateXLeft 3000ms ease; +} + +.animateOut { + animation: fadeInTranslateXLeft 3000ms ease; +} + +.llmCreateContainer { + display: flex; + flex-direction: column; + justify-content: space-between; + width: 100%; + margin-top: 32px; + button { + @include font-base; + font-weight: bold; + align-self: center; + color: white; + border-radius: 5px; + border: none; + padding: 8px 16px; + margin-top: 16px; + width: 60%; + cursor: pointer; + transition: all 0.2s ease-in-out; + &:hover { + background: var(--color-background-blue); + color: var(--color-text-contrast); + } + } + label { + margin-bottom: 12px; + } +} + +.dropdownContainer { + button { + border: 1px solid gray; + width: 100%; + color: black; + } +} diff --git a/frontend/control-center/src/pages/LLMConsumers/index.tsx b/frontend/control-center/src/pages/LLMConsumers/index.tsx new file mode 100644 index 000000000..d4d86eed5 --- /dev/null +++ b/frontend/control-center/src/pages/LLMConsumers/index.tsx @@ -0,0 +1,216 @@ +import React, {useEffect, useState} from 'react'; +import {Dropdown, Input, NotificationComponent} from 'components'; +import {SettingsModal} from 'components/alerts/SettingsModal'; +import {Button} from 'components/cta/Button'; +import {useTranslation} from 'react-i18next'; +import {connect, ConnectedProps} from 'react-redux'; +import {setPageTitle} from '../../services/pageTitle'; +import {NotificationModel} from 'model'; +import {AiryLoader} from 'components/loaders/AiryLoader'; +import {EmptyState} from './EmptyState'; +import {HttpClientInstance} from '../../httpClient'; +import {LLMConsumerItem} from './LLMConsumerItem'; +import {getValidTopics} from '../../selectors'; +import {StateModel} from '../../reducers'; +import styles from './index.module.scss'; +import {getSchemaInfo, getSchemas} from '../../actions'; + +type LLMConsumersProps = {} & ConnectedProps; + +const mapDispatchToProps = { + getSchemas, + getSchemaInfo, +}; + +const mapStateToProps = (state: StateModel) => { + return { + topics: getValidTopics(state), + schemas: state.data.streams.schemas, + }; +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +const LLMConsumers = (props: LLMConsumersProps) => { + const {topics, getSchemas} = props; + + const [consumers, setConsumers] = useState([]); + const [notification, setNotification] = useState(null); + const [dataFetched, setDataFetched] = useState(false); + const [showSettingsModal, setShowSettingsModal] = useState(false); + const [name, setName] = useState(''); + const [topic, setTopic] = useState(''); + const [type, setType] = useState(''); + const [textfield, setTextfield] = useState(''); + const [metadataFields, setMetadataFields] = useState(''); + const {t} = useTranslation(); + + useEffect(() => { + setPageTitle('LLM Consumers'); + getSchemas(); + }, []); + + useEffect(() => { + HttpClientInstance.listLLMConsumers() + .then((response: any) => { + setConsumers(response); + setDataFetched(true); + }) + .catch(() => { + handleNotification(true); + }); + }, []); + + const handleNotification = (show: boolean) => { + setNotification({show: show, successful: false, text: t('errorOccurred')}); + }; + + const toggleCreateView = () => { + setShowSettingsModal(!showSettingsModal); + }; + + const createNewLLM = () => { + const metadataFieldsArray = metadataFields.replace(' ', '').split(','); + HttpClientInstance.createLLMConsumer({ + name: name.trim(), + topic: topic.trim(), + textField: textfield.trim(), + metadataFields: metadataFieldsArray, + }) + .then(() => { + setNotification({show: true, successful: true, text: t('llmConsumerCreatedSuccessfully')}); + toggleCreateView(); + setName(''); + setTopic(''); + setTextfield(''); + setMetadataFields(''); + setType(''); + }) + .catch(() => { + handleNotification(true); + }); + }; + + return ( + <> + {' '} + {showSettingsModal && ( + +
+ ) => setName(event.target.value)} + minLength={6} + required={true} + height={32} + fontClass="font-base" + /> +
+ { + setTopic(topic); + // getSchemaInfo(topic).catch(() => { + // getSchemaInfo(topic + '-value'); + // }); + }} + /> +
+ ) => setType(event.target.value)} + minLength={2} + required={true} + height={32} + fontClass="font-base" + /> + ) => setTextfield(event.target.value)} + minLength={2} + required={true} + height={32} + fontClass="font-base" + /> + ) => setMetadataFields(event.target.value)} + minLength={6} + required={true} + height={32} + fontClass="font-base" + /> + +
+
+ )} +
+
+
+

LLM Consumers

+
+
+ +
+
+ {consumers?.length === 0 && dataFetched ? ( + + ) : consumers?.length === 0 ? ( + toggleCreateView()} /> + ) : ( + <> +
+

Name

+

Topic

+

Status

+

Lag

+
+
+ {consumers && + consumers.map((consumer: any) => ( + + ))} +
+ {notification?.show && ( + + )} + + )} +
+ + ); +}; + +export default connector(LLMConsumers); diff --git a/frontend/control-center/src/pages/LLMs/EmptyState/index.module.scss b/frontend/control-center/src/pages/LLMs/EmptyState/index.module.scss new file mode 100644 index 000000000..39684b4f2 --- /dev/null +++ b/frontend/control-center/src/pages/LLMs/EmptyState/index.module.scss @@ -0,0 +1,52 @@ +@import 'assets/scss/colors.scss'; +@import 'assets/scss/fonts.scss'; + +.container { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: calc(100% - 88px); +} + +.contentContainer { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + h1 { + @include font-m; + font-weight: 800; + color: var(--color-text-contrast); + margin: 31px 0; + } + + span { + @include font-base; + color: var(--color-text-gray); + } + + .subscribeButton { + color: var(--color-airy-blue); + &:hover { + cursor: pointer; + text-decoration: underline; + } + } +} + +.iconContainer { + display: flex; + justify-content: center; + align-items: center; + background: var(--color-background-gray); + height: 95px; + width: 105px; +} + +.searchIcon { + height: 45px; + width: 45px; + color: var(--color-airy-blue); +} diff --git a/frontend/control-center/src/pages/LLMs/EmptyState/index.tsx b/frontend/control-center/src/pages/LLMs/EmptyState/index.tsx new file mode 100644 index 000000000..2f1f7d737 --- /dev/null +++ b/frontend/control-center/src/pages/LLMs/EmptyState/index.tsx @@ -0,0 +1,30 @@ +import React, {Dispatch, SetStateAction} from 'react'; +import styles from './index.module.scss'; +import {ReactComponent as SearchIcon} from 'assets/images/icons/search.svg'; +import {useTranslation} from 'react-i18next'; + +type EmptyStateProps = { + createNewLLM: Dispatch>; +}; + +export const EmptyState = (props: EmptyStateProps) => { + const {createNewLLM} = props; + const {t} = useTranslation(); + + return ( +
+
+
+ +
+

{t('noLLMs')}

+ + {t('noLLMsText')} + createNewLLM(true)} className={styles.subscribeButton}> + {t('create') + ' one'} + + +
+
+ ); +}; diff --git a/frontend/control-center/src/pages/LLMs/LLMInfoItem/index.module.scss b/frontend/control-center/src/pages/LLMs/LLMInfoItem/index.module.scss new file mode 100644 index 000000000..331d24a20 --- /dev/null +++ b/frontend/control-center/src/pages/LLMs/LLMInfoItem/index.module.scss @@ -0,0 +1,21 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; + +.container { + display: flex; + flex-direction: row; + height: 50px; + align-items: center; + justify-content: flex-start; + + p { + @include font-base; + color: var(--color-text-contrast); + font-weight: bold; + width: 25%; + } + + p:first-child { + width: 30%; + } +} diff --git a/frontend/control-center/src/pages/LLMs/LLMInfoItem/index.tsx b/frontend/control-center/src/pages/LLMs/LLMInfoItem/index.tsx new file mode 100644 index 000000000..bcea927a8 --- /dev/null +++ b/frontend/control-center/src/pages/LLMs/LLMInfoItem/index.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import styles from './index.module.scss'; + +type EmptyStateProps = { + item: {llm: string; vectorDatabase: string; llmModel: string}; +}; + +export const LLMInfoItem = (props: EmptyStateProps) => { + const {item} = props; + + return ( +
+

{item.llm}

+

{item.vectorDatabase}

+

{item.llmModel}

+
+ ); +}; diff --git a/frontend/control-center/src/pages/LLMs/index.module.scss b/frontend/control-center/src/pages/LLMs/index.module.scss new file mode 100644 index 000000000..31195db2d --- /dev/null +++ b/frontend/control-center/src/pages/LLMs/index.module.scss @@ -0,0 +1,106 @@ +@import 'assets/scss/fonts.scss'; +@import 'assets/scss/colors.scss'; +@import 'assets/scss/animations.scss'; + +.webhooksWrapper { + background: var(--color-background-white); + border-top-right-radius: 10px; + border-top-left-radius: 10px; + padding: 32px; + margin: 88px 1.5em 0 191px; + height: calc(100vh - 88px); + overflow-y: scroll; + overflow-x: hidden; + width: 100%; +} + +.headlineContainer { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; +} + +.webhooksHeadline { + @include font-xl; + font-weight: 900; + letter-spacing: 0; + display: flex; + justify-content: space-between; + color: var(--color-text-contrast); + margin-bottom: 14px; +} + +.webhooksHeadlineText { + @include font-xl; + font-weight: 900; +} + +.wrapper { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.listHeader { + display: flex; + flex-direction: row; + height: 50px; + align-items: center; + justify-content: flex-start; + + h2 { + @include font-base; + color: var(--color-text-gray); + font-weight: bold; + width: 25%; + } + + h2:first-child { + width: 30%; + } + + h2:last-child { + width: 10%; + } +} + +.successfullySubscribed { + @include font-base; + color: white; +} + +@keyframes translateYIn { + 0% { + transform: translateY(-50px); + opacity: 0; + } + + 50% { + transform: translateY(16px); + opacity: 1; + } + + 100% { + transform: translateY(-50px); + opacity: 0; + } +} + +.translateYAnimIn { + animation: translateYIn 4s ease-in-out; +} + +.animateIn { + animation: fadeInTranslateXLeft 3000ms ease; +} + +.animateOut { + animation: fadeInTranslateXLeft 3000ms ease; +} + +.embeddingsSection { + color: var(--color-text-contrast); + font-weight: bold; + margin-top: 64px; +} diff --git a/frontend/control-center/src/pages/LLMs/index.tsx b/frontend/control-center/src/pages/LLMs/index.tsx new file mode 100644 index 000000000..385cdd1cb --- /dev/null +++ b/frontend/control-center/src/pages/LLMs/index.tsx @@ -0,0 +1,92 @@ +import React, {useEffect, useState} from 'react'; +import {NotificationComponent} from 'components'; +import {useTranslation} from 'react-i18next'; +import {connect} from 'react-redux'; +import {setPageTitle} from '../../services/pageTitle'; +import {NotificationModel} from 'model'; +import {AiryLoader} from 'components/loaders/AiryLoader'; +import styles from './index.module.scss'; +import {EmptyState} from './EmptyState'; +import {HttpClientInstance} from '../../httpClient'; +import {LLMSStatsPayload} from 'httpclient/src'; +import {LLMInfoItem} from './LLMInfoItem'; + +const mapDispatchToProps = {}; + +const connector = connect(null, mapDispatchToProps); + +const LLMs = () => { + const [llms, setLlms] = useState([]); + const [embeddings, setEmbeddings] = useState(0); + const [notification, setNotification] = useState(null); + const [dataFetched, setDataFetched] = useState(false); + const {t} = useTranslation(); + + useEffect(() => { + setPageTitle('LLMs'); + }, []); + + useEffect(() => { + HttpClientInstance.getLLMInfo() + .then((response: any) => { + setLlms(response); + setDataFetched(true); + }) + .catch(() => { + handleNotification(true); + }); + HttpClientInstance.getLLMStats() + .then((response: LLMSStatsPayload) => { + setEmbeddings(response.embeddings); + }) + .catch(() => { + handleNotification(true); + }); + }, []); + + const handleNotification = (show: boolean) => { + setNotification({show: show, successful: false, text: t('errorOccurred')}); + }; + + const createNewLLM = () => { + console.log('create new LLM'); + }; + + return ( + <> +
+
+
+

LLM Controller

+
+
+ {llms?.length === 0 && dataFetched ? ( + + ) : llms?.length === 0 ? ( + createNewLLM()} /> + ) : ( + <> +
+

LLM Provider

+

Vector Database

+

Model

+
+
{llms && llms.map((llm: any) => )}
+
Embeddings: {embeddings}
+ {notification?.show && ( + + )} + + )} +
+ + ); +}; + +export default connector(LLMs); diff --git a/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/EnrichedSchemaSection/index.tsx b/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/EnrichedSchemaSection/index.tsx new file mode 100644 index 000000000..7811f24b5 --- /dev/null +++ b/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/EnrichedSchemaSection/index.tsx @@ -0,0 +1,296 @@ +import React, {MutableRefObject, useEffect, useRef, useState} from 'react'; +import MonacoEditor from '@uiw/react-monacoeditor'; +import {calculateHeightOfCodeString, isJSON} from '../../../../../services'; +import {HttpClientInstance} from '../../../../../httpClient'; +import styles from '../index.module.scss'; +import {Button} from 'components'; +import {ConnectedProps, connect} from 'react-redux'; +import {checkCompatibilityOfNewSchema, setSchemaSchema} from '../../../../../actions'; +import {useTranslation} from 'react-i18next'; + +type EnrichedSchemaSectionProps = { + schemaName: string; + code: string; + setCode: (code: string) => void; + setFirstTabSelected: (flag: boolean) => void; + editorMode: string; + wrapperSection: MutableRefObject; + isEditMode: boolean; + setIsEditMode: (flag: boolean) => void; + setErrorMessage: (error: string) => void; + setShowErrorPopUp: (flag: boolean) => void; + version: number; +} & ConnectedProps; + +const mapDispatchToProps = { + setSchemaSchema, + checkCompatibilityOfNewSchema, +}; + +const connector = connect(null, mapDispatchToProps); + +const EnrichedSchemaSection = (props: EnrichedSchemaSectionProps) => { + const { + schemaName, + code, + setCode, + setFirstTabSelected, + editorMode, + wrapperSection, + setSchemaSchema, + isEditMode, + setIsEditMode, + checkCompatibilityOfNewSchema, + setErrorMessage, + setShowErrorPopUp, + version, + } = props; + + const [localCode, setLocalCode] = useState(undefined); + const [hasBeenChanged, setHasBeenChanged] = useState(false); + const codeRef = useRef(null); + const {t} = useTranslation(); + + useEffect(() => { + if (isEnrichmentAvailable(code)) { + setTimeout(() => { + const enriched = localStorage.getItem(schemaName); + if (enriched) { + setLocalCode(enriched); + recalculateContainerHeight(enriched); + } + }, 100); + enrichCode(code); + } else { + wrapperSection.current.style.height = '156px'; + } + }, [code]); + + const resetCodeAndEndEdition = () => { + setIsEditMode(false); + setFirstTabSelected(true); + }; + + const recalculateContainerHeight = (code: string) => { + const basicHeight = 220; + if (wrapperSection && wrapperSection.current) { + wrapperSection.current.style.height = `${calculateHeightOfCodeString(code) + basicHeight}px`; + } else { + wrapperSection.current.style.height = `${basicHeight}px`; + } + if ((wrapperSection.current.style.height.replace('px', '') as number) > 700) { + wrapperSection.current.style.height = '700px'; + } + }; + + const recalculateCodeHeight = (code: string) => { + const codeHeight = calculateHeightOfCodeString(code); + if (codeHeight > 478) { + return 478; + } + return codeHeight; + }; + + const isEnrichmentAvailable = (code: string): boolean => { + let needsEnrichment = false; + const parsedCode = JSON.parse(code); + (parsedCode.fields || []).map(field => { + if (typeof field.type === 'object' && !Array.isArray(field.type)) { + if (!field.type.doc) { + needsEnrichment = true; + } + } else if (!field.doc) { + needsEnrichment = true; + } + }); + return needsEnrichment; + }; + + const enrichCode = async (code: string) => { + let enrichedSchema = localStorage.getItem(schemaName); + + if (!enrichedSchema) { + const enrichedCode = JSON.parse(code); + + // Use map to create an array of promises + const promises = (enrichedCode.fields || []).map(async field => { + console.log(typeof field.type); + if (typeof field.type === 'object' && !Array.isArray(field.type)) { + if (!field.type.doc) { + const doc = await generateDocForField(field); + field.type.doc = doc; + } + } else if (!field.doc) { + const doc = await generateDocForField(field); + field.doc = doc; + } + }); + + // Wait for all promises to resolve + await Promise.all(promises); + + enrichedSchema = JSON.stringify(enrichedCode, null, 2); + localStorage.setItem(schemaName, enrichedSchema); + } + + setLocalCode(enrichedSchema); + recalculateContainerHeight(enrichedSchema); + }; + + const saveEnrichedSchema = () => { + setSchemaSchema(schemaName, JSON.stringify(localCode, null, 2)); + }; + + const checkCompatibility = (_schemaName: string, _code: string, _version: number) => { + checkCompatibilityOfNewSchema(_schemaName, _code, _version) + .then(() => { + setSchemaSchema(_schemaName, _code) + .then(() => { + setCode(localCode); + setHasBeenChanged(false); + }) + .catch((e: string) => { + setIsEditMode(true); + setErrorMessage(e); + setShowErrorPopUp(true); + setTimeout(() => setShowErrorPopUp(false), 5000); + }); + }) + .catch((e: string) => { + if (e.includes('404')) { + checkCompatibility(_schemaName + '-value', _code, _version); + } else { + setIsEditMode(true); + setErrorMessage(e); + setShowErrorPopUp(true); + setTimeout(() => setShowErrorPopUp(false), 5000); + } + }); + }; + + const generateDocForField = async (field: any): Promise => { + try { + const response = await HttpClientInstance.llmQuery({ + query: `This is the payload of a metadata field of a Kafka Schema ${JSON.stringify( + field + )}. A the name of the schema is ${schemaName}. This is the whole schema: ${code}. Give an accurante description of the field, so the users can understand what it is and what it is used for.`, + }); + return response.answer.result; + } catch (error) { + console.error('Error in generateDocForField:', error); + return ''; + } + }; + + return ( + <> +
+
+ + +
+
+ {isEnrichmentAvailable(code) ? ( + <> +
+
+
+ This schema can be automatically enriched with documentation and saved as a new version as follows. +
+ +
+
+
New schema:
+
+ + {hasBeenChanged && ( + + )} +
+
+
+ {localCode && localCode !== '{}' && ( +
+ { + if (value !== code) { + setHasBeenChanged(true); + } else { + setHasBeenChanged(false); + } + }} + onBlur={() => { + setLocalCode(codeRef.current.editor.getModel().getValue()); + }} + options={{ + scrollBeyondLastLine: isEditMode, + readOnly: !isEditMode, + theme: editorMode, + }} + /> +
+ )} + + ) : ( +
+
+
This schema has been enriched already with documentation.
+
+
+ )} + + ); +}; + +export default connector(EnrichedSchemaSection); diff --git a/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/SchemaDescription.tsx b/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/SchemaDescription.tsx index 673d8dda0..36ff6cf9a 100644 --- a/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/SchemaDescription.tsx +++ b/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/SchemaDescription.tsx @@ -1,14 +1,14 @@ import React, {MutableRefObject, useEffect, useState} from 'react'; -import {getSchemaInfo} from '../../../../actions'; +import {getSchemaInfo, getSchemaVersions} from '../../../../actions'; import {connect, ConnectedProps} from 'react-redux'; import {ErrorPopUp} from 'components'; -import {calculateHeightOfCodeString} from '../../../../services'; import SchemaSection from './SchemaSection'; import styles from './index.module.scss'; -import {MessageSection, lastMessageMock} from './MessageSection'; +import EnrichedSchemaSection from './EnrichedSchemaSection'; const mapDispatchToProps = { getSchemaInfo, + getSchemaVersions, }; const connector = connect(null, mapDispatchToProps); @@ -19,15 +19,17 @@ type SchemaDescriptionProps = { setCode: (code: string) => void; wrapperSection: MutableRefObject; version: number; + versions: string[]; } & ConnectedProps; const SchemaDescription = (props: SchemaDescriptionProps) => { - const {schemaName, code, setCode, getSchemaInfo, wrapperSection, version} = props; + const {schemaName, code, setCode, getSchemaInfo, getSchemaVersions, wrapperSection, version, versions} = props; useEffect(() => { getSchemaInfo(schemaName).catch(() => { getSchemaInfo(schemaName + '-value'); }); + getSchemaVersions(schemaName); }, []); useEffect(() => { @@ -43,26 +45,14 @@ const SchemaDescription = (props: SchemaDescriptionProps) => { const [errorMessage, setErrorMessage] = useState(''); const [editorMode, setEditorMode] = useState(localStorage.getItem('theme') === 'dark' ? 'vs-dark' : 'vs'); - useEffect(() => { - if (firstTabSelected) { - recalculateContainerHeight(code); - } else { - recalculateContainerHeight(lastMessageMock); - } - }, [firstTabSelected, code]); - const setNewSchemaCode = (text: string) => { setCode(text); }; - const recalculateContainerHeight = (code: string) => { - const basicHeight = 50; - const headerHeight = 32; - if (wrapperSection && wrapperSection.current) { - wrapperSection.current.style.height = `${calculateHeightOfCodeString(code) + headerHeight + basicHeight}px`; - } else { - wrapperSection.current.style.height = `${basicHeight}px`; - } + const loadSchemaVersion = (version: string) => { + getSchemaInfo(schemaName, version).catch(() => { + getSchemaInfo(schemaName + '-value', version); + }); }; return ( @@ -76,17 +66,26 @@ const SchemaDescription = (props: SchemaDescriptionProps) => { setIsEditMode={setIsEditMode} setFirstTabSelected={setFirstTabSelected} editorMode={editorMode} - recalculateContainerHeight={recalculateContainerHeight} + wrapperSection={wrapperSection} setErrorMessage={setErrorMessage} setShowErrorPopUp={setShowErrorPopUp} version={version} + versions={versions} + loadSchemaVersion={loadSchemaVersion} /> ) : ( - )} {showErrorPopUp && setShowErrorPopUp(false)} />} diff --git a/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/SchemaSection/index.tsx b/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/SchemaSection/index.tsx index 701b6f136..fad1bef22 100644 --- a/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/SchemaSection/index.tsx +++ b/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/SchemaSection/index.tsx @@ -1,11 +1,11 @@ -import React, {useEffect, useRef, useState} from 'react'; +import React, {MutableRefObject, useEffect, useRef, useState} from 'react'; import MonacoEditor from '@uiw/react-monacoeditor'; import {calculateHeightOfCodeString, isJSON} from '../../../../../services'; import {useTranslation} from 'react-i18next'; -import {Button} from 'components'; -import styles from '../index.module.scss'; +import {Button, Dropdown} from 'components'; import {checkCompatibilityOfNewSchema, setSchemaSchema} from '../../../../../actions'; import {ConnectedProps, connect} from 'react-redux'; +import styles from '../index.module.scss'; const mapDispatchToProps = { setSchemaSchema, @@ -22,10 +22,12 @@ type SchemaSectionProps = { setIsEditMode: (flag: boolean) => void; setFirstTabSelected: (flag: boolean) => void; editorMode: string; - recalculateContainerHeight: (text: string) => void; + wrapperSection: MutableRefObject; setErrorMessage: (error: string) => void; setShowErrorPopUp: (flag: boolean) => void; version: number; + loadSchemaVersion: (version: string) => void; + versions: string[]; } & ConnectedProps; const SchemaSection = (props: SchemaSectionProps) => { @@ -37,12 +39,14 @@ const SchemaSection = (props: SchemaSectionProps) => { setIsEditMode, setFirstTabSelected, editorMode, - recalculateContainerHeight, + wrapperSection, checkCompatibilityOfNewSchema, setSchemaSchema, setErrorMessage, setShowErrorPopUp, version, + loadSchemaVersion, + versions, } = props; const [localCode, setLocalCode] = useState(code); @@ -60,6 +64,51 @@ const SchemaSection = (props: SchemaSectionProps) => { setIsEditMode(!isEditMode); }; + const recalculateContainerHeight = (code: string) => { + const basicHeight = 220; + if (wrapperSection && wrapperSection.current) { + wrapperSection.current.style.height = `${calculateHeightOfCodeString(code) + basicHeight}px`; + } else { + wrapperSection.current.style.height = `${basicHeight}px`; + } + if (!isEnrichmentAvailable(code)) { + if ((wrapperSection.current.style.height.replace('px', '') as number) > 600) { + wrapperSection.current.style.height = '600px'; + } + } else { + if ((wrapperSection.current.style.height.replace('px', '') as number) > 700) { + wrapperSection.current.style.height = '700px'; + } + } + }; + + const recalculateCodeHeight = (code: string) => { + const codeHeight = calculateHeightOfCodeString(code); + let height = 478; + if (!isEnrichmentAvailable(code)) { + height = 510; + } + if (codeHeight > height) { + return height; + } + return codeHeight; + }; + + const isEnrichmentAvailable = (code: string): boolean => { + let needsEnrichment = false; + const parsedCode = JSON.parse(code); + (parsedCode.fields || []).map(field => { + if (typeof field.type === 'object' && !Array.isArray(field.type)) { + if (!field.type.doc) { + needsEnrichment = true; + } + } else if (!field.doc) { + needsEnrichment = true; + } + }); + return needsEnrichment; + }; + const checkCompatibility = (_schemaName: string, _code: string, _version: number) => { checkCompatibilityOfNewSchema(_schemaName, _code, _version) .then(() => { @@ -99,14 +148,17 @@ const SchemaSection = (props: SchemaSectionProps) => { > Schema - {/* */} + />
+ {isEnrichmentAvailable(code) && ( + <> +
+
+
+ This schema can be automatically enriched with documentation and saved as a new version. +
+ +
+
Current schema:
+
+ + )} {code && code !== '{}' && ( { diff --git a/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/index.module.scss b/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/index.module.scss index 6edad57e0..27566454c 100644 --- a/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/index.module.scss +++ b/frontend/control-center/src/pages/Schemas/SchemaItem/SchemaDescription/index.module.scss @@ -59,3 +59,70 @@ flex: auto; padding-bottom: 8px; } + +.enrichmentContainer { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 16px; +} + +.enrichmentText { + padding: 8px; +} + +.enrichmentButton { + @include font-s; + border: none; + background-color: var(--color-code-no-edit); + color: var(--color-text-contrast); + font-weight: 600; + cursor: pointer; +} + +.enrichmentSchemaText { + padding: 8px; + @include font-s; + font-size: 12; + font-weight: 800; +} + +.codeContainer { + height: 100%; + width: 100%; + overflow: auto; + padding: 8px; +} + +.version { + display: flex; + p { + @include font-s; + font-size: 13px; + font-weight: 800; + padding: 8px 0; + margin-left: 16px; + color: var(--color-text-contrast); + } + button { + margin: 4px; + height: 24px; + width: 44px; + font-size: 12px; + align-items: center; + justify-content: center; + } + svg { + margin: 0 0 0 6px; + width: 11px; + height: 11px; + } + div { + font-size: 12px; + } +} + +.infoContainer { + display: flex; + background: transparent; +} diff --git a/frontend/control-center/src/pages/Schemas/SchemaItem/index.tsx b/frontend/control-center/src/pages/Schemas/SchemaItem/index.tsx index 071f3fcc8..54367e998 100644 --- a/frontend/control-center/src/pages/Schemas/SchemaItem/index.tsx +++ b/frontend/control-center/src/pages/Schemas/SchemaItem/index.tsx @@ -9,6 +9,7 @@ import SchemaDescription from './SchemaDescription/SchemaDescription'; const mapStateToProps = (state: StateModel) => { return { schemas: state.data.streams.schemas, + schemasVersions: state.data.streams.schemasVersions, }; }; @@ -32,6 +33,7 @@ const SchemaItem = (props: SchemaItemProps) => { addSchemasToSelection, itemSelected, setItemSelected, + schemasVersions, } = props; const [code, setCode] = useState(formatJSON(schemas[schemaName] ? schemas[schemaName].schema : '{}')); @@ -59,6 +61,10 @@ const SchemaItem = (props: SchemaItemProps) => { return 1; }; + const getVersions = (): string[] => { + return schemasVersions[schemaName] || []; + }; + return (
{ setCode={setCode} wrapperSection={wrapperSection} version={getVersion()} + versions={getVersions()} /> )}
diff --git a/frontend/control-center/src/pages/Topics/TopicItem/TopicDescription/TopicDescription.tsx b/frontend/control-center/src/pages/Topics/TopicItem/TopicDescription/TopicDescription.tsx index 26d4d42d4..1a7d8f8dc 100644 --- a/frontend/control-center/src/pages/Topics/TopicItem/TopicDescription/TopicDescription.tsx +++ b/frontend/control-center/src/pages/Topics/TopicItem/TopicDescription/TopicDescription.tsx @@ -40,7 +40,18 @@ const TopicDescription = (props: TopicDescriptionProps) => { } else { wrapperSection.current.style.height = `${basicHeight}px`; } + if ((wrapperSection.current.style.height.replace('px', '') as number) > 700) { + wrapperSection.current.style.height = '640px'; + } + } + }; + + const recalculateCodeHeight = (code: string) => { + const codeHeight = calculateHeightOfCodeString(code); + if (codeHeight > 478) { + return 478; } + return codeHeight; }; const calculateRetentionTime = (_jsonCode): string => { @@ -87,7 +98,7 @@ const TopicDescription = (props: TopicDescriptionProps) => { {expanded && ( { export const formatJSON = (jsonString: string): string => { if (jsonString) { - return JSON.stringify(JSON.parse(jsonString), null, 4); + return JSON.stringify(JSON.parse(jsonString), null, 2); } return ''; }; diff --git a/lib/typescript/components/alerts/SettingsModal/ModalHeader.module.scss b/lib/typescript/components/alerts/SettingsModal/ModalHeader.module.scss index acb3117df..924c4834d 100644 --- a/lib/typescript/components/alerts/SettingsModal/ModalHeader.module.scss +++ b/lib/typescript/components/alerts/SettingsModal/ModalHeader.module.scss @@ -30,5 +30,5 @@ } .closeIcon { - width: 18px; + width: 12px; } diff --git a/lib/typescript/httpclient/src/client.ts b/lib/typescript/httpclient/src/client.ts index 9bc283d51..dc67b2852 100644 --- a/lib/typescript/httpclient/src/client.ts +++ b/lib/typescript/httpclient/src/client.ts @@ -37,6 +37,14 @@ import { GetStreamInfoPayload, DeleteStreamPayload, CreateStreamPayload, + LLMConsumersListPayload, + LLMInfoPayload, + LLMSStatsPayload, + LLMConsumersCreateRequestPayload, + LLMConsumersCreateResponsePayload, + LLMConsumersDeletePayload, + LLMQueryRequestPayload, + LLMQueryResponsePayload, } from './payload'; import { listChannelsDef, @@ -88,6 +96,12 @@ import { getStreamInfoDef, deleteStreamDef, createStreamDef, + llmConsumersListDef, + llmInfoDef, + llmStatsDef, + llmConsumersCreateDef, + llmConsumersDeleteDef, + llmQueryDef, } from './endpoints'; import 'isomorphic-fetch'; import FormData from 'form-data'; @@ -319,6 +333,20 @@ export class HttpClient { public createStream = this.getRequest(createStreamDef); + public getLLMInfo = this.getRequest(llmInfoDef); + + public getLLMStats = this.getRequest(llmStatsDef); + + public listLLMConsumers = this.getRequest(llmConsumersListDef); + + public deleteLLMConsumer = this.getRequest(llmConsumersDeleteDef); + + public createLLMConsumer = this.getRequest( + llmConsumersCreateDef + ); + + public llmQuery = this.getRequest(llmQueryDef); + private getRequest({endpoint, mapRequest, mapResponse}: EndpointDefinition): ApiRequest { return async (requestPayload: K) => { endpoint = typeof endpoint === 'string' ? endpoint : endpoint(requestPayload); diff --git a/lib/typescript/httpclient/src/endpoints/index.ts b/lib/typescript/httpclient/src/endpoints/index.ts index dae00d107..8d847bac6 100644 --- a/lib/typescript/httpclient/src/endpoints/index.ts +++ b/lib/typescript/httpclient/src/endpoints/index.ts @@ -47,3 +47,9 @@ export * from './getStreams'; export * from './getStreamInfo'; export * from './deleteStream'; export * from './createStream'; +export * from './llmConsumersCreate'; +export * from './llmConsumersList'; +export * from './llmStats'; +export * from './llmInfo'; +export * from './llmConsumersDelete'; +export * from './llmQuery'; diff --git a/lib/typescript/httpclient/src/endpoints/llmConsumersCreate.ts b/lib/typescript/httpclient/src/endpoints/llmConsumersCreate.ts new file mode 100644 index 000000000..fe6fb40d5 --- /dev/null +++ b/lib/typescript/httpclient/src/endpoints/llmConsumersCreate.ts @@ -0,0 +1,11 @@ +import {LLMConsumersCreateRequestPayload} from '../payload'; + +export const llmConsumersCreateDef = { + endpoint: 'llm-consumers.create', + mapRequest: (request: LLMConsumersCreateRequestPayload) => ({ + name: request.name, + topic: request.topic, + textField: request.textField, + metadataFields: request.metadataFields, + }), +}; diff --git a/lib/typescript/httpclient/src/endpoints/llmConsumersDelete.ts b/lib/typescript/httpclient/src/endpoints/llmConsumersDelete.ts new file mode 100644 index 000000000..c1a4add48 --- /dev/null +++ b/lib/typescript/httpclient/src/endpoints/llmConsumersDelete.ts @@ -0,0 +1,6 @@ +import camelcaseKeys from 'camelcase-keys'; + +export const llmConsumersDeleteDef = { + endpoint: 'llm-consumers.delete', + mapResponse: response => camelcaseKeys(response), +}; diff --git a/lib/typescript/httpclient/src/endpoints/llmConsumersList.ts b/lib/typescript/httpclient/src/endpoints/llmConsumersList.ts new file mode 100644 index 000000000..bcb4349af --- /dev/null +++ b/lib/typescript/httpclient/src/endpoints/llmConsumersList.ts @@ -0,0 +1,6 @@ +import camelcaseKeys from 'camelcase-keys'; + +export const llmConsumersListDef = { + endpoint: 'llm-consumers.list', + mapResponse: response => camelcaseKeys(response), +}; diff --git a/lib/typescript/httpclient/src/endpoints/llmInfo.ts b/lib/typescript/httpclient/src/endpoints/llmInfo.ts new file mode 100644 index 000000000..4e21de899 --- /dev/null +++ b/lib/typescript/httpclient/src/endpoints/llmInfo.ts @@ -0,0 +1,6 @@ +import camelcaseKeys from 'camelcase-keys'; + +export const llmInfoDef = { + endpoint: 'llm.info', + mapResponse: response => camelcaseKeys(response), +}; diff --git a/lib/typescript/httpclient/src/endpoints/llmQuery.ts b/lib/typescript/httpclient/src/endpoints/llmQuery.ts new file mode 100644 index 000000000..37f2bba2d --- /dev/null +++ b/lib/typescript/httpclient/src/endpoints/llmQuery.ts @@ -0,0 +1,6 @@ +import camelcaseKeys from 'camelcase-keys'; + +export const llmQueryDef = { + endpoint: 'llm.query', + mapResponse: response => camelcaseKeys(response), +}; diff --git a/lib/typescript/httpclient/src/endpoints/llmStats.ts b/lib/typescript/httpclient/src/endpoints/llmStats.ts new file mode 100644 index 000000000..245a8526d --- /dev/null +++ b/lib/typescript/httpclient/src/endpoints/llmStats.ts @@ -0,0 +1,6 @@ +import camelcaseKeys from 'camelcase-keys'; + +export const llmStatsDef = { + endpoint: 'llm.stats', + mapResponse: response => camelcaseKeys(response), +}; diff --git a/lib/typescript/httpclient/src/payload/LLMConsumerCreateRequestPayload.ts b/lib/typescript/httpclient/src/payload/LLMConsumerCreateRequestPayload.ts new file mode 100644 index 000000000..acc1327d3 --- /dev/null +++ b/lib/typescript/httpclient/src/payload/LLMConsumerCreateRequestPayload.ts @@ -0,0 +1,6 @@ +export interface LLMConsumersCreateRequestPayload { + name: string; + topic: string; + textField: string; + metadataFields: string[]; +} diff --git a/lib/typescript/httpclient/src/payload/LLMConsumerCreateResponsePayload.ts b/lib/typescript/httpclient/src/payload/LLMConsumerCreateResponsePayload.ts new file mode 100644 index 000000000..5afcaf4bf --- /dev/null +++ b/lib/typescript/httpclient/src/payload/LLMConsumerCreateResponsePayload.ts @@ -0,0 +1,3 @@ +export interface LLMConsumersCreateResponsePayload { + status: string; +} diff --git a/lib/typescript/httpclient/src/payload/LLMConsumerListPayload.ts b/lib/typescript/httpclient/src/payload/LLMConsumerListPayload.ts new file mode 100644 index 000000000..f96c6697d --- /dev/null +++ b/lib/typescript/httpclient/src/payload/LLMConsumerListPayload.ts @@ -0,0 +1,10 @@ +interface LLMConsumer { + lag: number; + name: string; + status: string; + topic: string; +} + +export interface LLMConsumersListPayload { + data: LLMConsumer[]; +} diff --git a/lib/typescript/httpclient/src/payload/LLMConsumersDeletePayload.ts b/lib/typescript/httpclient/src/payload/LLMConsumersDeletePayload.ts new file mode 100644 index 000000000..0c937869a --- /dev/null +++ b/lib/typescript/httpclient/src/payload/LLMConsumersDeletePayload.ts @@ -0,0 +1,3 @@ +export interface LLMConsumersDeletePayload { + name: string; +} diff --git a/lib/typescript/httpclient/src/payload/LLMInfoPayload.ts b/lib/typescript/httpclient/src/payload/LLMInfoPayload.ts new file mode 100644 index 000000000..b053b5096 --- /dev/null +++ b/lib/typescript/httpclient/src/payload/LLMInfoPayload.ts @@ -0,0 +1,8 @@ +interface LLMInfo { + llm: string; + llm_model: string; + vectorDatabase: string; +} +export interface LLMInfoPayload { + data: LLMInfo[]; +} diff --git a/lib/typescript/httpclient/src/payload/LLMQueryRequestPayload.ts b/lib/typescript/httpclient/src/payload/LLMQueryRequestPayload.ts new file mode 100644 index 000000000..cb7b9ec7c --- /dev/null +++ b/lib/typescript/httpclient/src/payload/LLMQueryRequestPayload.ts @@ -0,0 +1,3 @@ +export interface LLMQueryRequestPayload { + query: string; +} diff --git a/lib/typescript/httpclient/src/payload/LLMQueryResponsePayload.ts b/lib/typescript/httpclient/src/payload/LLMQueryResponsePayload.ts new file mode 100644 index 000000000..6780fd96a --- /dev/null +++ b/lib/typescript/httpclient/src/payload/LLMQueryResponsePayload.ts @@ -0,0 +1,6 @@ +export interface LLMQueryResponsePayload { + answer: { + query: string; + result: string; + }; +} diff --git a/lib/typescript/httpclient/src/payload/LLMStatsPayload.ts b/lib/typescript/httpclient/src/payload/LLMStatsPayload.ts new file mode 100644 index 000000000..c0dda67a5 --- /dev/null +++ b/lib/typescript/httpclient/src/payload/LLMStatsPayload.ts @@ -0,0 +1,3 @@ +export interface LLMSStatsPayload { + embeddings: number; +} diff --git a/lib/typescript/httpclient/src/payload/index.ts b/lib/typescript/httpclient/src/payload/index.ts index e1696f2dd..94aa9a34a 100644 --- a/lib/typescript/httpclient/src/payload/index.ts +++ b/lib/typescript/httpclient/src/payload/index.ts @@ -37,3 +37,11 @@ export * from './CreateTopicPayload'; export * from './GetStreamInfoPayload'; export * from './DeleteStreamPayload'; export * from './CreateStreamPayload'; +export * from './LLMConsumerCreateRequestPayload'; +export * from './LLMConsumerCreateResponsePayload'; +export * from './LLMConsumerListPayload'; +export * from './LLMInfoPayload'; +export * from './LLMStatsPayload'; +export * from './LLMConsumersDeletePayload'; +export * from './LLMQueryResponsePayload'; +export * from './LLMQueryRequestPayload'; diff --git a/lib/typescript/model/Streams.ts b/lib/typescript/model/Streams.ts index aee9ed512..749f887f9 100644 --- a/lib/typescript/model/Streams.ts +++ b/lib/typescript/model/Streams.ts @@ -8,6 +8,9 @@ export interface Streams { schemas: { [topicName: string]: Schema; }; + schemasVersions: { + [topicName: string]: string[]; + }; streamsInfo: { [streamName: string]: StreamInfo; }; diff --git a/lib/typescript/translations/translations.ts b/lib/typescript/translations/translations.ts index 89529fc01..938f8780d 100644 --- a/lib/typescript/translations/translations.ts +++ b/lib/typescript/translations/translations.ts @@ -608,6 +608,16 @@ const resources = { noWebhooksText: `You don't have any Webhooks installed, please `, customHeader: 'Customer Header', signKey: 'Sign key', + noLLMs: 'No LLMs Found', + noLLMsText: `You don't have any LLMs installed, please `, + noLLMConsumers: 'No LLM Consumers Found', + noLLMConsumersText: `You don't have any LLM Consumers installed, please `, + llmConsumerNameExplanation: 'The name of the LLM Consumer', + llmConsumerTopicNameExplanation: 'The name of the topic to which the LLM Consumer is subscribed', + llmConsumerTextFieldExplanation: 'The text field of the LLM Consumer', + llmConsumerMetadataFieldsExplanation: 'The metadata fields of the LLM Consumer', + llmConsumerCreatedSuccessfully: 'LLM Consumer created successfully', + llmConsumerTypeExplanation: 'The type serialization of the LLM Consumer', }, }, de: { @@ -1229,6 +1239,16 @@ const resources = { noWebhooksText: 'Sie haben keine Webhooks installiert, bitte ', customHeader: 'Kundenkopfzeile', signKey: 'Signierschlüssel', + noLLMs: 'Keine LLMs gefunden', + noLLMsText: `Sie haben keine LLMs installiert, bitte `, + noLLMConsumers: 'Keine LLM Consumers gefunden', + noLLMConsumersText: `Sie haben keine LLM Consumers installiert, bitte `, + llmConsumerNameExplanation: 'Der Name des LLM Consumer', + llmConsumerTopicNameExplanation: 'Der Name des LLM Consumer Topics', + llmConsumerTextFieldExplanation: 'Das Textfeld des LLM Consumer', + llmConsumerMetadataFieldsExplanation: 'Die Metadatenfelder des LLM Consumer', + llmConsumerCreatedSuccessfully: 'LLM Consumer erfolgreich erstellt', + llmConsumerTypeExplanation: 'Der Typ des LLM Consumer', }, }, fr: { @@ -1843,6 +1863,16 @@ const resources = { noWebhooksText: `Vous n'avez pas de Webhooks installé, veuillez vous `, customHeader: 'En-tête du client', signKey: 'Touche de signature', + noLLMs: 'Pas de LLMs trouvés', + noLLMsText: `Vous n'avez pas de LLMs installé, veuillez vous `, + noLLMConsumers: 'Pas de LLM Consumers trouvés', + noLLMConsumersText: `Vous n'avez pas de LLM Consumers installé, veuillez vous `, + llmConsumerNameExplanation: 'Le nom du LLM Consumer', + llmConsumerTopicNameExplanation: 'Le nom du topic du LLM Consumer', + llmConsumerTextFieldExplanation: 'Le champ de texte du LLM Consumer', + llmConsumerMetadataFieldsExplanation: 'Les champs de métadonnées du LLM Consumer', + llmConsumerCreatedSuccessfully: 'LLM Consumer créé avec succès', + llmConsumerTypeExplanation: 'Le type du LLM Consumer', }, }, es: { @@ -2460,6 +2490,16 @@ const resources = { noWebhooksText: 'No tiene instalado ningún Webhooks, por favor, ', customHeader: 'Cabecera del cliente', signKey: 'Clave de la firma', + noLLMs: 'No se han encontrado LLMs', + noLLMsText: `No tiene instalado ningún LLMs, por favor, `, + noLLMConsumers: 'No se han encontrado LLM Consumers', + noLLMConsumersText: `No tiene instalado ningún LLM Consumer, por favor, `, + llmConsumerNameExplanation: 'El nombre del LLM Consumer', + llmConsumerTopicNameExplanation: 'El nombre del topic del LLM Consumer', + llmConsumerTextFieldExplanation: 'El campo de texto del LLM Consumer', + llmConsumerMetadataFieldsExplanation: 'Los campos de metadatos del LLM Consumer', + llmConsumerCreatedSuccessfully: 'LLM Consumer creado con éxito', + llmConsumerTypeExplanation: 'El tipo de serialización del LLM Consumer', }, }, }; From 40f7aa48587536aaf22169e16f2d69d8939d5826 Mon Sep 17 00:00:00 2001 From: Aitor Algorta Date: Fri, 16 Feb 2024 11:27:23 +0100 Subject: [PATCH 16/37] [#4141] Schema Registry Manager (#4143) --- .github/workflows/main.yml | 10 + .../schema-registry-manager/Dockerfile | 19 ++ .../schema-registry-manager/Makefile | 6 + .../schema-registry-manager/helm/BUILD | 3 + .../schema-registry-manager/helm/Chart.yaml | 5 + .../helm/templates/configmap.yaml | 10 + .../helm/templates/deployment.yaml | 48 ++++ .../helm/templates/service.yaml | 15 + .../schema-registry-manager/helm/values.yaml | 9 + .../schema-registry-manager/src/app.ts | 263 ++++++++++++++++++ .../schema-registry-manager/src/package.json | 17 ++ .../src/providers/karapace.ts | 121 ++++++++ .../schema-registry-manager/src/tsconfig.json | 10 + .../schema-registry-manager/src/types.ts | 4 + 14 files changed, 540 insertions(+) create mode 100644 backend/components/schema-registry-manager/Dockerfile create mode 100644 backend/components/schema-registry-manager/Makefile create mode 100644 backend/components/schema-registry-manager/helm/BUILD create mode 100644 backend/components/schema-registry-manager/helm/Chart.yaml create mode 100644 backend/components/schema-registry-manager/helm/templates/configmap.yaml create mode 100644 backend/components/schema-registry-manager/helm/templates/deployment.yaml create mode 100644 backend/components/schema-registry-manager/helm/templates/service.yaml create mode 100644 backend/components/schema-registry-manager/helm/values.yaml create mode 100644 backend/components/schema-registry-manager/src/app.ts create mode 100644 backend/components/schema-registry-manager/src/package.json create mode 100644 backend/components/schema-registry-manager/src/providers/karapace.ts create mode 100644 backend/components/schema-registry-manager/src/tsconfig.json create mode 100644 backend/components/schema-registry-manager/src/types.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9348a3f58..02c8436e4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -54,6 +54,16 @@ jobs: run: | bazel test --test_tag_filters=-lint //... + - name: Cleanup space + run: | + df -h + sudo apt-get autoremove -y + sudo apt-get clean + docker images prune -a + sudo rm -rf /usr/local/share/powershell + sudo rm -rf /opt/hostedtoolcache + df -h + - name: Build all artifacts run: | bazel build //... diff --git a/backend/components/schema-registry-manager/Dockerfile b/backend/components/schema-registry-manager/Dockerfile new file mode 100644 index 000000000..e06995486 --- /dev/null +++ b/backend/components/schema-registry-manager/Dockerfile @@ -0,0 +1,19 @@ +FROM node:18 + +WORKDIR /app + +COPY ./src/package*.json ./ +COPY ./src/tsconfig*.json ./ + +RUN npm install +RUN npm install typescript -g + +COPY ./src/app.ts ./ +COPY ./src/types.ts ./ +COPY ./src/providers/karapace.ts ./providers/ + +RUN tsc + +EXPOSE 3000 + +CMD [ "node", "app.js" ] diff --git a/backend/components/schema-registry-manager/Makefile b/backend/components/schema-registry-manager/Makefile new file mode 100644 index 000000000..3aa78401e --- /dev/null +++ b/backend/components/schema-registry-manager/Makefile @@ -0,0 +1,6 @@ +build: + docker build -t schema-registry-manager . + +release: build + docker tag schema-registry-manager ghcr.io/airyhq/backend/schema-registry-manager:release + docker push ghcr.io/airyhq/backend/schema-registry-manager:release diff --git a/backend/components/schema-registry-manager/helm/BUILD b/backend/components/schema-registry-manager/helm/BUILD new file mode 100644 index 000000000..8d6495211 --- /dev/null +++ b/backend/components/schema-registry-manager/helm/BUILD @@ -0,0 +1,3 @@ +load("//tools/build:helm.bzl", "helm_ruleset_core_version") + +helm_ruleset_core_version() \ No newline at end of file diff --git a/backend/components/schema-registry-manager/helm/Chart.yaml b/backend/components/schema-registry-manager/helm/Chart.yaml new file mode 100644 index 000000000..7205b553c --- /dev/null +++ b/backend/components/schema-registry-manager/helm/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v2 +appVersion: "1.0" +description: Schema registry component to manage different providers +name: schema-registry-manager +version: 1.0 diff --git a/backend/components/schema-registry-manager/helm/templates/configmap.yaml b/backend/components/schema-registry-manager/helm/templates/configmap.yaml new file mode 100644 index 000000000..05de4d589 --- /dev/null +++ b/backend/components/schema-registry-manager/helm/templates/configmap.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.component }} + labels: + core.airy.co/managed: "true" + core.airy.co/mandatory: "{{ .Values.mandatory }}" + core.airy.co/component: "{{ .Values.component }}" + annotations: + core.airy.co/enabled: "{{ .Values.enabled }}" diff --git a/backend/components/schema-registry-manager/helm/templates/deployment.yaml b/backend/components/schema-registry-manager/helm/templates/deployment.yaml new file mode 100644 index 000000000..b90214517 --- /dev/null +++ b/backend/components/schema-registry-manager/helm/templates/deployment.yaml @@ -0,0 +1,48 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.component }} + labels: + app: {{ .Values.component }} + core.airy.co/managed: "true" + core.airy.co/mandatory: "{{ .Values.mandatory }}" + core.airy.co/component: {{ .Values.component }} +spec: + replicas: {{ if .Values.enabled }} 1 {{ else }} 0 {{ end }} + selector: + matchLabels: + app: {{ .Values.component }} + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: {{ .Values.component }} + spec: + containers: + - name: app + image: "ghcr.io/airyhq/{{ .Values.image }}:{{ .Values.imageTag }}" + imagePullPolicy: Always + envFrom: + - configMapRef: + name: security + - configMapRef: + name: kafka-config + - configMapRef: + name: {{ .Values.component }} + env: + - name: KAFKA_TOPIC_NAME + value: {{ .Values.kafka.topic }} + livenessProbe: + httpGet: + path: /actuator/health + port: {{ .Values.port }} + httpHeaders: + - name: Health-Check + value: health-check + initialDelaySeconds: 43200 + periodSeconds: 10 + failureThreshold: 3 diff --git a/backend/components/schema-registry-manager/helm/templates/service.yaml b/backend/components/schema-registry-manager/helm/templates/service.yaml new file mode 100644 index 000000000..4d636e8b2 --- /dev/null +++ b/backend/components/schema-registry-manager/helm/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.component }} + labels: + app: {{ .Values.component }} +spec: + type: ClusterIP + clusterIP: None + ports: + - name: {{ .Values.component }} + port: 80 + targetPort: {{ .Values.port }} + selector: + app: {{ .Values.component }} diff --git a/backend/components/schema-registry-manager/helm/values.yaml b/backend/components/schema-registry-manager/helm/values.yaml new file mode 100644 index 000000000..200ef3ca3 --- /dev/null +++ b/backend/components/schema-registry-manager/helm/values.yaml @@ -0,0 +1,9 @@ +component: schema-registry-manager +mandatory: false +enabled: false +image: backend/schema-registry-manager +imageTag: release +port: 3000 +resources: +kafka: + topic: application.communication.messages \ No newline at end of file diff --git a/backend/components/schema-registry-manager/src/app.ts b/backend/components/schema-registry-manager/src/app.ts new file mode 100644 index 000000000..fb7f384e3 --- /dev/null +++ b/backend/components/schema-registry-manager/src/app.ts @@ -0,0 +1,263 @@ +import dotenv from 'dotenv'; +import express, {Express, Request as ExpressRequest, Response as ExpressResponse} from 'express'; +import http from 'http'; +import cors from 'cors'; + +import {SchemaProvider} from './types'; +import { + checkCompatibilityOfNewSchema, + createSchema, + deleteSchema, + getLastMessage, + getSchemaInfo, + getSchemaVersions, + getSchemas, + updateSchema, +} from './providers/karapace'; + +dotenv.config(); + +const app: Express = express(); +const port = process.env.PORT || 3000; +const bodyParser = require('body-parser'); +const currentProvider: SchemaProvider = SchemaProvider.karapace; + +// Middleware +app.use(bodyParser.json()); + +// CORS options +const corsOptions = { + origin: 'http://localhost:8080', +}; + +// Use cors middleware with the specified options +app.use(cors(corsOptions)); + +app.get('/schemas.provider', (req: ExpressRequest, res: ExpressResponse) => { + res.status(200).send(currentProvider); +}); + +app.get('/schemas.list', (req: ExpressRequest, res: ExpressResponse) => { + switch (currentProvider) { + case SchemaProvider.karapace: + getSchemas(req.get('host') as string) + .then((response: string[]) => { + res.send(response); + }) + .catch((e: any) => { + res.status(500).send(e); + }); + break; + default: + res.status(404).send('Provider Not Found'); + break; + } +}); + +app.get('/schemas.versions', (req: ExpressRequest, res: ExpressResponse) => { + if (!req.query.topicName) { + res.status(400).send('Missing topicName'); + return; + } + + switch (currentProvider) { + case SchemaProvider.karapace: + getSchemaVersions(req.get('host') as string, req.query.topicName as string) + .then((response: any) => { + res.status(200).send(response); + }) + .catch((e: any) => { + res.status(500).send(e); + }); + break; + default: + res.status(404).send('Provider Not Found'); + break; + } +}); + +app.get('/schemas.info', (req: ExpressRequest, res: ExpressResponse) => { + if (!req.query.topicName) { + res.status(400).send('Missing topicName'); + return; + } + + let version = 'latest'; + if (req.query.version) { + version = req.query.version as string; + } + + switch (currentProvider) { + case SchemaProvider.karapace: + getSchemaInfo(req.get('host') as string, req.query.topicName as string, version) + .then((response: any) => { + res.status(200).send(response); + }) + .catch((e: any) => { + res.status(500).send(e); + }); + break; + default: + res.status(404).send('Provider Not Found'); + break; + } +}); + +app.post('/schemas.update', (req: ExpressRequest, res: ExpressResponse) => { + if (!req.query.topicName) { + res.status(400).send('Missing topicName'); + return; + } + if (!req.body.schema) { + res.status(400).send('Missing schema'); + return; + } + + switch (currentProvider) { + case SchemaProvider.karapace: + updateSchema(req.get('host') as string, req.query.topicName as string, req.body.schema as string) + .then((response: any) => { + res.status(200).send(response); + }) + .catch((e: any) => { + res.status(500).send(e); + }); + break; + default: + res.status(404).send('Provider Not Found'); + break; + } +}); + +app.post('/schemas.create', (req: ExpressRequest, res: ExpressResponse) => { + if (!req.query.topicName) { + res.status(400).send('Missing topicName'); + return; + } + if (!req.body.schema) { + res.status(400).send('Missing schema'); + return; + } + + switch (currentProvider) { + case SchemaProvider.karapace: + createSchema(req.get('host') as string, req.query.topicName as string, req.body.schema as string) + .then((response: any) => { + res.status(200).send(response); + }) + .catch((e: any) => { + res.status(500).send(e); + }); + break; + default: + res.status(404).send('Provider Not Found'); + break; + } +}); + +app.post('/schemas.compatibility', (req: ExpressRequest, res: ExpressResponse) => { + if (!req.query.topicName) { + res.status(400).send('Missing topicName'); + return; + } + if (!req.query.version) { + res.status(400).send('Missing version'); + return; + } + if (!req.body.schema) { + res.status(400).send('Missing schema'); + return; + } + + switch (currentProvider) { + case SchemaProvider.karapace: + checkCompatibilityOfNewSchema( + req.get('host') as string, + req.query.topicName as string, + req.body.schema as string, + req.query.version as string + ) + .then((response: any) => { + res.status(200).send(response); + }) + .catch((e: any) => { + res.status(500).send(e); + }); + break; + default: + res.status(404).send('Provider Not Found'); + break; + } +}); + +app.post('/schemas.delete', (req: ExpressRequest, res: ExpressResponse) => { + if (!req.query.topicName) { + res.status(400).send('Missing topicName'); + return; + } + + switch (currentProvider) { + case SchemaProvider.karapace: + deleteSchema(req.get('host') as string, req.query.topicName as string) + .then((response: any) => { + res.status(200).send(response); + }) + .catch((e: any) => { + res.status(500).send(e); + }); + break; + default: + res.status(404).send('Provider Not Found'); + break; + } +}); + +app.get('/schemas.lastMessage', (req: ExpressRequest, res: ExpressResponse) => { + if (!req.query.topicName) { + res.status(400).send('Missing topicName'); + return; + } + + switch (currentProvider) { + case SchemaProvider.karapace: + getLastMessage(req.get('host') as string, req.query.topicName as string) + .then((response: any) => { + res.status(200).send(response); + }) + .catch((e: any) => { + res.status(500).send(e); + }); + break; + default: + res.status(404).send('Provider Not Found'); + break; + } +}); + +async function startHealthcheck() { + const server = http.createServer((req: any, res: any) => { + if (req.url === '/actuator/health' && req.method === 'GET') { + const response = {status: 'UP'}; + const jsonResponse = JSON.stringify(response); + + res.writeHead(200, {'Content-Type': 'application/json'}); + res.end(jsonResponse); + } else { + res.writeHead(404, {'Content-Type': 'text/plain'}); + res.end('Not Found'); + } + }); + + server.listen(80, () => { + console.log('Health-check started'); + }); +} + +async function main() { + startHealthcheck(); + app.listen(port, () => { + console.log(`Server is running on http://localhost:${port}`); + }); +} + +main().catch(console.error); diff --git a/backend/components/schema-registry-manager/src/package.json b/backend/components/schema-registry-manager/src/package.json new file mode 100644 index 000000000..970c2ea5f --- /dev/null +++ b/backend/components/schema-registry-manager/src/package.json @@ -0,0 +1,17 @@ +{ + "dependencies": { + "@types/express": "^4.17.21", + "@types/node": "^20.10.3", + "cors": "^2.8.5", + "dotenv": "^16.4.2", + "express": "^4.18.2", + "node-fetch": "^2.6.1" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/node": "^20.10.3", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" + } +} diff --git a/backend/components/schema-registry-manager/src/providers/karapace.ts b/backend/components/schema-registry-manager/src/providers/karapace.ts new file mode 100644 index 000000000..487aa31a8 --- /dev/null +++ b/backend/components/schema-registry-manager/src/providers/karapace.ts @@ -0,0 +1,121 @@ +export async function getSchemas(host: string) { + return getData(host, 'subjects').then(response => { + return response; + }); +} + +export async function getSchemaVersions(host: string, topicName: string) { + return getData(host, `subjects/${topicName}/versions`).then(response => { + if (response.error_code && response.error_code.toString().includes('404') && !topicName.includes('-value')) { + return Promise.reject('404 Not Found'); + } + return response; + }); +} + +export async function getSchemaInfo(host: string, topicName: string, version: string) { + return getData(host, `subjects/${topicName}/versions/${version}`).then(response => { + if (response.error_code && response.error_code.toString().includes('404') && !topicName.includes('-value')) { + return Promise.reject('404 Not Found'); + } + return response; + }); +} + +export async function updateSchema(host: string, topicName: string, schema: string) { + const body = { + schema: JSON.stringify({...JSON.parse(schema)}), + }; + return postData(host, `subjects/${topicName}/versions`, body).then(response => { + if (response.error_code && response.error_code.toString().includes('404') && !topicName.includes('-value')) { + return Promise.reject('404 Not Found'); + } + if (response.id) return response; + if (response.message) return Promise.reject(response.message); + return Promise.reject('Unknown Error'); + }); +} + +export async function createSchema(host: string, topicName: string, schema: string) { + const body = { + schema: JSON.stringify({...JSON.parse(schema)}), + }; + return postData(host, `subjects/${topicName}/versions`, body) + .then(response => { + if (response.id) return response; + if (response.message) return Promise.reject(response.message); + return Promise.reject('Unknown Error'); + }) + .catch(e => { + return Promise.reject(e); + }); +} + +export async function checkCompatibilityOfNewSchema(host: string, topicName: string, schema: string, version: string) { + const body = { + schema: JSON.stringify({...JSON.parse(schema)}), + }; + + return postData(host, `compatibility/subjects/${topicName}/versions/${version}`, body) + .then(response => { + if (response.error_code && response.error_code.toString().includes('404') && !topicName.includes('-value')) { + return Promise.reject('404 Not Found'); + } + if (response.is_compatible !== undefined) { + if (response.is_compatible === true) { + return response; + } + return Promise.reject('Schema Not Compatible'); + } + if (response.message) return Promise.reject(response.message); + return Promise.reject('Unknown Error'); + }) + .catch(e => { + return Promise.reject(e); + }); +} + +export async function deleteSchema(host: string, topicName: string) { + return deleteData(host, `subjects/${topicName}`).then(response => { + if (response.error_code && response.error_code.toString().includes('404') && !topicName.includes('-value')) { + return Promise.reject('404 Not Found'); + } + return response; + }); +} + +export async function getLastMessage(host: string, topicName: string) { + const body = { + ksql: `PRINT '${topicName}' FROM BEGINNING LIMIT 1;`, + streamsProperties: {}, + }; + return postData(host, 'query', body).then(response => { + return response; + }); +} + +async function getData(host: string, url: string) { + const response = await fetch('https://' + host + '/' + url, { + method: 'GET', + }); + return response.json(); +} + +async function deleteData(host: string, url: string) { + const response = await fetch('https://' + host + '/' + url, { + method: 'DELETE', + }); + return response.json(); +} + +async function postData(host: string, url: string, body: any) { + const response = await fetch('https://' + host + '/' + url, { + method: 'POST', + headers: { + 'Content-Type': 'application/vnd.schemaregistry.v1+json', + }, + body: JSON.stringify(body), + }); + + return response.json(); +} diff --git a/backend/components/schema-registry-manager/src/tsconfig.json b/backend/components/schema-registry-manager/src/tsconfig.json new file mode 100644 index 000000000..8975f6604 --- /dev/null +++ b/backend/components/schema-registry-manager/src/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2016", + "module": "commonjs", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + } +} \ No newline at end of file diff --git a/backend/components/schema-registry-manager/src/types.ts b/backend/components/schema-registry-manager/src/types.ts new file mode 100644 index 000000000..53776abb6 --- /dev/null +++ b/backend/components/schema-registry-manager/src/types.ts @@ -0,0 +1,4 @@ +export enum SchemaProvider { + karapace = 'karapace', + confluentCloud = 'confluent-cloud', +} From dd7daddc5399f6141105382df0aac099a9270ee1 Mon Sep 17 00:00:00 2001 From: Aitor Algorta Date: Fri, 16 Feb 2024 11:40:05 +0100 Subject: [PATCH 17/37] [#4144] Adapt Frontend with Schema Manager (#4145) * wip * update schemas api * change content type for sschemas manager --- .../src/actions/streams/index.ts | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/frontend/control-center/src/actions/streams/index.ts b/frontend/control-center/src/actions/streams/index.ts index 26619e2e6..2813ef3e8 100644 --- a/frontend/control-center/src/actions/streams/index.ts +++ b/frontend/control-center/src/actions/streams/index.ts @@ -69,14 +69,15 @@ export const getTopicInfo = (topicName: string) => async (dispatch: Dispatch async (dispatch: Dispatch) => { - return getData('subjects').then(response => { + return getData('schemas.list').then(response => { + console.log(response); dispatch(setTopicSchemasAction(response)); return Promise.resolve(true); }); }; export const getSchemaVersions = (topicName: string) => async (dispatch: Dispatch) => { - return getData(`subjects/${topicName}/versions`).then(response => { + return getData(`schemas.versions?topicName=${topicName}`).then(response => { if (response.error_code && response.error_code.toString().includes('404') && !topicName.includes('-value')) { return Promise.reject('404 Not Found'); } else { @@ -91,7 +92,7 @@ export const getSchemaInfo = (topicName: string, version?: string) => async (dis if (version) { v = version; } - return getData(`subjects/${topicName}/versions/${v}`).then(response => { + return getData(`schemas.info?topicName=${topicName}&version=${v}`).then(response => { if (response.error_code && response.error_code.toString().includes('404') && !topicName.includes('-value')) { return Promise.reject('404 Not Found'); } else { @@ -105,7 +106,7 @@ export const setSchemaSchema = (topicName: string, schema: string) => async () = const body = { schema: JSON.stringify({...JSON.parse(schema)}), }; - return postData(`subjects/${topicName}/versions`, body).then(response => { + return postData(`schemas.update?topicName=${topicName}`, body).then(response => { if (response.error_code && response.error_code.toString().includes('404') && !topicName.includes('-value')) { return Promise.reject('404 Not Found'); } @@ -119,7 +120,7 @@ export const createSchema = (topicName: string, schema: string) => async () => { const body = { schema: JSON.stringify({...JSON.parse(schema)}), }; - return postData(`subjects/${topicName}/versions`, body) + return postData(`schemas.create?topicName=${topicName}`, body) .then(response => { if (response.id) return Promise.resolve(true); if (response.message) return Promise.reject(response.message); @@ -134,7 +135,7 @@ export const checkCompatibilityOfNewSchema = (topicName: string, schema: string, const body = { schema: JSON.stringify({...JSON.parse(schema)}), }; - return postData(`compatibility/subjects/${topicName}/versions/${version}`, body) + return postData(`schemas.compatibility?topicName=${topicName}&version=${version}`, body) .then(response => { if (response.error_code && response.error_code.toString().includes('404') && !topicName.includes('-value')) { return Promise.reject('404 Not Found'); @@ -154,7 +155,7 @@ export const checkCompatibilityOfNewSchema = (topicName: string, schema: string, }; export const deleteSchema = (topicName: string) => async () => { - return deleteData(`subjects/${topicName}`).then(response => { + return deleteData(`schemas.delete?topicName=${topicName}`).then(response => { if (response.error_code && response.error_code.toString().includes('404') && !topicName.includes('-value')) { return Promise.reject('404 Not Found'); } @@ -163,11 +164,7 @@ export const deleteSchema = (topicName: string) => async () => { }; export const getLastMessage = (topicName: string) => async (dispatch: Dispatch) => { - const body = { - ksql: `PRINT '${topicName}' FROM BEGINNING LIMIT 1;`, - streamsProperties: {}, - }; - return postData('query', body).then(response => { + return getData(`schemas.lastMessage?topicName=${topicName}`).then(response => { dispatch(setLastMessage(response)); return Promise.resolve(true); }); @@ -193,11 +190,10 @@ async function postData(url: string, body: any) { const response = await fetch(apiHostUrl + '/' + url, { method: 'POST', headers: { - 'Content-Type': 'application/vnd.schemaregistry.v1+json', + 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); - return response.json(); } From 81e21a220f5868c9d38c77110cca9ebd20a484ac Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Tue, 26 Mar 2024 16:05:27 +0100 Subject: [PATCH 18/37] [#3945] Slack connector (#4146) --- .../schema-registry-manager/helm/BUILD | 2 +- .../src/components/ChannelAvatar/index.tsx | 3 ++ lib/typescript/assets/images/icons/slack.svg | 31 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 lib/typescript/assets/images/icons/slack.svg diff --git a/backend/components/schema-registry-manager/helm/BUILD b/backend/components/schema-registry-manager/helm/BUILD index 8d6495211..45805d5f1 100644 --- a/backend/components/schema-registry-manager/helm/BUILD +++ b/backend/components/schema-registry-manager/helm/BUILD @@ -1,3 +1,3 @@ load("//tools/build:helm.bzl", "helm_ruleset_core_version") -helm_ruleset_core_version() \ No newline at end of file +helm_ruleset_core_version() diff --git a/frontend/control-center/src/components/ChannelAvatar/index.tsx b/frontend/control-center/src/components/ChannelAvatar/index.tsx index 7a981bb04..b1d6c33bf 100644 --- a/frontend/control-center/src/components/ChannelAvatar/index.tsx +++ b/frontend/control-center/src/components/ChannelAvatar/index.tsx @@ -26,6 +26,7 @@ import {ReactComponent as ChromaAvatar} from 'assets/images/icons/chroma.svg'; import {ReactComponent as MosaicAvatar} from 'assets/images/icons/mosaic.svg'; import {ReactComponent as WeaviateAvatar} from 'assets/images/icons/weaviate.svg'; import {ReactComponent as GmailAvatar} from 'assets/images/icons/gmail.svg'; +import {ReactComponent as SlackAvatar} from 'assets/images/icons/slack.svg'; import {Channel, Source} from 'model'; import styles from './index.module.scss'; @@ -132,6 +133,8 @@ export const getChannelAvatar = (source: string) => { case Source.gmail: case 'GMail connector': return ; + case 'Slack connector': + return ; default: return ; diff --git a/lib/typescript/assets/images/icons/slack.svg b/lib/typescript/assets/images/icons/slack.svg new file mode 100644 index 000000000..1b18e66c1 --- /dev/null +++ b/lib/typescript/assets/images/icons/slack.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + From 0a9c1547be9acb27a361675fcf8c7390b5b1b424 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:57:45 +0200 Subject: [PATCH 19/37] Bump word-wrap from 1.2.3 to 1.2.4 (#4112) Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/jonschlinkert/word-wrap/releases) - [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4) --- updated-dependencies: - dependency-name: word-wrap dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8818dbae4..62cc18ec3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9206,9 +9206,9 @@ wildcard@^2.0.0: integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== word-wrap@^1.2.3, word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + version "1.2.4" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" + integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== wrap-ansi@^6.2.0: version "6.2.0" From bfada800eb7970a7411b912cfde5a623df5936af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:58:28 +0200 Subject: [PATCH 20/37] Bump sass-loader from 13.1.0 to 13.3.2 (#4103) Bumps [sass-loader](https://github.com/webpack-contrib/sass-loader) from 13.1.0 to 13.3.2. - [Release notes](https://github.com/webpack-contrib/sass-loader/releases) - [Changelog](https://github.com/webpack-contrib/sass-loader/blob/master/CHANGELOG.md) - [Commits](https://github.com/webpack-contrib/sass-loader/compare/v13.1.0...v13.3.2) --- updated-dependencies: - dependency-name: sass-loader dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 346b78c5e..767ef8956 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,7 @@ "react-hot-loader": "^4.13.0", "react-test-renderer": "^18.2.0", "sass": "^1.55.0", - "sass-loader": "^13.0.2", + "sass-loader": "^13.3.2", "style-loader": "^3.3.1", "terser-webpack-plugin": "^5.3.6", "typescript": "^4.8.4", diff --git a/yarn.lock b/yarn.lock index 62cc18ec3..694602f82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6126,11 +6126,6 @@ kleur@^4.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== -klona@^2.0.4: - version "2.0.5" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc" - integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ== - lazy-ass@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" @@ -7884,12 +7879,11 @@ safe-regex-test@^1.0.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sass-loader@^13.0.2: - version "13.1.0" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-13.1.0.tgz#e5b9acf14199a9bc6eaed7a0b8b23951c2cebf6f" - integrity sha512-tZS1RJQ2n2+QNyf3CCAo1H562WjL/5AM6Gi8YcPVVoNxQX8d19mx8E+8fRrMWsyc93ZL6Q8vZDSM0FHVTJaVnQ== +sass-loader@^13.3.2: + version "13.3.2" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-13.3.2.tgz#460022de27aec772480f03de17f5ba88fa7e18c6" + integrity sha512-CQbKl57kdEv+KDLquhC+gE3pXt74LEAzm+tzywcA0/aHZuub8wTErbjAoNI57rPUWRYRNC5WUnNl8eGJNbDdwg== dependencies: - klona "^2.0.4" neo-async "^2.6.2" sass@^1.55.0: From d3f47b8fd6cde98131739767a077fa636f4524f0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:59:15 +0200 Subject: [PATCH 21/37] Bump semver from 5.7.1 to 5.7.2 in /docs (#4110) Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2. - [Release notes](https://github.com/npm/node-semver/releases) - [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md) - [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2) --- updated-dependencies: - dependency-name: semver dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/yarn.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/yarn.lock b/docs/yarn.lock index b57c514a2..9d7c592d3 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -6522,19 +6522,19 @@ semver-diff@^3.1.1: semver "^6.3.0" semver@^5.4.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.3.2, semver@^7.3.4, semver@^7.3.7, semver@^7.3.8: - version "7.3.8" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" From 17b0e197923f4890f665add2cd9101afaf40d442 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:59:59 +0200 Subject: [PATCH 22/37] Bump @svgr/plugin-svgo from 6.5.1 to 8.1.0 (#4114) Bumps [@svgr/plugin-svgo](https://github.com/gregberge/svgr) from 6.5.1 to 8.1.0. - [Release notes](https://github.com/gregberge/svgr/releases) - [Changelog](https://github.com/gregberge/svgr/blob/main/CHANGELOG.md) - [Commits](https://github.com/gregberge/svgr/compare/v6.5.1...v8.1.0) --- updated-dependencies: - dependency-name: "@svgr/plugin-svgo" dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 133 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 126 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 767ef8956..8a76eb974 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "@babel/preset-typescript": "^7.18.6", "@bazel/typescript": "5.6.0", "@hot-loader/react-dom": "^17.0.2", - "@svgr/plugin-svgo": "^6.5.0", + "@svgr/plugin-svgo": "^8.1.0", "@svgr/webpack": "^6.5.0", "@testing-library/dom": "^8.19.0", "@testing-library/react": "13.4.0", diff --git a/yarn.lock b/yarn.lock index 694602f82..fe40f2ce9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1672,7 +1672,7 @@ "@svgr/hast-util-to-babel-ast" "^6.5.1" svg-parser "^2.0.4" -"@svgr/plugin-svgo@^6.5.0", "@svgr/plugin-svgo@^6.5.1": +"@svgr/plugin-svgo@^6.5.1": version "6.5.1" resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-6.5.1.tgz#0f91910e988fc0b842f88e0960c2862e022abe84" integrity sha512-omvZKf8ixP9z6GWgwbtmP9qQMPX4ODXi+wzbVZgomNFsUIlHA1sf4fThdwTWSsZGgvGAG6yE+b/F5gWUkcZ/iQ== @@ -1681,6 +1681,15 @@ deepmerge "^4.2.2" svgo "^2.8.0" +"@svgr/plugin-svgo@^8.1.0": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz#b115b7b967b564f89ac58feae89b88c3decd0f00" + integrity sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA== + dependencies: + cosmiconfig "^8.1.3" + deepmerge "^4.3.1" + svgo "^3.0.2" + "@svgr/webpack@^6.5.0": version "6.5.1" resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-6.5.1.tgz#ecf027814fc1cb2decc29dc92f39c3cf691e40e8" @@ -2633,6 +2642,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + aria-query@^5.0.0: version "5.1.3" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" @@ -3421,6 +3435,16 @@ cosmiconfig@^7.0.1: path-type "^4.0.0" yaml "^1.10.0" +cosmiconfig@^8.1.3: + version "8.2.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.2.0.tgz#f7d17c56a590856cd1e7cee98734dca272b0d8fd" + integrity sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ== + dependencies: + import-fresh "^3.2.1" + js-yaml "^4.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" @@ -3460,6 +3484,17 @@ css-select@^4.1.3: domutils "^2.8.0" nth-check "^2.0.1" +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + css-tree@^1.1.2, css-tree@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" @@ -3468,7 +3503,23 @@ css-tree@^1.1.2, css-tree@^1.1.3: mdn-data "2.0.14" source-map "^0.6.1" -css-what@^6.0.1: +css-tree@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" + integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== + dependencies: + mdn-data "2.0.30" + source-map-js "^1.0.1" + +css-tree@~2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.2.1.tgz#36115d382d60afd271e377f9c5f67d02bd48c032" + integrity sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA== + dependencies: + mdn-data "2.0.28" + source-map-js "^1.0.1" + +css-what@^6.0.1, css-what@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== @@ -3490,6 +3541,13 @@ csso@^4.2.0: dependencies: css-tree "^1.1.2" +csso@^5.0.5: + version "5.0.5" + resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6" + integrity sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ== + dependencies: + css-tree "~2.2.0" + cssom@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36" @@ -3645,10 +3703,10 @@ deep-is@^0.1.3, deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -deepmerge@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" - integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== +deepmerge@^4.2.2, deepmerge@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== default-gateway@^6.0.3: version "6.0.3" @@ -3779,12 +3837,21 @@ dom-serializer@^1.0.1: domhandler "^4.2.0" entities "^2.0.0" +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + dom-walk@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84" integrity sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w== -domelementtype@^2.0.1, domelementtype@^2.2.0: +domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== @@ -3803,6 +3870,13 @@ domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: dependencies: domelementtype "^2.2.0" +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + domutils@^2.5.2, domutils@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" @@ -3812,6 +3886,15 @@ domutils@^2.5.2, domutils@^2.8.0: domelementtype "^2.2.0" domhandler "^4.2.0" +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + dot-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" @@ -3914,6 +3997,11 @@ entities@^2.0.0: resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== +entities@^4.2.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + entities@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174" @@ -5995,6 +6083,13 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -6397,6 +6492,16 @@ mdn-data@2.0.14: resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== +mdn-data@2.0.28: + version "2.0.28" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba" + integrity sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g== + +mdn-data@2.0.30: + version "2.0.30" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" + integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -8122,7 +8227,7 @@ sockjs@^0.3.24: uuid "^8.3.2" websocket-driver "^0.7.4" -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== @@ -8400,6 +8505,18 @@ svgo@^2.8.0: picocolors "^1.0.0" stable "^0.1.8" +svgo@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.0.2.tgz#5e99eeea42c68ee0dc46aa16da093838c262fe0a" + integrity sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^5.1.0" + css-tree "^2.2.1" + csso "^5.0.5" + picocolors "^1.0.0" + symbol-tree@^3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" From 05c1c84072b6b74b106aa3c2fd76f2cce0f4df68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 12:01:20 +0200 Subject: [PATCH 23/37] Bump @adobe/css-tools from 4.0.1 to 4.3.1 (#4116) Bumps [@adobe/css-tools](https://github.com/adobe/css-tools) from 4.0.1 to 4.3.1. - [Changelog](https://github.com/adobe/css-tools/blob/main/History.md) - [Commits](https://github.com/adobe/css-tools/commits) --- updated-dependencies: - dependency-name: "@adobe/css-tools" dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index fe40f2ce9..07b921b1e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3,9 +3,9 @@ "@adobe/css-tools@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.0.1.tgz#b38b444ad3aa5fedbb15f2f746dcd934226a12dd" - integrity sha512-+u76oB43nOHrF4DDWRLWDCtci7f3QJoEBigemIdIeTi1ODqjx6Tad9NCVnPRwewWlKkVab5PlK8DCtPTyX7S8g== + version "4.3.1" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.1.tgz#abfccb8ca78075a2b6187345c26243c1a0842f28" + integrity sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg== "@ampproject/remapping@^2.2.0": version "2.2.0" From fafddd7da1e4cf0ebeb6d05a7af5eae6016247ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 12:05:40 +0200 Subject: [PATCH 24/37] Bump github.com/cyphar/filepath-securejoin from 0.2.3 to 0.2.4 (#4118) Bumps [github.com/cyphar/filepath-securejoin](https://github.com/cyphar/filepath-securejoin) from 0.2.3 to 0.2.4. - [Release notes](https://github.com/cyphar/filepath-securejoin/releases) - [Commits](https://github.com/cyphar/filepath-securejoin/compare/v0.2.3...v0.2.4) --- updated-dependencies: - dependency-name: github.com/cyphar/filepath-securejoin dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 58 ++-------------------------------------------------------- go.sum | 26 +++----------------------- 2 files changed, 5 insertions(+), 79 deletions(-) diff --git a/go.mod b/go.mod index 37b2cb174..4e14d025b 100644 --- a/go.mod +++ b/go.mod @@ -6,26 +6,10 @@ go 1.18 // Automatically generated by running //tools/update-deps require ( - github.com/Masterminds/sprig v2.22.0+incompatible - github.com/TwinProduction/go-color v1.0.0 github.com/airyhq/airy/lib/go/httpclient v0.0.0-20230426122014-b3add0488aa1 - github.com/aws/aws-sdk-go v1.44.21 - github.com/aws/aws-sdk-go-v2/config v1.15.7 - github.com/aws/aws-sdk-go-v2/service/ec2 v1.44.0 - github.com/aws/aws-sdk-go-v2/service/eks v1.21.1 - github.com/aws/aws-sdk-go-v2/service/iam v1.18.5 + github.com/airyhq/airy/lib/go/payloads v0.0.0-20230426122014-b3add0488aa1 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/gorilla/mux v1.8.0 - github.com/kr/pretty v0.3.0 - github.com/mitchellh/go-homedir v1.1.0 - github.com/spf13/cobra v1.5.0 - github.com/spf13/viper v1.11.0 - github.com/thanhpk/randstr v1.0.4 - github.com/txn2/txeh v1.3.0 - goji.io v2.0.2+incompatible - golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 - gopkg.in/segmentio/analytics-go.v3 v3.1.0 - gopkg.in/yaml.v2 v2.4.0 k8s.io/api v0.24.2 k8s.io/apimachinery v0.24.2 k8s.io/client-go v0.24.2 @@ -33,38 +17,6 @@ require ( ) require ( - cloud.google.com/go v0.102.0 // indirect - cloud.google.com/go/compute v1.7.0 // indirect - cloud.google.com/go/iam v0.3.0 // indirect - cloud.google.com/go/storage v1.22.1 // indirect - github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect - github.com/BurntSushi/toml v1.1.0 // indirect - github.com/MakeNowJust/heredoc v1.0.0 // indirect - github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver v1.5.0 // indirect - github.com/Masterminds/semver/v3 v3.1.1 // indirect - github.com/Masterminds/sprig/v3 v3.2.2 // indirect - github.com/Masterminds/squirrel v1.5.3 // indirect - github.com/PuerkitoBio/purell v1.1.1 // indirect - github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect - github.com/airyhq/airy/lib/go/payloads v0.0.0-20230426122014-b3add0488aa1 // indirect - github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect - github.com/aws/aws-sdk-go-v2 v1.16.4 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.12.2 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.5 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.11 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.5 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.3.12 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.11.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.16.6 // indirect - github.com/aws/smithy-go v1.11.2 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect - github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect - github.com/containerd/containerd v1.6.6 // indirect - github.com/cyphar/filepath-securejoin v0.2.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.8.0 // indirect github.com/go-logr/logr v1.2.3 // indirect @@ -75,6 +27,7 @@ require ( github.com/golang/protobuf v1.5.2 // indirect github.com/google/gnostic v0.6.9 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/iancoleman/strcase v0.2.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kr/pretty v0.3.0 // indirect @@ -83,7 +36,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/rogpeppe/go-internal v1.8.1 // indirect - github.com/stretchr/testify v1.8.0 // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0 // indirect golang.org/x/sys v0.5.0 // indirect @@ -96,12 +48,6 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - helm.sh/helm/v3 v3.9.0 // indirect - k8s.io/apiextensions-apiserver v0.24.2 // indirect - k8s.io/apiserver v0.24.2 // indirect - k8s.io/cli-runtime v0.24.2 // indirect - k8s.io/component-base v0.24.2 // indirect - k8s.io/helm v2.17.0+incompatible // indirect k8s.io/klog/v2 v2.70.0 // indirect k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 // indirect k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect diff --git a/go.sum b/go.sum index e5914c573..ed0dd7fbb 100644 --- a/go.sum +++ b/go.sum @@ -49,16 +49,10 @@ github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb0 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/TwinProduction/go-color v1.0.0/go.mod h1:5hWpSyT+mmKPjCwPNEruBW5Dkbs/2PwOuU468ntEXNQ= github.com/airyhq/airy/lib/go/httpclient v0.0.0-20230426122014-b3add0488aa1 h1:oNPI7IFTuBW+EO2XLgYUye3wXx3Jf7MekUlN6lFq3xg= github.com/airyhq/airy/lib/go/httpclient v0.0.0-20230426122014-b3add0488aa1/go.mod h1:DmqTLti3ZCZ3PZcNNNL0C9oNzNSnAbrigBDwN66Ogaw= github.com/airyhq/airy/lib/go/payloads v0.0.0-20230426122014-b3add0488aa1 h1:nAKH/lr1NPPGQIPB7A69TINQCBQQH8pkYzeGgpseD4M= github.com/airyhq/airy/lib/go/payloads v0.0.0-20230426122014-b3add0488aa1/go.mod h1:QokQEnLolj8aluiKF11Sv52K88M6fO6XVxPtvdG4dR8= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= @@ -121,6 +115,9 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +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/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -189,16 +186,6 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 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/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= -github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= -github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -207,8 +194,6 @@ github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFb 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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -278,15 +263,12 @@ 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/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= @@ -678,8 +660,6 @@ k8s.io/apimachinery v0.24.2/go.mod h1:82Bi4sCzVBdpYjyI4jY6aHX+YCUchUIrZrXKedjd2U k8s.io/client-go v0.24.2 h1:CoXFSf8if+bLEbinDqN9ePIDGzcLtqhfd6jpfnwGOFA= k8s.io/client-go v0.24.2/go.mod h1:zg4Xaoo+umDsfCWr4fCnmLEtQXyCNXCvJuSsglNcV30= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= -k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= -k8s.io/helm v2.17.0+incompatible/go.mod h1:LZzlS4LQBHfciFOurYBFkCMTaZ0D1l+p0teMg7TSULI= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= From 78e5f54152f7bf393e300e0660b94522983ce968 Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Tue, 2 Apr 2024 12:12:38 +0200 Subject: [PATCH 25/37] [#4139] Add Flink connector (#4147) --- .../flink-connector/Dockerfile.result-sender | 16 + .../Dockerfile.statements-executor | 16 + backend/components/flink-connector/Makefile | 13 + backend/components/flink-connector/helm/BUILD | 3 + .../flink-connector/helm/Chart.yaml | 6 + .../helm/templates/configmap.yaml | 10 + .../templates/result-sender/deployment.yaml | 50 ++ .../helm/templates/result-sender/service.yaml | 15 + .../statements-executor/deployment.yaml | 50 ++ .../statements-executor/service.yaml | 16 + .../flink-connector/helm/values.yaml | 16 + .../flink-connector/src/result-sender.go | 158 ++++++ .../src/statements-executor.go | 140 +++++ .../components/flink-connector/src/tools.go | 503 ++++++++++++++++++ .../components/flink-connector/src/types.go | 137 +++++ docs/docs/getting-started/components.md | 1 + .../src/components/ChannelAvatar/index.tsx | 3 + lib/typescript/assets/images/icons/flink.svg | 1 + 18 files changed, 1154 insertions(+) create mode 100644 backend/components/flink-connector/Dockerfile.result-sender create mode 100644 backend/components/flink-connector/Dockerfile.statements-executor create mode 100644 backend/components/flink-connector/Makefile create mode 100644 backend/components/flink-connector/helm/BUILD create mode 100644 backend/components/flink-connector/helm/Chart.yaml create mode 100644 backend/components/flink-connector/helm/templates/configmap.yaml create mode 100644 backend/components/flink-connector/helm/templates/result-sender/deployment.yaml create mode 100644 backend/components/flink-connector/helm/templates/result-sender/service.yaml create mode 100644 backend/components/flink-connector/helm/templates/statements-executor/deployment.yaml create mode 100644 backend/components/flink-connector/helm/templates/statements-executor/service.yaml create mode 100644 backend/components/flink-connector/helm/values.yaml create mode 100644 backend/components/flink-connector/src/result-sender.go create mode 100644 backend/components/flink-connector/src/statements-executor.go create mode 100644 backend/components/flink-connector/src/tools.go create mode 100644 backend/components/flink-connector/src/types.go create mode 100644 lib/typescript/assets/images/icons/flink.svg diff --git a/backend/components/flink-connector/Dockerfile.result-sender b/backend/components/flink-connector/Dockerfile.result-sender new file mode 100644 index 000000000..6580476fa --- /dev/null +++ b/backend/components/flink-connector/Dockerfile.result-sender @@ -0,0 +1,16 @@ +FROM golang:1.17 + +WORKDIR /app + +COPY ./src/types.go ./src/tools.go ./src/result-sender.go ./ + +RUN go mod init main && \ + go get github.com/confluentinc/confluent-kafka-go/v2/kafka && \ + go get github.com/confluentinc/confluent-kafka-go/v2/schemaregistry && \ + go get github.com/confluentinc/confluent-kafka-go/v2/schemaregistry/serde && \ + go get github.com/confluentinc/confluent-kafka-go/v2/schemaregistry/serde/avro && \ + go get golang.org/x/net + +RUN go build -o app + +CMD ["./app"] diff --git a/backend/components/flink-connector/Dockerfile.statements-executor b/backend/components/flink-connector/Dockerfile.statements-executor new file mode 100644 index 000000000..3280b144f --- /dev/null +++ b/backend/components/flink-connector/Dockerfile.statements-executor @@ -0,0 +1,16 @@ +FROM golang:1.17 + +WORKDIR /app + +COPY ./src/types.go ./src/tools.go ./src/statements-executor.go ./ + +RUN go mod init main && \ + go get github.com/confluentinc/confluent-kafka-go/v2/kafka && \ + go get github.com/confluentinc/confluent-kafka-go/v2/schemaregistry && \ + go get github.com/confluentinc/confluent-kafka-go/v2/schemaregistry/serde && \ + go get github.com/confluentinc/confluent-kafka-go/v2/schemaregistry/serde/avro && \ + go get golang.org/x/net + +RUN go build -o app + +CMD ["./app"] diff --git a/backend/components/flink-connector/Makefile b/backend/components/flink-connector/Makefile new file mode 100644 index 000000000..1e7dcec8d --- /dev/null +++ b/backend/components/flink-connector/Makefile @@ -0,0 +1,13 @@ +build-statements-executor: + docker build -t flink-connector/statements-executor -f Dockerfile.statements-executor . + +release-statements-executor: build-statements-executor + docker tag flink-connector/statements-executor ghcr.io/airyhq/connectors/flink/statements-executor:release + docker push ghcr.io/airyhq/connectors/flink/statements-executor:release + +build-result-sender: + docker build -t flink-connector/result-sender -f Dockerfile.result-sender . + +release-result-sender: build-result-sender + docker tag flink-connector/result-sender ghcr.io/airyhq/connectors/flink/result-sender:release + docker push ghcr.io/airyhq/connectors/flink/result-sender:release diff --git a/backend/components/flink-connector/helm/BUILD b/backend/components/flink-connector/helm/BUILD new file mode 100644 index 000000000..45805d5f1 --- /dev/null +++ b/backend/components/flink-connector/helm/BUILD @@ -0,0 +1,3 @@ +load("//tools/build:helm.bzl", "helm_ruleset_core_version") + +helm_ruleset_core_version() diff --git a/backend/components/flink-connector/helm/Chart.yaml b/backend/components/flink-connector/helm/Chart.yaml new file mode 100644 index 000000000..4eb993fb2 --- /dev/null +++ b/backend/components/flink-connector/helm/Chart.yaml @@ -0,0 +1,6 @@ + +apiVersion: v2 +appVersion: "1.0" +description: Flink connector +name: flink-connector +version: 1.0 \ No newline at end of file diff --git a/backend/components/flink-connector/helm/templates/configmap.yaml b/backend/components/flink-connector/helm/templates/configmap.yaml new file mode 100644 index 000000000..d8301a65d --- /dev/null +++ b/backend/components/flink-connector/helm/templates/configmap.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.component }} + labels: + core.airy.co/managed: "true" + core.airy.co/mandatory: "{{ .Values.mandatory }}" + core.airy.co/component: "{{ .Values.component }}" + annotations: + core.airy.co/enabled: "{{ .Values.enabled }}" \ No newline at end of file diff --git a/backend/components/flink-connector/helm/templates/result-sender/deployment.yaml b/backend/components/flink-connector/helm/templates/result-sender/deployment.yaml new file mode 100644 index 000000000..0f50fc554 --- /dev/null +++ b/backend/components/flink-connector/helm/templates/result-sender/deployment.yaml @@ -0,0 +1,50 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.component }}-{{ .Values.resultSender.name }} + labels: + app: {{ .Values.component }} + core.airy.co/managed: "true" + core.airy.co/mandatory: "{{ .Values.mandatory }}" + core.airy.co/component: {{ .Values.component }} +spec: + replicas: {{ if .Values.enabled }} 1 {{ else }} 0 {{ end }} + selector: + matchLabels: + app: {{ .Values.component }}-{{ .Values.resultSender.name }} + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: {{ .Values.component }}-{{ .Values.resultSender.name }} + spec: + containers: + - name: app + image: "ghcr.io/airyhq/{{ .Values.resultSender.image }}:release" + imagePullPolicy: Always + envFrom: + - configMapRef: + name: security + - configMapRef: + name: kafka-config + - configMapRef: + name: {{ .Values.component }} + env: + - name: KAFKA_TOPIC_NAME + value: {{ .Values.resultSender.topic }} + - name: API_COMMUNICATION_URL + value: {{ .Values.apiCommunicationUrl }} + livenessProbe: + httpGet: + path: /actuator/health + port: {{ .Values.port }} + httpHeaders: + - name: Health-Check + value: health-check + initialDelaySeconds: 43200 + periodSeconds: 10 + failureThreshold: 3 \ No newline at end of file diff --git a/backend/components/flink-connector/helm/templates/result-sender/service.yaml b/backend/components/flink-connector/helm/templates/result-sender/service.yaml new file mode 100644 index 000000000..cdf73d72b --- /dev/null +++ b/backend/components/flink-connector/helm/templates/result-sender/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.component }}-{{ .Values.resultSender.name }} + labels: + app: {{ .Values.component }}-{{ .Values.resultSender.name }} +spec: + type: ClusterIP + clusterIP: None + ports: + - name: {{ .Values.component }}-{{ .Values.resultSender.name }} + port: 80 + targetPort: {{ .Values.port }} + selector: + app: {{ .Values.component }}-{{ .Values.resultSender.name }} \ No newline at end of file diff --git a/backend/components/flink-connector/helm/templates/statements-executor/deployment.yaml b/backend/components/flink-connector/helm/templates/statements-executor/deployment.yaml new file mode 100644 index 000000000..44c7b6e59 --- /dev/null +++ b/backend/components/flink-connector/helm/templates/statements-executor/deployment.yaml @@ -0,0 +1,50 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Values.component }}-{{ .Values.executor.name }} + labels: + app: {{ .Values.component }} + core.airy.co/managed: "true" + core.airy.co/mandatory: "{{ .Values.mandatory }}" + core.airy.co/component: {{ .Values.component }} +spec: + replicas: {{ if .Values.enabled }} 1 {{ else }} 0 {{ end }} + selector: + matchLabels: + app: {{ .Values.component }}-{{ .Values.executor.name }} + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + template: + metadata: + labels: + app: {{ .Values.component }}-{{ .Values.executor.name }} + spec: + containers: + - name: app + image: "ghcr.io/airyhq/{{ .Values.executor.image }}:release" + imagePullPolicy: Always + envFrom: + - configMapRef: + name: security + - configMapRef: + name: kafka-config + - configMapRef: + name: {{ .Values.component }} + env: + - name: KAFKA_TOPIC_NAME + value: {{ .Values.executor.topic }} + - name: FLINK_GATEWAY_URL + value: {{ .Values.gatewayUrl }} + livenessProbe: + httpGet: + path: /actuator/health + port: {{ .Values.port }} + httpHeaders: + - name: Health-Check + value: health-check + initialDelaySeconds: 43200 + periodSeconds: 10 + failureThreshold: 3 \ No newline at end of file diff --git a/backend/components/flink-connector/helm/templates/statements-executor/service.yaml b/backend/components/flink-connector/helm/templates/statements-executor/service.yaml new file mode 100644 index 000000000..3e5fbfc30 --- /dev/null +++ b/backend/components/flink-connector/helm/templates/statements-executor/service.yaml @@ -0,0 +1,16 @@ + +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.component }}-{{ .Values.executor.name }} + labels: + app: {{ .Values.component }}-{{ .Values.executor.name }} +spec: + type: ClusterIP + clusterIP: None + ports: + - name: {{ .Values.component }}-{{ .Values.executor.name }} + port: 80 + targetPort: {{ .Values.port }} + selector: + app: {{ .Values.component }}-{{ .Values.executor.name }} \ No newline at end of file diff --git a/backend/components/flink-connector/helm/values.yaml b/backend/components/flink-connector/helm/values.yaml new file mode 100644 index 000000000..71f4475a3 --- /dev/null +++ b/backend/components/flink-connector/helm/values.yaml @@ -0,0 +1,16 @@ + +component: flink-connector +mandatory: false +enabled: false +port: 8080 +resources: +gatewayUrl: "http://flink-jobmanager:8083" +apiCommunicationUrl: "http://api-communication/messages.send" +executor: + name: statements-executor + image: connectors/flink/statements-executor + topic: flink.statements +resultSender: + name: result-sender + image: connectors/flink/result-sender + topic: flink.output \ No newline at end of file diff --git a/backend/components/flink-connector/src/result-sender.go b/backend/components/flink-connector/src/result-sender.go new file mode 100644 index 000000000..90bfff738 --- /dev/null +++ b/backend/components/flink-connector/src/result-sender.go @@ -0,0 +1,158 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/confluentinc/confluent-kafka-go/v2/kafka" +) + +func main() { + + // Create Kafka consumer to read the statements + kafkaURL := os.Getenv("KAFKA_BROKERS") + schemaRegistryURL := os.Getenv("KAFKA_SCHEMA_REGISTRY_URL") + topicName := os.Getenv("KAFKA_TOPIC_NAME") + systemToken := os.Getenv("systemToken") + authUsername := os.Getenv("AUTH_JAAS_USERNAME") + authPassword := os.Getenv("AUTH_JAAS_PASSWORD") + flinkProvider := os.Getenv("provider") + groupID := "result-sender" + msgNormal := false + msgDebug := true + + if kafkaURL == "" || schemaRegistryURL == "" || topicName == "" { + fmt.Println("KAFKA_BROKERS, KAFKA_SCHEMA_REGISTRY_URL, and KAFKA_TOPIC_NAME environment variables must be set") + return + } + + var confluentConnection ConfluentConnection + confluentConnection.Token = os.Getenv("confluentToken") + confluentConnection.ComputePoolID = os.Getenv("confluentComputePoolID") + confluentConnection.Principal = os.Getenv("confluentPrincipal") + confluentConnection.SQLCurrentCatalog = os.Getenv("confluentSQLCurrentCatalog") + confluentConnection.SQLCurrentDatabase = os.Getenv("confluentSQLCurrentDatabase") + + // Healthcheck + http.HandleFunc("/actuator/health", func(w http.ResponseWriter, r *http.Request) { + response := map[string]string{"status": "UP"} + jsonResponse, err := json.Marshal(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + }) + + go func() { + if err := http.ListenAndServe(":80", nil); err != nil { + panic(err) + } + }() + + fmt.Println("Health-check started") + + // Create Kafka consumer configuration + fmt.Println("Creating Kafka consumer for topic: ", topicName) + + c, err := kafka.NewConsumer(&kafka.ConfigMap{ + "bootstrap.servers": kafkaURL, + "group.id": groupID, + "auto.offset.reset": "earliest", + "security.protocol": "SASL_SSL", + "sasl.mechanisms": "PLAIN", + "sasl.username": authUsername, + "sasl.password": authPassword, + }) + if err != nil { + fmt.Printf("Error creating consumer: %v\n", err) + return + } + c.SubscribeTopics([]string{topicName}, nil) + // Channel for signals + signals := make(chan os.Signal, 1) + done := make(chan bool, 1) + + signal.Notify(signals, os.Interrupt, syscall.SIGTERM) + + go func() { + for { + select { + case sig := <-signals: + // If an interrupt signal is received, break the loop + fmt.Printf("Caught signal %v: terminating\n", sig) + done <- true + return + default: + msg, err := c.ReadMessage(-1) + if err == nil { + var flinkOutput FlinkOutput + if err := json.Unmarshal(msg.Value, &flinkOutput); err != nil { + fmt.Printf("Error unmarshalling message: %v\n", err) + continue + } else { + fmt.Printf("Received message: %+v\n", flinkOutput) + + flinkGatewayURL := os.Getenv("FLINK_GATEWAY_URL") + confluentGatewayURL := os.Getenv("CONFLUENT_GATEWAY_URL") + + var result FlinkResult + var headerConfluent []string + var resultConfluent string + + if flinkProvider == "flink" { + fmt.Println("Flink gateway: ", flinkGatewayURL) + result, err = getFlinkResult(flinkGatewayURL, flinkOutput.SessionID) + headerConfluent = []string{} + } else { + fmt.Println("Flink gateway: ", confluentGatewayURL) + fmt.Println("Waiting 20 seconds...") + time.Sleep(20 * time.Second) + headerConfluent, resultConfluent, err = getFlinkResultConfluent(confluentGatewayURL, flinkOutput.SessionID, confluentConnection) + } + if err != nil { + fmt.Println("Unable to get Flink result:", err) + sendMessage("Error: "+err.Error(), flinkOutput.ConversationID, systemToken, msgDebug) + return + } + if flinkProvider == "flink" { + sendMessage("Result retrieved from Flink: "+fmt.Sprintf("%#v", result), flinkOutput.ConversationID, systemToken, msgDebug) + sendMessage("Now converting the result to Markdown", flinkOutput.ConversationID, systemToken, msgDebug) + response, err := convertResultToMarkdown(result) + if err != nil { + fmt.Println("Unable to generate Markdown from result:", err) + sendMessage("Error: "+err.Error(), flinkOutput.ConversationID, systemToken, msgDebug) + sendMessage("I'm sorry, I am unable to fetch the results from the Flink table.", flinkOutput.ConversationID, systemToken, msgNormal) + return + } + sendMessage(response, flinkOutput.ConversationID, systemToken, msgNormal) + } else { + sendMessage("Result retrieved from Flink: "+fmt.Sprintf("%#v", resultConfluent), flinkOutput.ConversationID, systemToken, msgDebug) + sendMessage("Now converting the result to Markdown", flinkOutput.ConversationID, systemToken, msgDebug) + response, err := convertConfluentResultToMarkdown(headerConfluent, resultConfluent) + if err != nil { + fmt.Println("Unable to generate Markdown from result:", err) + sendMessage("Error: "+err.Error(), flinkOutput.ConversationID, systemToken, msgDebug) + sendMessage("I'm sorry, I am unable to fetch the results from the Flink table.", flinkOutput.ConversationID, systemToken, msgNormal) + return + } + sendMessage(response, flinkOutput.ConversationID, systemToken, msgNormal) + } + } + } else { + fmt.Printf("Consumer error: %v\n", err) + } + } + } + }() + <-done + c.Close() + fmt.Println("Consumer closed") +} diff --git a/backend/components/flink-connector/src/statements-executor.go b/backend/components/flink-connector/src/statements-executor.go new file mode 100644 index 000000000..6ccf2e91b --- /dev/null +++ b/backend/components/flink-connector/src/statements-executor.go @@ -0,0 +1,140 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + + "github.com/confluentinc/confluent-kafka-go/v2/kafka" +) + +func main() { + + kafkaURL := os.Getenv("KAFKA_BROKERS") + schemaRegistryURL := os.Getenv("KAFKA_SCHEMA_REGISTRY_URL") + topicName := os.Getenv("KAFKA_TOPIC_NAME") + systemToken := os.Getenv("systemToken") + authUsername := os.Getenv("AUTH_JAAS_USERNAME") + authPassword := os.Getenv("AUTH_JAAS_PASSWORD") + flinkProvider := os.Getenv("provider") + groupID := "statement-executor-" + msgNormal := false + msgDebug := true + + if kafkaURL == "" || schemaRegistryURL == "" || topicName == "" { + fmt.Println("KAFKA_BROKERS, KAFKA_SCHEMA_REGISTRY_URL, and KAFKA_TOPIC_NAME environment variables must be set") + return + } + + var confluentConnection ConfluentConnection + confluentConnection.Token = os.Getenv("confluentToken") + confluentConnection.ComputePoolID = os.Getenv("confluentComputePoolID") + confluentConnection.Principal = os.Getenv("confluentPrincipal") + confluentConnection.SQLCurrentCatalog = os.Getenv("confluentSQLCurrentCatalog") + confluentConnection.SQLCurrentDatabase = os.Getenv("confluentSQLCurrentDatabase") + + // Healthcheck + http.HandleFunc("/actuator/health", func(w http.ResponseWriter, r *http.Request) { + response := map[string]string{"status": "UP"} + jsonResponse, err := json.Marshal(response) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(jsonResponse) + }) + + go func() { + if err := http.ListenAndServe(":80", nil); err != nil { + panic(err) + } + }() + + fmt.Println("Health-check started") + + // Create Kafka consumer configuration + fmt.Println("Creating Kafka consumer for topic: ", topicName) + + c, err := kafka.NewConsumer(&kafka.ConfigMap{ + "bootstrap.servers": kafkaURL, + "group.id": groupID, + "auto.offset.reset": "earliest", + "security.protocol": "SASL_SSL", + "sasl.mechanisms": "PLAIN", + "sasl.username": authUsername, + "sasl.password": authPassword, + }) + if err != nil { + fmt.Printf("Error creating consumer: %v\n", err) + return + } + c.SubscribeTopics([]string{topicName}, nil) + // Channel for signals + signals := make(chan os.Signal, 1) + done := make(chan bool, 1) + + signal.Notify(signals, os.Interrupt, syscall.SIGTERM) + + go func() { + for { + select { + case sig := <-signals: + fmt.Printf("Caught signal %v: terminating\n", sig) + done <- true + return + default: + msg, err := c.ReadMessage(-1) + if err == nil { + var statementSet FlinkStatementSet + if err := json.Unmarshal(msg.Value, &statementSet); err != nil { + fmt.Printf("Error unmarshalling message: %v\n", err) + continue + } else { + fmt.Printf("Received message: %+v\n", statementSet) + + flinkGatewayURL := os.Getenv("FLINK_GATEWAY_URL") + confluentGatewayURL := os.Getenv("CONFLUENT_GATEWAY_URL") + var sessionID string + if flinkProvider == "flink" { + sessionID, err = sendFlinkSQL(flinkGatewayURL, statementSet) + } else { + sessionID, err = sendFlinkSQLConfluent(confluentGatewayURL, statementSet, confluentConnection) + } + + if err != nil { + fmt.Println("Error running Flink statement:", err) + sendMessage("Error: "+err.Error(), statementSet.ConversationID, systemToken, msgDebug) + sendMessage("I am sorry, I am unable to answer that question.", statementSet.ConversationID, systemToken, msgNormal) + return + } + fmt.Println("Successfully executed the Flink statement.") + sendMessage("FlinkSessionID: "+sessionID, statementSet.ConversationID, systemToken, msgDebug) + var flinkOutput FlinkOutput + flinkOutput.SessionID = sessionID + flinkOutput.Question = statementSet.Question + flinkOutput.MessageID = statementSet.MessageID + flinkOutput.ConversationID = statementSet.ConversationID + err = produceFlinkOutput(flinkOutput, kafkaURL, "flink-producer-"+groupID, authUsername, authPassword) + if err != nil { + + fmt.Printf("error producing message to Kafka: %v\n", err) + sendMessage("Error: "+err.Error(), statementSet.ConversationID, systemToken, msgDebug) + sendMessage("I am sorry, I am unable to answer that question.", statementSet.ConversationID, systemToken, msgNormal) + } + sendMessage("Message produced to topic: flink.outputs", statementSet.ConversationID, systemToken, msgDebug) + } + } else { + fmt.Printf("Consumer error: %v\n", err) + } + } + } + }() + <-done + c.Close() + fmt.Println("Consumer closed") +} diff --git a/backend/components/flink-connector/src/tools.go b/backend/components/flink-connector/src/tools.go new file mode 100644 index 000000000..be68a6368 --- /dev/null +++ b/backend/components/flink-connector/src/tools.go @@ -0,0 +1,503 @@ +package main + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "strings" + "time" + + "github.com/confluentinc/confluent-kafka-go/v2/kafka" +) + +func sendFlinkSQL(url string, statementSet FlinkStatementSet) (string, error) { + timestamp := time.Now().Unix() + strTimestamp := fmt.Sprintf("%d", timestamp) + replacements := map[string]string{ + "{PROPERTIES_GROUP_ID}": "flink-" + strTimestamp, + "{PROPERTIES_BOOTSTRAP_SERVERS}": os.Getenv("KAFKA_BROKERS"), + "{PROPERTIES_SASL_JAAS_CONFIG}": fmt.Sprintf("org.apache.flink.kafka.shaded.org.apache.kafka.common.security.plain.PlainLoginModule required username=\"%s\" password=\"%s\";", os.Getenv("AUTH_JAAS_USERNAME"), os.Getenv("AUTH_JAAS_PASSWORD")), + } + for i, stmt := range statementSet.Statements { + for placeholder, value := range replacements { + stmt = strings.Replace(stmt, placeholder, value, -1) + } + statementSet.Statements[i] = stmt + } + fmt.Println("Updated StatementSet: %+v\n", statementSet.Statements) + + req, err := http.NewRequest("POST", url+"/v1/sessions/", bytes.NewReader([]byte(""))) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Println("Error reading response body from the API: %v", err) + } + fmt.Println("Response: ", string(body)) + var sessionResponse FlinkSessionResponse + if err := json.Unmarshal(body, &sessionResponse); err != nil { + fmt.Printf("Error unmarshaling message: %v\n", err) + return "", err + } + defer resp.Body.Close() + + fmt.Println("The Flink session is: ", sessionResponse.SessionHandle) + for _, statement := range statementSet.Statements { + payload := FlinkSQLRequest{ + Statement: statement, + } + payloadBytes, err := json.Marshal(payload) + if err != nil { + return "", err + } + + req, err = http.NewRequest("POST", url+"/v1/sessions/"+sessionResponse.SessionHandle+"/statements/", bytes.NewReader(payloadBytes)) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/json") + + client = &http.Client{} + resp, err = client.Do(req) + if err != nil { + return "", err + } + body, err = io.ReadAll(resp.Body) + if err != nil { + fmt.Println("Error reading response body from the API: %v", err) + } + fmt.Println("Statement submitted. Response: ", string(body)) + var statementResponse FlinkStatementResponse + if err := json.Unmarshal(body, &statementResponse); err != nil { + fmt.Printf("Error unmarshaling message: %v\n", err) + return "", err + } + fmt.Printf("Check status on: %s/v1/sessions/%s/operations/%s/result/0\n", url, sessionResponse.SessionHandle, statementResponse.OperationHandle) + defer resp.Body.Close() + } + + return sessionResponse.SessionHandle, nil +} + +func produceFlinkOutput(flinkOutput FlinkOutput, kafkaURL, groupID, authUsername, authPassword string) error { + + kafkaTopic := "flink.outputs" + + flinkOutputJSON, err := json.Marshal(flinkOutput) + if err != nil { + return fmt.Errorf("error marshaling query to JSON: %w", err) + } + + configMap := kafka.ConfigMap{ + "bootstrap.servers": kafkaURL, + } + if authUsername != "" && authPassword != "" { + configMap.SetKey("security.protocol", "SASL_SSL") + configMap.SetKey("sasl.mechanisms", "PLAIN") + configMap.SetKey("sasl.username", authUsername) + configMap.SetKey("sasl.password", authPassword) + } + + producer, err := kafka.NewProducer(&configMap) + if err != nil { + return fmt.Errorf("failed to create producer: %w", err) + } + defer producer.Close() + + // Produce the message + message := kafka.Message{ + TopicPartition: kafka.TopicPartition{Topic: &kafkaTopic, Partition: kafka.PartitionAny}, + Key: []byte(flinkOutput.SessionID), + Value: flinkOutputJSON, + } + + err = producer.Produce(&message, nil) + if err != nil { + return fmt.Errorf("failed to produce message: %w", err) + } + fmt.Println("message scheduled for production") + producer.Flush(15 * 1000) + fmt.Println("message flushed") + return nil +} + +func sendMessage(message string, conversationId string, systemToken string, debug bool) (int, string, error) { + messageContent := messageContent{ + Text: message, + Debug: debug, + } + messageToSend := ApplicationCommunicationSendMessage{ + ConversationID: conversationId, + Message: messageContent, + } + messageJSON, err := json.Marshal(messageToSend) + if err != nil { + fmt.Printf("Error encoding response to JSON: %v\n", err) + return 0, "", errors.New("The message could not be encoded to JSON for sending.") + } + + req, err := http.NewRequest("POST", "http://api-communication/messages.send", bytes.NewReader(messageJSON)) + if err != nil { + fmt.Printf("Error creating request: %v\n", err) + return 0, "", errors.New("The message could not be sent.") + } + req.Header.Add("Authorization", "Bearer "+systemToken) + req.Header.Add("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + fmt.Printf("Error sending POST request: %v\n", err) + return 0, "", errors.New("Error sending POST request.") + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + fmt.Println("Error reading response body:", err) + return 0, "", errors.New("Error reading response body.") + } + + var response SendMessageResponse + err = json.Unmarshal(body, &response) + if err != nil { + fmt.Println("Error unmarshaling response:", err) + return 0, "", errors.New("Response couldn't be unmarshaled.") + } + + fmt.Printf("Message sent with status code: %d\n", resp.StatusCode) + return resp.StatusCode, response.ID, nil +} + +func sendFlinkSQLConfluent(url string, statementSet FlinkStatementSet, connection ConfluentConnection) (string, error) { + timestamp := time.Now().Unix() + strTimestamp := fmt.Sprintf("%d", timestamp) + statementName := "airy-" + strTimestamp + payload := ConfluentFlink{ + Name: statementName, + Spec: ConfluentFlinkSpec{ + Statement: statementSet.Statements[0], + ComputePoolID: connection.ComputePoolID, + Principal: connection.Principal, + Properties: FlinkSpecProperties{ + SQLCurrentCatalog: connection.SQLCurrentCatalog, + SQLCurrentDatabase: connection.SQLCurrentDatabase, + }, + Stopped: false, + }, + } + payloadBytes, err := json.Marshal(payload) + if err != nil { + return "", err + } + + req, err := http.NewRequest("POST", url, bytes.NewReader(payloadBytes)) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Add("Authorization", "Basic "+connection.Token) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Println("Error reading response body from the API: %v", err) + } + fmt.Println("Statement submitted. Response: ", string(body)) + var statementResponse ConfluentFlinkStatementResponse + if err := json.Unmarshal(body, &statementResponse); err != nil { + fmt.Printf("Error unmarshaling message: %v\n", err) + return "", err + } + fmt.Printf("Check status on: %s/%s\n", url, statementName) + defer resp.Body.Close() + + return statementName, nil +} + +func getFlinkResult(url, sessionID string) (FlinkResult, error) { + fmt.Println("The Flink session is: ", sessionID) + payload := FlinkSQLRequest{ + Statement: "select * from output;", + } + payloadBytes, err := json.Marshal(payload) + if err != nil { + return FlinkResult{}, err + } + + req, err := http.NewRequest("POST", url+"/v1/sessions/"+sessionID+"/statements/", bytes.NewReader(payloadBytes)) + if err != nil { + return FlinkResult{}, err + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return FlinkResult{}, err + } + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Println("Error reading response body from the API: %v", err) + } + fmt.Println("Statement submitted. Response: ", string(body)) + var statementResponse FlinkStatementResponse + if err := json.Unmarshal(body, &statementResponse); err != nil { + fmt.Printf("Error unmarshaling message: %v\n", err) + return FlinkResult{}, err + } + + fmt.Printf("Fetching result from: %s/v1/sessions/%s/operations/%s/result/0\n", url, sessionID, statementResponse.OperationHandle) + time.Sleep(20 * time.Second) + req, err = http.NewRequest("GET", url+"/v1/sessions/"+sessionID+"/operations/"+statementResponse.OperationHandle+"/result/0", nil) + if err != nil { + return FlinkResult{}, err + } + req.Header.Set("Content-Type", "application/json") + + client = &http.Client{} + resp, err = client.Do(req) + if err != nil { + return FlinkResult{}, err + } + body, err = io.ReadAll(resp.Body) + if err != nil { + fmt.Println("Error reading response body from the API: %v", err) + } + fmt.Println("Statement submitted. Response: ", string(body)) + var flinkResultResponse FlinkResultResponse + if err := json.Unmarshal(body, &flinkResultResponse); err != nil { + fmt.Printf("Error unmarshaling message: %v\n", err) + return FlinkResult{}, err + } + defer resp.Body.Close() + + if flinkResultResponse.Errors != nil { + statementError := errors.New(strings.Join(flinkResultResponse.Errors, ",")) + return FlinkResult{}, statementError + } + return flinkResultResponse.Results, nil +} + +func markdown(message string) (string, error) { + return message, nil +} + +func convertResultToMarkdown(result FlinkResult) (string, error) { + var builder strings.Builder + + if len(result.Columns) == 0 { + return "", errors.New("No columns found for generating the Markdown table.") + } + for _, col := range result.Columns { + builder.WriteString("| " + col.Name + " ") + } + builder.WriteString("|\n") + + for range result.Columns { + builder.WriteString("|---") + } + builder.WriteString("|\n") + + for _, d := range result.Data { + for _, field := range d.Fields { + builder.WriteString(fmt.Sprintf("| %v ", field)) + } + builder.WriteString("|\n") + } + + return builder.String(), nil +} + +func getFlinkResultConfluent(url, sessionID string, connection ConfluentConnection) ([]string, string, error) { + req, err := http.NewRequest("GET", url+"/"+sessionID, bytes.NewReader([]byte(""))) + if err != nil { + return []string{}, "", err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Add("Authorization", "Basic "+connection.Token) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return []string{}, "", err + } + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Println("Error reading response body from the API: %v", err) + } + fmt.Println("Statement submitted. Response: ", string(body)) + var statementResponse ConfluentFlinkStatementResponse + if err := json.Unmarshal(body, &statementResponse); err != nil { + fmt.Printf("Error unmarshaling message: %v\n", err) + return []string{}, "", err + } + fmt.Printf("Received result for statement: %s\n", sessionID) + fmt.Println("Phase: ", statementResponse.Status.Phase, " Detail: ", statementResponse.Status.Detail) + defer resp.Body.Close() + + if statementResponse.Status.Phase == "RUNNING" || statementResponse.Status.Phase == "COMPLETED" { + columns, err := getColumnNames(statementResponse.Status.ResultSchema) + if err != nil { + fmt.Println("Extracting of the column names failed.") + return []string{}, "", err + } + req, err := http.NewRequest("GET", url+"/"+sessionID+"/results", bytes.NewReader([]byte(""))) + if err != nil { + return []string{}, "", err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Add("Authorization", "Basic "+connection.Token) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return []string{}, "", err + } + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Println("Error reading response body from the API: %v", err) + } + fmt.Println("Statement submitted. Response: ", string(body)) + var result ConfluentFlinkResultsResponse + if err := json.Unmarshal(body, &result); err != nil { + fmt.Printf("Error unmarshaling message: %v\n", err) + return []string{}, "", err + } + nextResult := result.Metadata.Next + fmt.Println("Next result: ", nextResult) + fmt.Println("Result: ", result.Results.Data) + data, err := dataToString(result.Results.Data) + if data != "" { + return columns, data, nil + } else { + req, err := http.NewRequest("GET", nextResult, bytes.NewReader([]byte(""))) + if err != nil { + return []string{}, "", err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Add("Authorization", "Basic "+connection.Token) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return []string{}, "", err + } + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Println("Error reading response body from the API: %v", err) + } + fmt.Println("Statement submitted. Response: ", string(body)) + var result ConfluentFlinkResultsResponse + if err := json.Unmarshal(body, &result); err != nil { + fmt.Printf("Error unmarshaling message: %v\n", err) + return []string{}, "", err + } + data, err := dataToString(result.Results.Data) + return columns, data, err + } + } else { + err := errors.New("Flink statement failed. Status: " + statementResponse.Status.Phase) + return []string{}, "", err + } +} + +func dataToString(data interface{}) (string, error) { + if slice, ok := data.([]interface{}); ok && len(slice) > 0 { + dataBytes, err := json.Marshal(data) + if err != nil { + return "", err + } + return string(dataBytes), nil + } + return "", nil +} + +func convertConfluentResultToMarkdown(headerNames []string, jsonStr string) (string, error) { + var dataRows []ConfluentDataRow + err := json.Unmarshal([]byte(jsonStr), &dataRows) + if err != nil { + return "", err + } + + var sb strings.Builder + + header := generateMarkdownHeader(headerNames) + sb.WriteString(header) + sb.WriteString("\n") + + separator := strings.Repeat("| --- ", strings.Count(header, "|")-1) + "|" + sb.WriteString(separator) + sb.WriteString("\n") + + for _, dataRow := range dataRows { + sb.WriteString("|") + for _, cell := range dataRow.Row { + sb.WriteString(" ") + sb.WriteString(cell) + sb.WriteString(" |") + } + sb.WriteString("\n") + } + + return sb.String(), nil +} + +func extractColumnNames(jsonStr string) ([]string, error) { + var schema ConfluentResultSchema + err := json.Unmarshal([]byte(jsonStr), &schema) + if err != nil { + return nil, err + } + + var columnNames []string + for _, column := range schema.Columns { + columnNames = append(columnNames, column.Name) + } + + return columnNames, nil +} + +func generateMarkdownHeader(columnNames []string) string { + var header string + + for _, name := range columnNames { + header += "| " + name + " " + } + header += "|" + + return header +} + +func ResultsToString(rs ConfluentResultSchema) string { + var columnNames []string + for _, column := range rs.Columns { + columnNames = append(columnNames, column.Name) + } + return strings.Join(columnNames, ", ") +} + +func getColumnNames(schema ConfluentResultSchema) ([]string, error) { + var columnNames []string + for _, column := range schema.Columns { + columnNames = append(columnNames, column.Name) + } + return columnNames, nil +} diff --git a/backend/components/flink-connector/src/types.go b/backend/components/flink-connector/src/types.go new file mode 100644 index 000000000..c67ee3944 --- /dev/null +++ b/backend/components/flink-connector/src/types.go @@ -0,0 +1,137 @@ +package main + +type ApplicationCommunicationSendMessage struct { + ConversationID string `json:"conversation_id"` + Message messageContent `json:"message"` + Metadata map[string]string `json:"metadata"` +} + +type messageContent struct { + Text string `json:"text"` + Debug bool `json:"debug"` +} + +type SendMessageResponse struct { + ID string `json:"id"` + State string `json:"state"` +} + +type FlinkOutput struct { + SessionID string `json:"session_id"` + Question string `json:"question"` + MessageID string `json:"message_id"` + ConversationID string `json:"conversation_id"` +} + +type FlinkSQLRequest struct { + Statement string `json:"statement"` +} + +type FlinkSessionResponse struct { + SessionHandle string `json:"sessionHandle"` +} + +type FlinkStatementResponse struct { + OperationHandle string `json:"operationHandle"` +} + +type Column struct { + Name string `json:"name"` + LogicalType struct { + Type string `json:"type"` + Nullable bool `json:"nullable"` + Length int `json:"length,omitempty"` + } `json:"logicalType"` + Comment interface{} `json:"comment"` +} + +type Data struct { + Kind string `json:"kind"` + Fields []interface{} `json:"fields"` +} + +type FlinkResult struct { + Columns []Column `json:"columns"` + RowFormat string `json:"rowFormat"` + Data []Data `json:"data"` +} + +type FlinkResultResponse struct { + ResultType string `json:"resultType"` + IsQueryResult bool `json:"isQueryResult"` + JobID string `json:"jobID"` + ResultKind string `json:"resultKind"` + Results FlinkResult `json:"results"` + NextResultUri string `json:"nextResultUri"` + Errors []string `json:"errors"` +} + +type ConfluentFlink struct { + Name string `json:"name"` + Spec ConfluentFlinkSpec `json:"spec"` +} + +type ConfluentFlinkSpec struct { + Statement string `json:"statement"` + ComputePoolID string `json:"compute_pool_id"` + Principal string `json:"principal"` + Properties FlinkSpecProperties `json:"properties"` + Stopped bool `json:"stopped"` +} + +type FlinkSpecProperties struct { + SQLCurrentCatalog string `json:"sql.current-catalog"` + SQLCurrentDatabase string `json:"sql.current-database"` +} + +type ConfluentFlinkStatementResponse struct { + Name string `json:"name"` + Status ConfluentFlinkStatementStatus `json:"status"` +} + +type ConfluentFlinkStatementStatus struct { + Detail string `json:"detail"` + Phase string `json:"phase"` + ResultSchema ConfluentResultSchema `json:"result_schema"` +} + +type ConfluentResultSchema struct { + Columns []struct { + Name string `json:"name"` + } `json:"columns"` +} + +type ConfluentFlinkResultsResponse struct { + Metadata ResultResponseMetadata `json:"metadata"` + Results ResultResponseResults `json:"results"` +} + +type ResultResponseMetadata struct { + CreatedAt string `json:"created_at"` + Next string `json:"next"` + Self string `json:"self"` +} + +type ResultResponseResults struct { + Data interface{} `json:"data"` +} + +type ConfluentDataRow struct { + Op int `json:"op"` + Row []string `json:"row"` +} + +type FlinkStatementSet struct { + Statements []string `json:"statements"` + Question string `json:"question"` + MessageID string `json:"message_id"` + ConversationID string `json:"conversation_id"` +} + +type ConfluentConnection struct { + Token string + ComputePoolID string + Principal string + SQLCurrentCatalog string + SQLCurrentDatabase string +} diff --git a/docs/docs/getting-started/components.md b/docs/docs/getting-started/components.md index c358383c5..2e5b32d58 100644 --- a/docs/docs/getting-started/components.md +++ b/docs/docs/getting-started/components.md @@ -96,5 +96,6 @@ Here is a list of the open source components which can be added to `Airy Core`: - sources-twilio - sources-viber - sources-whatsapp +- flink-connector More information about the components API can be found [here](/api/endpoints/components). diff --git a/frontend/control-center/src/components/ChannelAvatar/index.tsx b/frontend/control-center/src/components/ChannelAvatar/index.tsx index b1d6c33bf..6894e2c24 100644 --- a/frontend/control-center/src/components/ChannelAvatar/index.tsx +++ b/frontend/control-center/src/components/ChannelAvatar/index.tsx @@ -27,6 +27,7 @@ import {ReactComponent as MosaicAvatar} from 'assets/images/icons/mosaic.svg'; import {ReactComponent as WeaviateAvatar} from 'assets/images/icons/weaviate.svg'; import {ReactComponent as GmailAvatar} from 'assets/images/icons/gmail.svg'; import {ReactComponent as SlackAvatar} from 'assets/images/icons/slack.svg'; +import {ReactComponent as FlinkAvatar} from 'assets/images/icons/flink.svg'; import {Channel, Source} from 'model'; import styles from './index.module.scss'; @@ -135,6 +136,8 @@ export const getChannelAvatar = (source: string) => { return ; case 'Slack connector': return ; + case 'Flink connector': + return ; default: return ; diff --git a/lib/typescript/assets/images/icons/flink.svg b/lib/typescript/assets/images/icons/flink.svg new file mode 100644 index 000000000..fb63bd841 --- /dev/null +++ b/lib/typescript/assets/images/icons/flink.svg @@ -0,0 +1 @@ + \ No newline at end of file From 37ad74532a416377dfc34a1e6db0bf89dfda0ba5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 14:54:07 +0200 Subject: [PATCH 26/37] Bump follow-redirects from 1.15.2 to 1.15.6 in /docs (#4148) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.6. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.6) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/yarn.lock b/docs/yarn.lock index 9d7c592d3..36997870d 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -3909,9 +3909,9 @@ flux@^4.0.1: fbjs "^3.0.1" follow-redirects@^1.0.0, follow-redirects@^1.14.7: - version "1.15.2" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== fork-ts-checker-webpack-plugin@^6.5.0: version "6.5.3" From cf30afa70e35b202a3852f9f1900715ddc4b387b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 14:54:26 +0200 Subject: [PATCH 27/37] Bump follow-redirects from 1.15.2 to 1.15.6 (#4149) Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.6. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.6) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 07b921b1e..b8aba1d39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4561,9 +4561,9 @@ flatted@^3.1.0: integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== follow-redirects@^1.0.0: - version "1.15.2" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== for-each@^0.3.3: version "0.3.3" From 1aef4cefbe788a4dd9d6a63aae68d45ea9e38f69 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:00:41 +0200 Subject: [PATCH 28/37] Bump express from 4.18.2 to 4.19.2 in /docs (#4150) Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2. - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2) --- updated-dependencies: - dependency-name: express dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/yarn.lock | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/yarn.lock b/docs/yarn.lock index 36997870d..521a13fdb 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -2620,13 +2620,13 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== -body-parser@1.20.1: - version "1.20.1" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" - integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== dependencies: bytes "3.1.2" - content-type "~1.0.4" + content-type "~1.0.5" debug "2.6.9" depd "2.0.0" destroy "1.2.0" @@ -2634,7 +2634,7 @@ body-parser@1.20.1: iconv-lite "0.4.24" on-finished "2.4.1" qs "6.11.0" - raw-body "2.5.1" + raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" @@ -3057,7 +3057,7 @@ content-disposition@0.5.4: dependencies: safe-buffer "5.2.1" -content-type@~1.0.4: +content-type@~1.0.4, content-type@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== @@ -3072,10 +3072,10 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== copy-text-to-clipboard@^3.0.1: version "3.1.0" @@ -3713,16 +3713,16 @@ execa@^5.0.0: strip-final-newline "^2.0.0" express@^4.17.3: - version "4.18.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" - integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.1" + body-parser "1.20.2" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.5.0" + cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" @@ -5990,10 +5990,10 @@ range-parser@^1.2.1, range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== dependencies: bytes "3.1.2" http-errors "2.0.0" From c585499f6a865abe0b407a9bda3a23d2f2ca57f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:01:05 +0200 Subject: [PATCH 29/37] Bump express from 4.18.2 to 4.19.2 (#4151) Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2. - [Release notes](https://github.com/expressjs/express/releases) - [Changelog](https://github.com/expressjs/express/blob/master/History.md) - [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2) --- updated-dependencies: - dependency-name: express dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 43 ++++++++++++++++++++++++------------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/yarn.lock b/yarn.lock index b8aba1d39..404f68511 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2927,13 +2927,13 @@ bluebird@^3.7.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -body-parser@1.20.1: - version "1.20.1" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" - integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== dependencies: bytes "3.1.2" - content-type "~1.0.4" + content-type "~1.0.5" debug "2.6.9" depd "2.0.0" destroy "1.2.0" @@ -2941,7 +2941,7 @@ body-parser@1.20.1: iconv-lite "0.4.24" on-finished "2.4.1" qs "6.11.0" - raw-body "2.5.1" + raw-body "2.5.2" type-is "~1.6.18" unpipe "1.0.0" @@ -3365,6 +3365,11 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== +content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: version "1.9.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" @@ -3375,10 +3380,10 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== copy-webpack-plugin@^11.0.0: version "11.0.0" @@ -4359,16 +4364,16 @@ expect@^29.0.0: jest-util "^29.2.1" express@^4.17.3: - version "4.18.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" - integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== dependencies: accepts "~1.3.8" array-flatten "1.1.1" - body-parser "1.20.1" + body-parser "1.20.2" content-disposition "0.5.4" content-type "~1.0.4" - cookie "0.5.0" + cookie "0.6.0" cookie-signature "1.0.6" debug "2.6.9" depd "2.0.0" @@ -7444,10 +7449,10 @@ range-parser@^1.2.1, range-parser@~1.2.1: resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== dependencies: bytes "3.1.2" http-errors "2.0.0" From 933fcf17efead058267b279ee20bcd9c15df40bd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:01:45 +0200 Subject: [PATCH 30/37] Bump webpack-dev-middleware from 5.3.3 to 5.3.4 (#4153) Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.3 to 5.3.4. - [Release notes](https://github.com/webpack/webpack-dev-middleware/releases) - [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4) --- updated-dependencies: - dependency-name: webpack-dev-middleware dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 404f68511..54c233140 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9141,9 +9141,9 @@ webpack-cli@^4.10.0: webpack-merge "^5.7.3" webpack-dev-middleware@^5.3.1: - version "5.3.3" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f" - integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== + version "5.3.4" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" + integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== dependencies: colorette "^2.0.10" memfs "^3.4.3" From 68569ab9c1b642959494bfe6d10b05b541b4f066 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:06:05 +0200 Subject: [PATCH 31/37] Bump google.golang.org/protobuf in /infrastructure/lib/go/k8s/handler (#4152) Bumps google.golang.org/protobuf from 1.28.0 to 1.33.0. --- updated-dependencies: - dependency-name: google.golang.org/protobuf dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- infrastructure/lib/go/k8s/handler/go.mod | 2 +- infrastructure/lib/go/k8s/handler/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/infrastructure/lib/go/k8s/handler/go.mod b/infrastructure/lib/go/k8s/handler/go.mod index 5621294cb..065623955 100644 --- a/infrastructure/lib/go/k8s/handler/go.mod +++ b/infrastructure/lib/go/k8s/handler/go.mod @@ -34,7 +34,7 @@ require ( golang.org/x/text v0.7.0 // indirect golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.28.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/infrastructure/lib/go/k8s/handler/go.sum b/infrastructure/lib/go/k8s/handler/go.sum index a7e95d05a..29fb9918c 100644 --- a/infrastructure/lib/go/k8s/handler/go.sum +++ b/infrastructure/lib/go/k8s/handler/go.sum @@ -605,8 +605,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 77c4c2f763eab90295c35b4a8e79424729cfe1af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:06:30 +0200 Subject: [PATCH 32/37] Bump webpack-dev-middleware from 5.3.3 to 5.3.4 in /docs (#4154) Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.3 to 5.3.4. - [Release notes](https://github.com/webpack/webpack-dev-middleware/releases) - [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4) --- updated-dependencies: - dependency-name: webpack-dev-middleware dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/yarn.lock b/docs/yarn.lock index 521a13fdb..89bc9054d 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -7378,9 +7378,9 @@ webpack-bundle-analyzer@^4.5.0: ws "^7.3.1" webpack-dev-middleware@^5.3.1: - version "5.3.3" - resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f" - integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== + version "5.3.4" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" + integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== dependencies: colorette "^2.0.10" memfs "^3.4.3" From c589bba0f0d929ff7551a1422735477bedea5bce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:32:33 +0200 Subject: [PATCH 33/37] Bump google.golang.org/protobuf from 1.28.0 to 1.33.0 (#4155) Bumps google.golang.org/protobuf from 1.28.0 to 1.33.0. --- updated-dependencies: - dependency-name: google.golang.org/protobuf dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 13 +++++++------ go.sum | 53 +++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 4e14d025b..5e80f333d 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ go 1.18 require ( github.com/airyhq/airy/lib/go/httpclient v0.0.0-20230426122014-b3add0488aa1 github.com/airyhq/airy/lib/go/payloads v0.0.0-20230426122014-b3add0488aa1 + github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/gorilla/mux v1.8.0 k8s.io/api v0.24.2 @@ -24,7 +25,7 @@ require ( github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/swag v0.21.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic v0.6.9 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/iancoleman/strcase v0.2.0 // indirect @@ -36,14 +37,14 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/rogpeppe/go-internal v1.8.1 // indirect - golang.org/x/net v0.7.0 // indirect + golang.org/x/net v0.8.0 // indirect golang.org/x/oauth2 v0.0.0-20220630143837-2104d58473e0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/term v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/term v0.6.0 // indirect + golang.org/x/text v0.8.0 // indirect golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.28.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index ed0dd7fbb..a071abe33 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,7 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= @@ -45,6 +46,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ 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/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/hcsshim v0.9.4 h1:mnUj0ivWy6UzbB1uLFqKR6F+ZyiDc7j4iGgHTpO+5+I= 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= @@ -57,6 +60,7 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= 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= @@ -67,10 +71,18 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 h1:icCHutJouWlQREayFwCc7lxDAhws08td+W3/gdqgZts= +github.com/confluentinc/confluent-kafka-go/v2 v2.3.0/go.mod h1:/VTy8iEpe6mD9pkCH5BhijlUl8ulUXymKv1Qig5Rgb8= +github.com/containerd/cgroups v1.0.4 h1:jN/mbWBEaz+T1pi5OFtnkQ+8qnmEbAr1Oo1FRm5B0dA= +github.com/containerd/containerd v1.6.8 h1:h4dOFDwzHmqFEP754PgfgTeVXFnLiRc6kiqC7tplDJs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 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= @@ -119,6 +131,7 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU 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/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/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= @@ -144,8 +157,9 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 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/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= @@ -184,6 +198,7 @@ github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 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/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= @@ -217,6 +232,7 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= 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/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -224,12 +240,16 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/sys/mount v0.3.3 h1:fX1SVkXFJ47XWDoeFW4Sq7PdQJnV2QIDZAqjNqgEjUs= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= 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 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= @@ -246,8 +266,12 @@ github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGV github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= +github.com/opencontainers/runc v1.1.3 h1:vIXrkId+0/J2Ymu2m7VjGvbSlAId9XNRPhn2p4b+d8w= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 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= @@ -257,6 +281,7 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -268,7 +293,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/testcontainers/testcontainers-go v0.14.0 h1:h0D5GaYG9mhOWr2qHdEKDXpkce/VlvaYOCzTRi6UBi8= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= @@ -283,6 +309,7 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -366,8 +393,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 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= @@ -439,12 +466,12 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 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= @@ -454,8 +481,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 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= @@ -587,6 +614,7 @@ google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633 h1:0BOZf6qNozI3pkN3fJLwNubheHJYHhMh91GRFOWWK08= 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= @@ -607,6 +635,7 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= 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= @@ -620,8 +649,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 05c5f8a982fbd80a956e1ef01145f3e1770f0363 Mon Sep 17 00:00:00 2001 From: ljupcovangelski Date: Thu, 4 Apr 2024 10:55:19 +0200 Subject: [PATCH 34/37] Fixes #4156 --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index b93005464..316ba4bd9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.55.0-alpha +0.55.0 From 2f052cd199c6df9cca03b738b7ddded0a0dfd59e Mon Sep 17 00:00:00 2001 From: ljupcovangelski Date: Wed, 10 Apr 2024 09:49:39 +0200 Subject: [PATCH 35/37] [#4156] Fix CI --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 02c8436e4..699da3a78 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -81,6 +81,7 @@ jobs: env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_REGION: ${{ secrets.AWS_REGION }} GITHUB_BRANCH: ${{ github.ref }} - name: Publish helm charts From 2efde819c7ce4b762f4e2f9bf7c7071f17250ba4 Mon Sep 17 00:00:00 2001 From: Ljupco Vangelski Date: Mon, 6 May 2024 13:25:24 +0200 Subject: [PATCH 36/37] [#4167] Kafka certificate authentication (#4168) --- .../helm/templates/backend/deployment.yaml | 10 ++++++ .../contacts/helm/templates/deployment.yaml | 10 ++++++ .../facebook/helm/templates/deployments.yaml | 20 +++++++++++ .../google/helm/templates/deployments.yaml | 20 +++++++++++ .../helm/templates/deployment.yaml | 10 ++++++ .../helm/templates/deployment.yaml | 10 ++++++ .../streams/helm/templates/deployment.yaml | 10 ++++++ .../twilio/helm/templates/deployments.yaml | 10 ++++++ .../viber/helm/templates/deployments.yaml | 10 ++++++ .../webhook/helm/templates/deployments.yaml | 10 ++++++ .../whatsapp/helm/templates/deployments.yaml | 10 ++++++ .../docs/getting-started/installation/helm.md | 33 +++++++++++++++++++ .../components/api-admin/deployment.yaml | 10 ++++++ .../api-communication/deployment.yaml | 10 ++++++ .../api-components-installer/deployment.yaml | 11 ++++++- .../components/api-websocket/deployment.yaml | 10 ++++++ .../components/unread-counter/deployment.yaml | 10 ++++++ .../helm-chart/templates/config/kafka.yaml | 1 + infrastructure/helm-chart/values.yaml | 1 + .../airy/kafka/core/KafkaConsumerWrapper.java | 16 ++++++++- .../kafka/streams/KafkaStreamsWrapper.java | 19 ++++++++++- .../spring/kafka/core/KafkaCoreConfig.java | 21 ++++++++++-- .../kafka/streams/KafkaStreamsConfig.java | 5 ++- 23 files changed, 270 insertions(+), 7 deletions(-) diff --git a/backend/components/chat-plugin/helm/templates/backend/deployment.yaml b/backend/components/chat-plugin/helm/templates/backend/deployment.yaml index 1f8995b81..a82f25fd2 100644 --- a/backend/components/chat-plugin/helm/templates/backend/deployment.yaml +++ b/backend/components/chat-plugin/helm/templates/backend/deployment.yaml @@ -51,6 +51,11 @@ spec: initialDelaySeconds: 120 periodSeconds: 10 failureThreshold: 3 +{{ if .Values.global.kafkaCertAuth }} + volumeMounts: + - name: kafka-config-certs + mountPath: /opt/kafka/certs +{{ end }} resources: {{ toYaml .Values.backend.resources | indent 10 }} initContainers: @@ -68,3 +73,8 @@ spec: - name: provisioning-scripts configMap: name: provisioning-scripts +{{ if .Values.global.kafkaCertAuth }} + - name: kafka-config-certs + configMap: + name: kafka-config-certs +{{ end }} \ No newline at end of file diff --git a/backend/components/contacts/helm/templates/deployment.yaml b/backend/components/contacts/helm/templates/deployment.yaml index 5e4c6846a..b0d2e68d9 100644 --- a/backend/components/contacts/helm/templates/deployment.yaml +++ b/backend/components/contacts/helm/templates/deployment.yaml @@ -45,6 +45,11 @@ spec: initialDelaySeconds: 120 periodSeconds: 10 failureThreshold: 3 +{{ if .Values.global.kafkaCertAuth }} + volumeMounts: + - name: kafka-config-certs + mountPath: /opt/kafka/certs +{{ end }} resources: {{ toYaml .Values.resources | indent 10 }} initContainers: @@ -62,3 +67,8 @@ spec: - name: provisioning-scripts configMap: name: provisioning-scripts +{{ if .Values.global.kafkaCertAuth }} + - name: kafka-config-certs + configMap: + name: kafka-config-certs +{{ end }} \ No newline at end of file diff --git a/backend/components/facebook/helm/templates/deployments.yaml b/backend/components/facebook/helm/templates/deployments.yaml index 4d72ef517..a530a8122 100644 --- a/backend/components/facebook/helm/templates/deployments.yaml +++ b/backend/components/facebook/helm/templates/deployments.yaml @@ -59,6 +59,11 @@ spec: - name: Health-Check value: health-check initialDelaySeconds: 120 +{{ if .Values.global.kafkaCertAuth }} + volumeMounts: + - name: kafka-config-certs + mountPath: /opt/kafka/certs +{{ end }} resources: {{ toYaml .Values.connector.resources | indent 12 }} initContainers: @@ -76,6 +81,11 @@ spec: - name: provisioning-scripts configMap: name: provisioning-scripts +{{ if .Values.global.kafkaCertAuth }} + - name: kafka-config-certs + configMap: + name: kafka-config-certs +{{ end }} --- apiVersion: apps/v1 kind: Deployment @@ -124,6 +134,11 @@ spec: initialDelaySeconds: 120 periodSeconds: 10 failureThreshold: 3 +{{ if .Values.global.kafkaCertAuth }} + volumeMounts: + - name: kafka-config-certs + mountPath: /opt/kafka/certs +{{ end }} resources: {{ toYaml .Values.eventsRouter.resources | indent 10 }} initContainers: @@ -141,3 +156,8 @@ spec: - name: provisioning-scripts configMap: name: provisioning-scripts +{{ if .Values.global.kafkaCertAuth }} + - name: kafka-config-certs + configMap: + name: kafka-config-certs +{{ end }} \ No newline at end of file diff --git a/backend/components/google/helm/templates/deployments.yaml b/backend/components/google/helm/templates/deployments.yaml index 7af611787..80c2af237 100644 --- a/backend/components/google/helm/templates/deployments.yaml +++ b/backend/components/google/helm/templates/deployments.yaml @@ -54,6 +54,11 @@ spec: - name: Health-Check value: health-check initialDelaySeconds: 120 +{{ if .Values.global.kafkaCertAuth }} + volumeMounts: + - name: kafka-config-certs + mountPath: /opt/kafka/certs +{{ end }} resources: {{ toYaml .Values.connector.resources | indent 12 }} initContainers: @@ -71,6 +76,11 @@ spec: - name: provisioning-scripts configMap: name: provisioning-scripts +{{ if .Values.global.kafkaCertAuth }} + - name: kafka-config-certs + configMap: + name: kafka-config-certs +{{ end }} --- apiVersion: apps/v1 kind: Deployment @@ -122,6 +132,11 @@ spec: initialDelaySeconds: 120 periodSeconds: 10 failureThreshold: 3 +{{ if .Values.global.kafkaCertAuth }} + volumeMounts: + - name: kafka-config-certs + mountPath: /opt/kafka/certs +{{ end }} resources: {{ toYaml .Values.eventsRouter.resources | indent 10 }} initContainers: @@ -139,3 +154,8 @@ spec: - name: provisioning-scripts configMap: name: provisioning-scripts +{{ if .Values.global.kafkaCertAuth }} + - name: kafka-config-certs + configMap: + name: kafka-config-certs +{{ end }} \ No newline at end of file diff --git a/backend/components/media-resolver/helm/templates/deployment.yaml b/backend/components/media-resolver/helm/templates/deployment.yaml index b31af97ff..3484e0b71 100644 --- a/backend/components/media-resolver/helm/templates/deployment.yaml +++ b/backend/components/media-resolver/helm/templates/deployment.yaml @@ -45,6 +45,11 @@ spec: initialDelaySeconds: 120 periodSeconds: 10 failureThreshold: 3 +{{ if .Values.global.kafkaCertAuth }} + volumeMounts: + - name: kafka-config-certs + mountPath: /opt/kafka/certs +{{ end }} resources: {{ toYaml .Values.resources | indent 12 }} initContainers: @@ -62,3 +67,8 @@ spec: - name: provisioning-scripts configMap: name: provisioning-scripts +{{ if .Values.global.kafkaCertAuth }} + - name: kafka-config-certs + configMap: + name: kafka-config-certs +{{ end }} \ No newline at end of file diff --git a/backend/components/sources-api/helm/templates/deployment.yaml b/backend/components/sources-api/helm/templates/deployment.yaml index 9ce7cda66..4d23b16de 100644 --- a/backend/components/sources-api/helm/templates/deployment.yaml +++ b/backend/components/sources-api/helm/templates/deployment.yaml @@ -50,6 +50,11 @@ spec: initialDelaySeconds: 120 periodSeconds: 10 failureThreshold: 3 +{{ if .Values.global.kafkaCertAuth }} + volumeMounts: + - name: kafka-config-certs + mountPath: /opt/kafka/certs +{{ end }} resources: {{ toYaml .Values.resources | indent 10 }} initContainers: @@ -67,3 +72,8 @@ spec: - name: provisioning-scripts configMap: name: provisioning-scripts +{{ if .Values.global.kafkaCertAuth }} + - name: kafka-config-certs + configMap: + name: kafka-config-certs +{{ end }} \ No newline at end of file diff --git a/backend/components/streams/helm/templates/deployment.yaml b/backend/components/streams/helm/templates/deployment.yaml index 067e9bf6c..b0cd1dfb3 100644 --- a/backend/components/streams/helm/templates/deployment.yaml +++ b/backend/components/streams/helm/templates/deployment.yaml @@ -48,6 +48,11 @@ spec: initialDelaySeconds: 120 periodSeconds: 10 failureThreshold: 3 +{{ if .Values.global.kafkaCertAuth }} + volumeMounts: + - name: kafka-config-certs + mountPath: /opt/kafka/certs +{{ end }} resources: {{ toYaml .Values.resources | indent 10 }} initContainers: @@ -67,3 +72,8 @@ spec: - name: provisioning-scripts configMap: name: provisioning-scripts +{{ if .Values.global.kafkaCertAuth }} + - name: kafka-config-certs + configMap: + name: kafka-config-certs +{{ end }} \ No newline at end of file diff --git a/backend/components/twilio/helm/templates/deployments.yaml b/backend/components/twilio/helm/templates/deployments.yaml index 0e3a6a570..461dd6c30 100644 --- a/backend/components/twilio/helm/templates/deployments.yaml +++ b/backend/components/twilio/helm/templates/deployments.yaml @@ -54,6 +54,11 @@ spec: - name: Health-Check value: health-check initialDelaySeconds: 120 +{{ if .Values.global.kafkaCertAuth }} + volumeMounts: + - name: kafka-config-certs + mountPath: /opt/kafka/certs +{{ end }} resources: {{ toYaml .Values.connector.resources | indent 12 }} initContainers: @@ -141,3 +146,8 @@ spec: - name: provisioning-scripts configMap: name: provisioning-scripts +{{ if .Values.global.kafkaCertAuth }} + - name: kafka-config-certs + configMap: + name: kafka-config-certs +{{ end }} \ No newline at end of file diff --git a/backend/components/viber/helm/templates/deployments.yaml b/backend/components/viber/helm/templates/deployments.yaml index ebb0d5254..55fbb10d7 100644 --- a/backend/components/viber/helm/templates/deployments.yaml +++ b/backend/components/viber/helm/templates/deployments.yaml @@ -49,6 +49,11 @@ spec: - name: Health-Check value: health-check initialDelaySeconds: 120 +{{ if .Values.global.kafkaCertAuth }} + volumeMounts: + - name: kafka-config-certs + mountPath: /opt/kafka/certs +{{ end }} resources: {{ toYaml .Values.connector.resources | indent 12 }} initContainers: @@ -66,3 +71,8 @@ spec: - name: provisioning-scripts configMap: name: provisioning-scripts +{{ if .Values.global.kafkaCertAuth }} + - name: kafka-config-certs + configMap: + name: kafka-config-certs +{{ end }} \ No newline at end of file diff --git a/backend/components/webhook/helm/templates/deployments.yaml b/backend/components/webhook/helm/templates/deployments.yaml index 814a41c9a..6c6cb5b1b 100644 --- a/backend/components/webhook/helm/templates/deployments.yaml +++ b/backend/components/webhook/helm/templates/deployments.yaml @@ -55,6 +55,11 @@ spec: - name: Health-Check value: health-check initialDelaySeconds: 120 +{{ if .Values.global.kafkaCertAuth }} + volumeMounts: + - name: kafka-config-certs + mountPath: /opt/kafka/certs +{{ end }} resources: {{ toYaml .Values.consumer.resources | indent 10 }} initContainers: @@ -157,3 +162,8 @@ spec: - name: provisioning-scripts configMap: name: provisioning-scripts +{{ if .Values.global.kafkaCertAuth }} + - name: kafka-config-certs + configMap: + name: kafka-config-certs +{{ end }} \ No newline at end of file diff --git a/backend/components/whatsapp/helm/templates/deployments.yaml b/backend/components/whatsapp/helm/templates/deployments.yaml index 1fcbff4b1..4c5e04b48 100644 --- a/backend/components/whatsapp/helm/templates/deployments.yaml +++ b/backend/components/whatsapp/helm/templates/deployments.yaml @@ -113,6 +113,11 @@ spec: initialDelaySeconds: 120 periodSeconds: 10 failureThreshold: 3 +{{ if .Values.global.kafkaCertAuth }} + volumeMounts: + - name: kafka-config-certs + mountPath: /opt/kafka/certs +{{ end }} resources: {{ toYaml .Values.eventsRouter.resources | indent 10 }} initContainers: @@ -130,3 +135,8 @@ spec: - name: provisioning-scripts configMap: name: provisioning-scripts +{{ if .Values.global.kafkaCertAuth }} + - name: kafka-config-certs + configMap: + name: kafka-config-certs +{{ end }} \ No newline at end of file diff --git a/docs/docs/getting-started/installation/helm.md b/docs/docs/getting-started/installation/helm.md index e47ed9b44..4d3738cc0 100644 --- a/docs/docs/getting-started/installation/helm.md +++ b/docs/docs/getting-started/installation/helm.md @@ -290,6 +290,39 @@ Run the following command to create the `Airy` platform without the bundled inst helm install airy airy/airy --timeout 10m --set prerequisites.kafka.enabled=false --values ./airy.yaml ``` +#### Confluent + +To connect to a Kafka instance in Confluent cloud, settings the `config.kafka.brokers` and `config.kafka.aurhJaas` is enough, prior to deploying the Helm chart. + +#### Aiven + +Aiven cloud uses a keystore and truststore certificates that need to be loaded on the workloads that are connecting to Kafka. Get the necessary certificates and connection files from Aiven using the `avn` CLI and place them in a separate directory. + +``` +avn service user-kafka-java-creds {KAFKA_INSTANCE} --username {USERNAME} -d ./aiven/ --password {PASSWORD} +``` + +Create a Kubernetes ConfigMap that contains the contents of the created directory: + +``` +kubectl create configmap kafka-config-certs --from-file aiven/ +``` + +Set the connection appropriate parameters in your `airy.yaml` file: + +```yaml +config: + kafka: + brokers: "the-aiven-kafka-broker-url" + keyTrustSecret: "the-key-trust-secret" +``` + +Then install Airy with the following command: + +```sh +helm install airy airy/airy --timeout 10m --set prerequisites.kafka.enabled=false --set global.kafkaCertAuth=true --values ./airy.yaml +``` + ### Kafka partitions per topic Currently all the default topics in the Airy instance are created with 10 partitions. To create these topics with a different number of partitions, add the following to your `airy.yaml` file before running `helm install` (before the initial creation of the topics): diff --git a/infrastructure/helm-chart/templates/components/api-admin/deployment.yaml b/infrastructure/helm-chart/templates/components/api-admin/deployment.yaml index 75cab9b8d..3af2e1344 100644 --- a/infrastructure/helm-chart/templates/components/api-admin/deployment.yaml +++ b/infrastructure/helm-chart/templates/components/api-admin/deployment.yaml @@ -60,6 +60,11 @@ spec: initialDelaySeconds: 120 periodSeconds: 10 failureThreshold: 3 +{{ if .Values.global.kafkaCertAuth }} + volumeMounts: + - name: kafka-config-certs + mountPath: /opt/kafka/certs +{{ end }} resources: {{ toYaml .Values.components.api.admin.resources | indent 10 }} initContainers: @@ -77,3 +82,8 @@ spec: - name: provisioning-scripts configMap: name: provisioning-scripts +{{ if .Values.global.kafkaCertAuth }} + - name: kafka-config-certs + configMap: + name: kafka-config-certs +{{ end }} \ No newline at end of file diff --git a/infrastructure/helm-chart/templates/components/api-communication/deployment.yaml b/infrastructure/helm-chart/templates/components/api-communication/deployment.yaml index 362428ee8..a461864d1 100644 --- a/infrastructure/helm-chart/templates/components/api-communication/deployment.yaml +++ b/infrastructure/helm-chart/templates/components/api-communication/deployment.yaml @@ -45,6 +45,11 @@ spec: initialDelaySeconds: 120 periodSeconds: 10 failureThreshold: 3 +{{ if .Values.global.kafkaCertAuth }} + volumeMounts: + - name: kafka-config-certs + mountPath: /opt/kafka/certs +{{ end }} resources: {{ toYaml .Values.components.api.communication.resources | indent 10 }} initContainers: @@ -62,3 +67,8 @@ spec: - name: provisioning-scripts configMap: name: provisioning-scripts +{{ if .Values.global.kafkaCertAuth }} + - name: kafka-config-certs + configMap: + name: kafka-config-certs +{{ end }} \ No newline at end of file diff --git a/infrastructure/helm-chart/templates/components/api-components-installer/deployment.yaml b/infrastructure/helm-chart/templates/components/api-components-installer/deployment.yaml index fd865b890..363863faf 100644 --- a/infrastructure/helm-chart/templates/components/api-components-installer/deployment.yaml +++ b/infrastructure/helm-chart/templates/components/api-components-installer/deployment.yaml @@ -81,6 +81,11 @@ spec: initialDelaySeconds: 60 periodSeconds: 10 failureThreshold: 3 +{{ if .Values.global.kafkaCertAuth }} + volumeMounts: + - name: kafka-config-certs + mountPath: /opt/kafka/certs +{{ end }} resources: {{ toYaml .Values.components.api.components.installer.resources | indent 10 }} initContainers: @@ -102,4 +107,8 @@ spec: - name: provisioning-scripts configMap: name: provisioning-scripts - +{{ if .Values.global.kafkaCertAuth }} + - name: kafka-config-certs + configMap: + name: kafka-config-certs +{{ end }} \ No newline at end of file diff --git a/infrastructure/helm-chart/templates/components/api-websocket/deployment.yaml b/infrastructure/helm-chart/templates/components/api-websocket/deployment.yaml index 6739cfe3c..c4d95e0c5 100644 --- a/infrastructure/helm-chart/templates/components/api-websocket/deployment.yaml +++ b/infrastructure/helm-chart/templates/components/api-websocket/deployment.yaml @@ -45,6 +45,11 @@ spec: initialDelaySeconds: 120 periodSeconds: 10 failureThreshold: 3 +{{ if .Values.global.kafkaCertAuth }} + volumeMounts: + - name: kafka-config-certs + mountPath: /opt/kafka/certs +{{ end }} resources: {{ toYaml .Values.components.api.websocket.resources | indent 10 }} initContainers: @@ -62,3 +67,8 @@ spec: - name: provisioning-scripts configMap: name: provisioning-scripts +{{ if .Values.global.kafkaCertAuth }} + - name: kafka-config-certs + configMap: + name: kafka-config-certs +{{ end }} \ No newline at end of file diff --git a/infrastructure/helm-chart/templates/components/unread-counter/deployment.yaml b/infrastructure/helm-chart/templates/components/unread-counter/deployment.yaml index c8650099f..4b4421256 100644 --- a/infrastructure/helm-chart/templates/components/unread-counter/deployment.yaml +++ b/infrastructure/helm-chart/templates/components/unread-counter/deployment.yaml @@ -45,6 +45,11 @@ spec: initialDelaySeconds: 120 periodSeconds: 10 failureThreshold: 3 +{{ if .Values.global.kafkaCertAuth }} + volumeMounts: + - name: kafka-config-certs + mountPath: /opt/kafka/certs +{{ end }} resources: {{ toYaml .Values.components.api.unread_counter.resources | indent 10 }} initContainers: @@ -62,3 +67,8 @@ spec: - name: provisioning-scripts configMap: name: provisioning-scripts +{{ if .Values.global.kafkaCertAuth }} + - name: kafka-config-certs + configMap: + name: kafka-config-certs +{{ end }} \ No newline at end of file diff --git a/infrastructure/helm-chart/templates/config/kafka.yaml b/infrastructure/helm-chart/templates/config/kafka.yaml index a00933d80..23897e721 100644 --- a/infrastructure/helm-chart/templates/config/kafka.yaml +++ b/infrastructure/helm-chart/templates/config/kafka.yaml @@ -13,3 +13,4 @@ data: {{- end }} KAFKA_SCHEMA_REGISTRY_URL: {{ .Values.config.kafka.schemaRegistryUrl }} KAFKA_COMMIT_INTERVAL_MS: "{{ .Values.config.kafka.commitInterval }}" + KAFKA_KEY_TRUST_SECRET: {{ .Values.config.kafka.keyTrustSecret }} diff --git a/infrastructure/helm-chart/values.yaml b/infrastructure/helm-chart/values.yaml index e4f6afa18..982a736b5 100644 --- a/infrastructure/helm-chart/values.yaml +++ b/infrastructure/helm-chart/values.yaml @@ -13,6 +13,7 @@ config: brokers: "kafka-headless:9092" zookeepers: "zookeeper:2181" authJaas: "" + keyTrustSecret: "" minimumReplicas: 1 schemaRegistryUrl: "http://schema-registry:8081" commitInterval: 1000 diff --git a/lib/java/kafka/core/src/main/java/co/airy/kafka/core/KafkaConsumerWrapper.java b/lib/java/kafka/core/src/main/java/co/airy/kafka/core/KafkaConsumerWrapper.java index b5739d950..ca75dc902 100644 --- a/lib/java/kafka/core/src/main/java/co/airy/kafka/core/KafkaConsumerWrapper.java +++ b/lib/java/kafka/core/src/main/java/co/airy/kafka/core/KafkaConsumerWrapper.java @@ -12,6 +12,10 @@ import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.Properties; +import org.apache.kafka.clients.CommonClientConfigs; +import org.apache.kafka.common.config.SslConfigs; +import java.util.HashMap; + public class KafkaConsumerWrapper { @@ -22,6 +26,7 @@ public class KafkaConsumerWrapper { private KafkaConsumer consumer; private String jaasConfig; + private String kafkaKeyTrustSecret; public KafkaConsumerWrapper(final String brokers, final String schemaRegistryUrl) { props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers); @@ -33,13 +38,22 @@ public KafkaConsumerWrapper(final String brokers, final String schemaRegistryUrl props.put(KafkaAvroDeserializerConfig.SPECIFIC_AVRO_READER_CONFIG, true); } - public KafkaConsumerWrapper withAuthJaas(String jaasConfig) { + public KafkaConsumerWrapper withAuthJaas(String jaasConfig, String kafkaKeyTrustSecret) { this.jaasConfig = jaasConfig; if(jaasConfig != null) { props.put("security.protocol", "SASL_SSL"); props.put("sasl.mechanism", "PLAIN"); props.put("sasl.jaas.config", jaasConfig); } + if (kafkaKeyTrustSecret != null) { + props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SSL"); + props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, "/opt/kafka/certs/client.truststore.jks"); + props.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, kafkaKeyTrustSecret); + props.put(SslConfigs.SSL_KEYSTORE_TYPE_CONFIG, "PKCS12"); + props.put(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, "/opt/kafka/certs/client.keystore.p12"); + props.put(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, kafkaKeyTrustSecret); + props.put(SslConfigs.SSL_KEY_PASSWORD_CONFIG, kafkaKeyTrustSecret); + } return this; } diff --git a/lib/java/kafka/streams/src/main/java/co/airy/kafka/streams/KafkaStreamsWrapper.java b/lib/java/kafka/streams/src/main/java/co/airy/kafka/streams/KafkaStreamsWrapper.java index f44f5075a..18ca499ca 100644 --- a/lib/java/kafka/streams/src/main/java/co/airy/kafka/streams/KafkaStreamsWrapper.java +++ b/lib/java/kafka/streams/src/main/java/co/airy/kafka/streams/KafkaStreamsWrapper.java @@ -30,6 +30,10 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; +import org.apache.kafka.clients.CommonClientConfigs; +import org.apache.kafka.common.config.SslConfigs; +import java.util.HashMap; + public class KafkaStreamsWrapper { private static final Logger log = AiryLoggerFactory.getLogger(KafkaStreamsWrapper.class); @@ -37,6 +41,7 @@ public class KafkaStreamsWrapper { private final String brokers; private final String schemaRegistryUrl; private String jaasConfig; + private String kafkaKeyTrustSecret; private long commitIntervalInMs; private long suppressIntervalInMs; private int threadCount; @@ -70,8 +75,9 @@ public KafkaStreamsWrapper(final String brokers, final String schemaRegistryUrl) healthCheckRunnerThread = new HealthCheckRunner(testMode); } - public KafkaStreamsWrapper withJaasConfig(String jaasConfig) { + public KafkaStreamsWrapper withJaasConfig(String jaasConfig, String kafkaKeyTrustSecret) { this.jaasConfig = jaasConfig; + this.kafkaKeyTrustSecret = kafkaKeyTrustSecret; return this; } @@ -227,6 +233,17 @@ public synchronized void start(final Topology topology, final String appId) thro props.put("sasl.jaas.config", jaasConfig); } + if (this.kafkaKeyTrustSecret != null) { + props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SSL"); + props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, "/opt/kafka/certs/client.truststore.jks"); + props.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, kafkaKeyTrustSecret); + props.put(SslConfigs.SSL_KEYSTORE_TYPE_CONFIG, "PKCS12"); + props.put(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, "/opt/kafka/certs/client.keystore.p12"); + props.put(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, kafkaKeyTrustSecret); + props.put(SslConfigs.SSL_KEY_PASSWORD_CONFIG, kafkaKeyTrustSecret); + } + + props.put(ProducerConfig.MAX_REQUEST_SIZE_CONFIG, this.maxRequestSize); props.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, this.fetchMaxBytes); diff --git a/lib/java/spring/kafka/core/src/main/java/co/airy/spring/kafka/core/KafkaCoreConfig.java b/lib/java/spring/kafka/core/src/main/java/co/airy/spring/kafka/core/KafkaCoreConfig.java index 0cd6c2812..f8488e2f6 100644 --- a/lib/java/spring/kafka/core/src/main/java/co/airy/spring/kafka/core/KafkaCoreConfig.java +++ b/lib/java/spring/kafka/core/src/main/java/co/airy/spring/kafka/core/KafkaCoreConfig.java @@ -13,6 +13,9 @@ import org.springframework.context.annotation.Scope; import java.util.Properties; +import org.apache.kafka.clients.CommonClientConfigs; +import org.apache.kafka.common.config.SslConfigs; +import java.util.HashMap; @Configuration @PropertySource("classpath:kafka-core.properties") @@ -21,7 +24,8 @@ public class KafkaCoreConfig { @Lazy @Scope("prototype") public KafkaProducer kafkaProducer(@Value("${kafka.brokers}") final String brokers, @Value("${kafka.schema-registry-url}") final String schemaRegistryUrl, - @Value("${AUTH_JAAS:#{null}}") final String jaasConfig) { + @Value("${AUTH_JAAS:#{null}}") final String jaasConfig, + @Value("${KAFKA_KEY_TRUST_SECRET:#{null}}") final String kafkaKeyTrustSecret) { final Properties props = new Properties(); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, brokers); @@ -37,6 +41,16 @@ public KafkaProducer kafkaProducer(@Value("${kafka.brokers}") final props.put("sasl.jaas.config", jaasConfig); } + if (kafkaKeyTrustSecret != null) { + props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SSL"); + props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, "/opt/kafka/certs/client.truststore.jks"); + props.put(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG, kafkaKeyTrustSecret); + props.put(SslConfigs.SSL_KEYSTORE_TYPE_CONFIG, "PKCS12"); + props.put(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG, "/opt/kafka/certs/client.keystore.p12"); + props.put(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG, kafkaKeyTrustSecret); + props.put(SslConfigs.SSL_KEY_PASSWORD_CONFIG, kafkaKeyTrustSecret); + } + return new KafkaProducer<>(props); } @@ -44,8 +58,9 @@ public KafkaProducer kafkaProducer(@Value("${kafka.brokers}") final @Lazy @Scope("prototype") public KafkaConsumerWrapper kafkaConsumer(@Value("${kafka.brokers}") final String brokers, @Value("${kafka.schema-registry-url}") final String schemaRegistryUrl, - @Value("${kafka.sasl.jaas.config:#{null}}") final String jaasConfig) { + @Value("${kafka.sasl.jaas.config:#{null}}") final String jaasConfig, + @Value("${KAFKA_KEY_TRUST_SECRET:#{null}}") final String kafkaKeyTrustSecret) { return new KafkaConsumerWrapper(brokers, schemaRegistryUrl) - .withAuthJaas(jaasConfig); + .withAuthJaas(jaasConfig, kafkaKeyTrustSecret); } } diff --git a/lib/java/spring/kafka/streams/src/main/java/co/airy/spring/kafka/streams/KafkaStreamsConfig.java b/lib/java/spring/kafka/streams/src/main/java/co/airy/spring/kafka/streams/KafkaStreamsConfig.java index f9995abfb..217f2f9a6 100644 --- a/lib/java/spring/kafka/streams/src/main/java/co/airy/spring/kafka/streams/KafkaStreamsConfig.java +++ b/lib/java/spring/kafka/streams/src/main/java/co/airy/spring/kafka/streams/KafkaStreamsConfig.java @@ -30,6 +30,9 @@ public class KafkaStreamsConfig { @Value("${AUTH_JAAS:#{null}}") private String jaasConfig; + @Value("${KAFKA_KEY_TRUST_SECRET:#{null}}") + private String kafkaKeyTrustSecret; + @Value("${kafka.rpc-port:0}") private int rpcPort; @@ -68,7 +71,7 @@ public class KafkaStreamsConfig { public KafkaStreamsWrapper airyKafkaStreams(@Value("${kafka.brokers}") final String brokers, @Value("${kafka.schema-registry-url}") final String schemaRegistryUrl) { return new KafkaStreamsWrapper(brokers, schemaRegistryUrl) .withCommitIntervalInMs(commitIntervalMs) - .withJaasConfig(jaasConfig) + .withJaasConfig(jaasConfig, kafkaKeyTrustSecret) .withSuppressIntervalInMs(suppressIntervalMs) .withThreadCount(streamsThreadCount) .withAppServerHost(rpcHost) From 397e04be7d63fe9091b44974158344b6201e6c21 Mon Sep 17 00:00:00 2001 From: ljupcovangelski Date: Thu, 9 May 2024 11:49:53 +0200 Subject: [PATCH 37/37] Update changelog # --- docs/docs/changelog.md | 93 ++++++++++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 35 deletions(-) diff --git a/docs/docs/changelog.md b/docs/docs/changelog.md index 0b0cb793a..9d4aa94e5 100644 --- a/docs/docs/changelog.md +++ b/docs/docs/changelog.md @@ -3,6 +3,64 @@ title: Changelog sidebar_label: 📝 Changelog --- +## 0.55.0 + +#### Changes + +- [[#4167](https://github.com/airyhq/airy/issues/4167)] Kafka certificate authentication [[#4168](https://github.com/airyhq/airy/pull/4168)] +- [[#4156](https://github.com/airyhq/airy/issues/4156)] Change name for Pinecone connector [[#4127](https://github.com/airyhq/airy/pull/4127)] +- [[#4156](https://github.com/airyhq/airy/issues/4156)] Add description for components [[#4126](https://github.com/airyhq/airy/pull/4126)] +- [[#4156](https://github.com/airyhq/airy/issues/4156)] Add LLM filter in Catalog [[#4125](https://github.com/airyhq/airy/pull/4125)] + +#### 🚀 Features + +- [[#4139](https://github.com/airyhq/airy/issues/4139)] Add Flink connector [[#4147](https://github.com/airyhq/airy/pull/4147)] +- [[#3945](https://github.com/airyhq/airy/issues/3945)] Slack connector [[#4146](https://github.com/airyhq/airy/pull/4146)] +- [[#4144](https://github.com/airyhq/airy/issues/4144)] Adapt Frontend with Schema Manager [[#4145](https://github.com/airyhq/airy/pull/4145)] +- [[#4141](https://github.com/airyhq/airy/issues/4141)] Schema Registry Manager [[#4143](https://github.com/airyhq/airy/pull/4143)] +- [[#4137](https://github.com/airyhq/airy/issues/4137)] Improve Kafka Sections [[#4138](https://github.com/airyhq/airy/pull/4138)] +- [[#4132](https://github.com/airyhq/airy/issues/4132)] Added Copilot source to libs and apps [[#4133](https://github.com/airyhq/airy/pull/4133)] + +#### 🐛 Bug Fixes + +- [[#4156](https://github.com/airyhq/airy/issues/4156)] Fix selecting fields in streams [[#4123](https://github.com/airyhq/airy/pull/4123)] +- [[#4156](https://github.com/airyhq/airy/issues/4156)] Fix symbol [[#4122](https://github.com/airyhq/airy/pull/4122)] +- [[#4156](https://github.com/airyhq/airy/issues/4156)] Update icons for LLM components [[#4120](https://github.com/airyhq/airy/pull/4120)] +- [[#4108](https://github.com/airyhq/airy/issues/4108)] Fix schemas and topics screens [[#4109](https://github.com/airyhq/airy/pull/4109)] + +#### 📚 Documentation + +- [[#4134](https://github.com/airyhq/airy/issues/4134)] Update the diagram in the README file [[#4136](https://github.com/airyhq/airy/pull/4136)] +- [[#4134](https://github.com/airyhq/airy/issues/4134)] Update main diagram [[#4135](https://github.com/airyhq/airy/pull/4135)] +- [[#4156](https://github.com/airyhq/airy/issues/4156)] Improve docs [[#4131](https://github.com/airyhq/airy/pull/4131)] +- [[#4156](https://github.com/airyhq/airy/issues/4156)] Improve docs [[#4128](https://github.com/airyhq/airy/pull/4128)] +- [[#4156](https://github.com/airyhq/airy/issues/4156)] Docs - Remove Solutions link [[#4115](https://github.com/airyhq/airy/pull/4115)] + +#### 🧰 Maintenance + +- Bump google.golang.org/protobuf from 1.28.0 to 1.33.0 [[#4155](https://github.com/airyhq/airy/pull/4155)] +- Bump webpack-dev-middleware from 5.3.3 to 5.3.4 in /docs [[#4154](https://github.com/airyhq/airy/pull/4154)] +- Bump google.golang.org/protobuf from 1.28.0 to 1.33.0 in /infrastructure/lib/go/k8s/handler [[#4152](https://github.com/airyhq/airy/pull/4152)] +- Bump webpack-dev-middleware from 5.3.3 to 5.3.4 [[#4153](https://github.com/airyhq/airy/pull/4153)] +- Bump express from 4.18.2 to 4.19.2 [[#4151](https://github.com/airyhq/airy/pull/4151)] +- Bump express from 4.18.2 to 4.19.2 in /docs [[#4150](https://github.com/airyhq/airy/pull/4150)] +- Bump follow-redirects from 1.15.2 to 1.15.6 [[#4149](https://github.com/airyhq/airy/pull/4149)] +- Bump follow-redirects from 1.15.2 to 1.15.6 in /docs [[#4148](https://github.com/airyhq/airy/pull/4148)] +- Bump github.com/cyphar/filepath-securejoin from 0.2.3 to 0.2.4 [[#4118](https://github.com/airyhq/airy/pull/4118)] +- Bump @adobe/css-tools from 4.0.1 to 4.3.1 [[#4116](https://github.com/airyhq/airy/pull/4116)] +- Bump @svgr/plugin-svgo from 6.5.1 to 8.1.0 [[#4114](https://github.com/airyhq/airy/pull/4114)] +- Bump semver from 5.7.1 to 5.7.2 in /docs [[#4110](https://github.com/airyhq/airy/pull/4110)] +- Bump sass-loader from 13.1.0 to 13.3.2 [[#4103](https://github.com/airyhq/airy/pull/4103)] +- Bump word-wrap from 1.2.3 to 1.2.4 [[#4112](https://github.com/airyhq/airy/pull/4112)] + +#### Airy CLI + +You can download the Airy CLI for your operating system from the following links: + +[MacOS](https://airy-core-binaries.s3.amazonaws.com/0.55.0/darwin/amd64/airy) +[Linux](https://airy-core-binaries.s3.amazonaws.com/0.55.0/linux/amd64/airy) +[Windows](https://airy-core-binaries.s3.amazonaws.com/0.55.0/windows/amd64/airy.exe) + ## 0.54.0 #### 🚀 Features @@ -1348,38 +1406,3 @@ You can download the Airy CLI for your operating system from the following links [Linux](https://airy-core-binaries.s3.amazonaws.com/0.35.0/linux/amd64/airy) [Windows](https://airy-core-binaries.s3.amazonaws.com/0.35.0/windows/amd64/airy.exe) -## 0.34.0 - -#### Changes - -#### 🚀 Features - -- [[#2518](https://github.com/airyhq/airy/issues/2518)] Add fargate annotation [[#2540](https://github.com/airyhq/airy/pull/2540)] -- [[#2305](https://github.com/airyhq/airy/issues/2305)] Add CLI outdated version warning [[#2529](https://github.com/airyhq/airy/pull/2529)] - -#### 🐛 Bug Fixes - -- [[#2434](https://github.com/airyhq/airy/issues/2434)] Fix broken instagram Facebook inbox ingestion [[#2535](https://github.com/airyhq/airy/pull/2535)] -- [[#2457](https://github.com/airyhq/airy/issues/2457)] Fix upgrade to same version [[#2538](https://github.com/airyhq/airy/pull/2538)] -- [[#2510](https://github.com/airyhq/airy/issues/2510)] Improve error logging for helm install [[#2522](https://github.com/airyhq/airy/pull/2522)] -- [[#2255](https://github.com/airyhq/airy/issues/2255)] Fix helm chart url [[#2525](https://github.com/airyhq/airy/pull/2525)] -- [[#2523](https://github.com/airyhq/airy/issues/2523)] Fix VERSION and add changelog [[#2524](https://github.com/airyhq/airy/pull/2524)] -- [[#2473](https://github.com/airyhq/airy/issues/2473)] fix failing cypress test [[#2507](https://github.com/airyhq/airy/pull/2507)] - -#### 🧰 Maintenance - -- Bump react-redux from 7.2.5 to 7.2.6 [[#2539](https://github.com/airyhq/airy/pull/2539)] -- Bump reselect from 4.0.0 to 4.1.1 [[#2533](https://github.com/airyhq/airy/pull/2533)] -- Bump sass-loader from 12.1.0 to 12.3.0 [[#2534](https://github.com/airyhq/airy/pull/2534)] -- Bump @types/react-dom from 17.0.9 to 17.0.10 [[#2526](https://github.com/airyhq/airy/pull/2526)] -- Bump react-markdown from 7.0.1 to 7.1.0 [[#2527](https://github.com/airyhq/airy/pull/2527)] -- Bump webpack from 5.54.0 to 5.59.1 [[#2517](https://github.com/airyhq/airy/pull/2517)] - -#### Airy CLI - -You can download the Airy CLI for your operating system from the following links: - -[MacOS](https://airy-core-binaries.s3.amazonaws.com/0.34.0/darwin/amd64/airy) -[Linux](https://airy-core-binaries.s3.amazonaws.com/0.34.0/linux/amd64/airy) -[Windows](https://airy-core-binaries.s3.amazonaws.com/0.34.0/windows/amd64/airy.exe) -