Sample App: Code Clean up (#5948)
* Fix the content in the bad word filter knative function code * Remove the unused import * Adding the action for the button in the front end, so that the new review comment can be delivered to the review service. * enable cors * Install cors * fix: configure node.js so that it won't be restricted by CORS policy * fix: add hardcoded the address for the review service * feat: the yaml files to run the front end in the k8s cluster * fix: fix the wrong command in the docker file * fix: change the naming convention * fix: update knative slack sink to use the value from secret * fix: change the name for the front end application package * fix: update the event type for the outputs of the ML workflows * fix: create the sequence and trigger for the ML workflow * fix: change the input key to reviewText * fix: change the event type for the input cloudevent * fix: set up the trigger that will filter out the bad comment and send them to the event display * feat: add the loading state for the front end * feat: expose the postgresql service as headless service, so we can use the statefulset's name as the host url * fix: format fix, and also make sure the sentiment analysis result is all lower case * feat: update to use websocket for the comment area * feat: add Kuack to the bookstore as advised by UX WG * feat: add kuack to the bookstore * feat: add the new timing format and also set up the status badge * feat: make sure all the ML output are lower case * feat: styling change * feat: add the database insertion logic * fix: update to use kafka broker * feat: add the quickstart setup script * fix: use deployment instead of knative service * fix: Send the reply as cloud event back to broker * fix: Go back to use MT broker * fix: Restructure the code * fix: Restructure the code * Save progress: TODO: will confirm whether use the pre-built images or build the images locally * fix: change the naming of the files * fix: cut the pulling rate of the websocket * fix: front end clean up and change the error message * fix: change the port to 8080 * fix: fix the setup script * fix: update the front end port to use 80 as the nodeserver, as trigger default to use 80, instead of 8080. * fix: trigger default to use 80, and cannot use customized port 8080. * fix: renaming and use the pre-built image * fix: update the naming and add comments to the yaml * feat: move the code to solution folder * feat: add the start folder * fix: the deployment file is not in the /start folder * fix: shorten the bad word filter name * fix: shorten the bad word filter name * fix: delete the deployment files for ML workflows * fix: add the port 8080 * fix: use Service instead of deployment when setting up temp trigger * fix: add the missing deployment yaml for /start * fix: add the shortcut script in the /start folder * fix: change the favicon to kuack to improve the reader's experience
|
@ -20,7 +20,7 @@ hack/__pycache__
|
|||
!/blog/overrides/partials/source-file.html
|
||||
!/blog/overrides/partials/content.html
|
||||
/blog/docs/stylesheets/
|
||||
/node_modules/
|
||||
node_modules
|
||||
venv/
|
||||
|
||||
# TODO clean up images copied between blog/ and docs/
|
||||
|
@ -36,4 +36,3 @@ maven-wrapper.jar
|
|||
|
||||
# Ignore Python virtual environments
|
||||
.venv
|
||||
/code-samples/eventing/bookstore-sample-app/db/bookstore-eda/node_modules/*
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
specVersion: 0.36.0
|
||||
name: inappropriate-language-filter
|
||||
runtime: python
|
||||
created: 2024-03-27T23:12:06.178272+08:00
|
|
@ -1,50 +0,0 @@
|
|||
from parliament import Context
|
||||
from flask import Request,request, jsonify
|
||||
import json
|
||||
from textblob import TextBlob
|
||||
from time import sleep
|
||||
from cloudevents.http import CloudEvent, to_structured
|
||||
|
||||
# The function to convert the sentiment analysis result into a CloudEvent
|
||||
def create_cloud_event(inputText,data):
|
||||
attributes = {
|
||||
"type": "knative.sampleapp.sentiment.response",
|
||||
"source": "sentiment-analysis",
|
||||
"datacontenttype": "application/json",
|
||||
}
|
||||
|
||||
# Put the sentiment analysis result into a dictionary
|
||||
data = {
|
||||
"input": inputText,
|
||||
"result": data
|
||||
}
|
||||
|
||||
# Create a CloudEvent object
|
||||
event = CloudEvent(attributes, data)
|
||||
|
||||
return event
|
||||
|
||||
def analyze_sentiment(text):
|
||||
analysis = TextBlob(text["input"])
|
||||
sentiment = "Neutral"
|
||||
if analysis.sentiment.polarity > 0:
|
||||
sentiment = "Positive"
|
||||
elif analysis.sentiment.polarity < 0:
|
||||
sentiment = "Negative"
|
||||
|
||||
# Convert the sentiment into a CloudEvent
|
||||
sentiment = create_cloud_event(text["input"],sentiment)
|
||||
|
||||
return sentiment
|
||||
|
||||
def main(context: Context):
|
||||
"""
|
||||
Function template
|
||||
The context parameter contains the Flask request object and any
|
||||
CloudEvent received with the request.
|
||||
"""
|
||||
|
||||
print("Received CloudEvent: ", context.cloud_event)
|
||||
|
||||
# Add your business logic here
|
||||
return analyze_sentiment(context.cloud_event.data)
|
|
@ -1,11 +0,0 @@
|
|||
specVersion: 0.35.0
|
||||
name: sentiment-analysis-app
|
||||
runtime: python
|
||||
registry: <your docker registry>
|
||||
image: <Your docker registry>/sentiment-analysis-app:latest
|
||||
imageDigest: sha256:0bc*****d3c
|
||||
created: 2024-02-31T00:18:00.06485162-04:00
|
||||
build:
|
||||
builder: s2i
|
||||
deploy:
|
||||
namespace: default
|
|
@ -1,4 +0,0 @@
|
|||
apiVersion: eventing.knative.dev/v1
|
||||
kind: Broker
|
||||
metadata:
|
||||
name: broker
|
|
@ -1,21 +0,0 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: eda
|
||||
labels:
|
||||
app: eda
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: eda
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: eda
|
||||
spec:
|
||||
containers:
|
||||
- name: eda
|
||||
image: quay.io/rh-ee-leoli/eda:latest
|
||||
ports:
|
||||
- containerPort: 8000
|
|
@ -1,9 +0,0 @@
|
|||
apiVersion: serving.knative.dev/v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: event-display
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- image: gcr.io/knative-releases/knative.dev/eventing-contrib/cmd/event_display
|
|
@ -1,53 +0,0 @@
|
|||
const express = require('express');
|
||||
const { HTTP, CloudEvent } = require('cloudevents');
|
||||
|
||||
const app = express();
|
||||
const port = 8000;
|
||||
|
||||
|
||||
// Middleware to parse JSON bodies
|
||||
app.use(express.json());
|
||||
|
||||
|
||||
app.post('/add', async (req, res) => {
|
||||
try {
|
||||
const receivedEvent = HTTP.toEvent({ headers: req.headers, body: req.body });
|
||||
const brokerURI = process.env.K_SINK;
|
||||
|
||||
if (receivedEvent.type === 'new-review-comment') {
|
||||
// Forward the event to the broker with the necessary CloudEvent headers
|
||||
const response = await fetch(brokerURI, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'ce-specversion': '1.0',
|
||||
'ce-type': 'sentiment-analysis-request',
|
||||
'ce-source': 'bookstore-eda',
|
||||
'ce-id': receivedEvent.id,
|
||||
},
|
||||
body: JSON.stringify(receivedEvent.data),
|
||||
});
|
||||
|
||||
if (!response.ok) { // If the response status code is not 2xx, consider it a failure
|
||||
console.error('Failed to forward event:', receivedEvent);
|
||||
return res.status(500).json({ error: 'Failed to forward event' });
|
||||
}
|
||||
|
||||
// If forwarding was successful, acknowledge the receipt of the event
|
||||
console.log('Event forwarded successfully:', receivedEvent);
|
||||
return res.status(200).json({ success: true, message: 'Event forwarded successfully' });
|
||||
} else {
|
||||
// Handle unexpected event types
|
||||
console.warn('Unexpected event type:', receivedEvent.type);
|
||||
return res.status(400).json({ error: 'Unexpected event type' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing request:', error);
|
||||
return res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Start the server
|
||||
app.listen(port, () => {
|
||||
console.log(`Server listening at http://localhost:${port}`);
|
||||
});
|
|
@ -1,12 +0,0 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: eda-service
|
||||
spec:
|
||||
selector:
|
||||
app: eda
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 8000
|
||||
type: LoadBalancer
|
|
@ -1,34 +0,0 @@
|
|||
apiVersion: eventing.knative.dev/v1
|
||||
kind: Trigger
|
||||
metadata:
|
||||
name: event-display-trigger
|
||||
spec:
|
||||
broker: broker
|
||||
filter:
|
||||
attributes:
|
||||
type: sentiment-analysis-request
|
||||
subscriber:
|
||||
ref:
|
||||
apiVersion: serving.knative.dev/v1
|
||||
kind: Service
|
||||
name: sentiment-analysis-app
|
||||
|
||||
|
||||
---
|
||||
|
||||
apiVersion: eventing.knative.dev/v1
|
||||
kind: Trigger
|
||||
metadata:
|
||||
name: reply-trigger
|
||||
spec:
|
||||
broker: broker
|
||||
filter:
|
||||
attributes:
|
||||
type: knative.sampleapp.sentiment.response
|
||||
subscriber:
|
||||
ref:
|
||||
apiVersion: serving.knative.dev/v1
|
||||
kind: Service
|
||||
name: event-display
|
||||
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS book_reviews(
|
||||
id SERIAL PRIMARY KEY,
|
||||
post_time timestamp NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
sentiment TEXT,
|
||||
CONSTRAINT sentiment_check CHECK (sentiment IN ('positive', 'negative', 'neutral')),
|
||||
);
|
||||
|
||||
INSERT INTO book_reviews (post_time, content, sentiment) VALUES
|
||||
('2020-01-01 00:00:00', 'This book is great!', 'positive'),
|
||||
('2020-01-02 00:02:00', 'This book is terrible!', 'negative'),
|
||||
('2020-01-03 00:01:30', 'This book is okay.', 'neutral'),
|
||||
('2020-01-04 00:00:00', 'Meh', 'neutral');
|
|
@ -1,38 +0,0 @@
|
|||
import Emoji from './Emoji';
|
||||
const CommentDisplay = ({ comment }) => {
|
||||
// Assume receiving a comment object
|
||||
let emoji;
|
||||
if (comment.emotion === 'Positive') {
|
||||
emoji = '😃';
|
||||
} else if (comment.emotion === 'Neutral') {
|
||||
emoji = '😐';
|
||||
} else {
|
||||
emoji = '😡';
|
||||
}
|
||||
return (
|
||||
<div className='flex my-4 p-4 justify-center align-middle items-center'>
|
||||
<div className='comment-display w-full w-7/12 flex flex-row rounded-lg p-4 bg-gray-800 text-white dark:bg-white dark:text-black'>
|
||||
<div className='flex items-center justify-center md:w-1/12'>
|
||||
<img
|
||||
src={comment.avatar}
|
||||
alt='Avatar'
|
||||
className='rounded-full w-8 h-8'
|
||||
/>
|
||||
</div>
|
||||
<div className='md:w-1/12 flex items-center content-center text-gray-200 dark:text-black'>
|
||||
{comment.time}
|
||||
</div>
|
||||
<div className='md:w-9/12 '>
|
||||
<span className='h-full flex items-center content-center'>
|
||||
{comment.text}
|
||||
</span>
|
||||
</div>
|
||||
<div className='md:w-1/12 text-4xl flex items-center content-center'>
|
||||
<Emoji symbol={emoji} label={comment.emotion} size='text-2xl' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentDisplay;
|
|
@ -1,44 +0,0 @@
|
|||
'use-client';
|
||||
import { useState } from 'react';
|
||||
const CommentForm = () => {
|
||||
const [hover, setHover] = useState(false);
|
||||
const [comment, setComment] = useState('');
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
setComment(event.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
console.log('Submitted comment:', comment); // Use inspect to see
|
||||
};
|
||||
return (
|
||||
<div className='flex my-4 p-4 justify-center'>
|
||||
<form
|
||||
className='w-full w-8/12 flex flex-col items-end '
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<textarea
|
||||
className='form-textarea w-full mb-2 p-2 border border-2 border-black rounded-lg p-4'
|
||||
rows='3'
|
||||
placeholder='Leave your comment here...'
|
||||
value={comment}
|
||||
onChange={handleInputChange}
|
||||
></textarea>
|
||||
<button
|
||||
type='submit'
|
||||
className={`font-bold py-2 px-9 rounded ${
|
||||
hover ? '' : 'bg-blue-600'
|
||||
}`}
|
||||
style={{ backgroundColor: hover ? '#A0DDFF' : '#A5D8FF' }}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentForm;
|
|
@ -1,12 +0,0 @@
|
|||
import CommentDisplay from './CommentDisplay';
|
||||
const CommentList = () => {
|
||||
const comment = {
|
||||
avatar: '/images/avatar.jpg',
|
||||
time: '10:05',
|
||||
text: 'I used this provider to insert a different theme object depending on a person ',
|
||||
emotion: 'Neutral',
|
||||
};
|
||||
return <CommentDisplay comment={comment} />;
|
||||
};
|
||||
|
||||
export default CommentList;
|
|
@ -1,37 +0,0 @@
|
|||
'use client';
|
||||
import Header from '../components/Header';
|
||||
import BookDetail from '../components/BookDetail';
|
||||
import CommentForm from '../components/CommentForm';
|
||||
import CommentList from '../components/CommentList';
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
|
||||
export default function Main() {
|
||||
/* Example Book object */
|
||||
const book = {
|
||||
img: '/images/Bookcover.jpg',
|
||||
title: 'Building serverless applications on Knative',
|
||||
author: 'Evan Anderson',
|
||||
ISBN: '978-1098142070',
|
||||
publisher: 'Oreilly & Associates Inc',
|
||||
publishedDate: 'December 19, 2023',
|
||||
description:
|
||||
'A Guide to Designing and Writing Serverless Cloud Application',
|
||||
price: '$49',
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
|
||||
<main className='container mx-auto my-8'>
|
||||
<BookDetail book={book} />
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<CommentForm />
|
||||
<p className="text-xl font-semibold mt-6 mb-4 text-gray-800 dark:text-gray-200">
|
||||
Comments
|
||||
</p>
|
||||
<CommentList />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -6,13 +6,14 @@ from cloudevents.http import CloudEvent
|
|||
# The function to convert the bad word filter result into a CloudEvent
|
||||
def create_cloud_event(inputText, data):
|
||||
attributes = {
|
||||
"type": "knative.sampleapp.badword.filter.response",
|
||||
"source": "bad-word-filter",
|
||||
"type": "new-review-comment",
|
||||
"source": "book-review-broker",
|
||||
"datacontenttype": "application/json",
|
||||
"badwordfilter": data,
|
||||
}
|
||||
|
||||
# Put the bad word filter result into a dictionary
|
||||
data = {"input": inputText, "result": data}
|
||||
data = {"reviewText": inputText, "badWordResult": data}
|
||||
|
||||
# Create a CloudEvent object
|
||||
event = CloudEvent(attributes, data)
|
||||
|
@ -21,12 +22,12 @@ def create_cloud_event(inputText, data):
|
|||
|
||||
|
||||
def inappropriate_language_filter(text):
|
||||
profanity_result = predict([text["input"]])
|
||||
profanity_result = predict([text["reviewText"]])
|
||||
result = "good"
|
||||
if profanity_result[0] == 1:
|
||||
result = "bad"
|
||||
|
||||
profanity_event = create_cloud_event(text["input"], result)
|
||||
profanity_event = create_cloud_event(text["reviewText"], result)
|
||||
return profanity_event
|
||||
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
specVersion: 0.36.0
|
||||
name: bad-word-filter
|
||||
runtime: python
|
||||
registry: quay.io/rh-ee-leoli
|
||||
image: quay.io/rh-ee-leoli/bad-word-filter:latest
|
||||
imageDigest: sha256:b6bec398e44dc20630856f39ef29195db9a3ee5b43cec0372207c89ad633aeb2
|
||||
created: 2024-03-27T23:12:06.178272+08:00
|
||||
build:
|
||||
builder: s2i
|
||||
deploy:
|
||||
namespace: default
|
|
@ -0,0 +1,61 @@
|
|||
from parliament import Context
|
||||
from flask import Request, request, jsonify
|
||||
import json
|
||||
from textblob import TextBlob
|
||||
from time import sleep
|
||||
from cloudevents.http import CloudEvent, to_structured
|
||||
|
||||
|
||||
# The function to convert the sentiment analysis result into a CloudEvent
|
||||
def create_cloud_event(inputText, badWordResult, data):
|
||||
attributes = {
|
||||
"type": "moderated-comment",
|
||||
"source": "sentiment-analysis",
|
||||
"datacontenttype": "application/json",
|
||||
"sentimentResult": data,
|
||||
"badwordfilter": badWordResult,
|
||||
}
|
||||
|
||||
# Put the sentiment analysis result into a dictionary
|
||||
data = {
|
||||
"reviewText": inputText,
|
||||
"badWordResult": badWordResult,
|
||||
"sentimentResult": data,
|
||||
}
|
||||
|
||||
# Create a CloudEvent object
|
||||
event = CloudEvent(attributes, data)
|
||||
|
||||
return event
|
||||
|
||||
|
||||
def analyze_sentiment(text):
|
||||
analysis = TextBlob(text["reviewText"])
|
||||
sentiment = "neutral"
|
||||
if analysis.sentiment.polarity > 0:
|
||||
sentiment = "positive"
|
||||
elif analysis.sentiment.polarity < 0:
|
||||
sentiment = "negative"
|
||||
|
||||
badWordResult = ""
|
||||
try:
|
||||
badWordResult = text["badWordResult"]
|
||||
except:
|
||||
pass
|
||||
# Convert the sentiment into a CloudEvent
|
||||
sentiment = create_cloud_event(text["reviewText"], badWordResult, sentiment)
|
||||
|
||||
return sentiment
|
||||
|
||||
|
||||
def main(context: Context):
|
||||
"""
|
||||
Function template
|
||||
The context parameter contains the Flask request object and any
|
||||
CloudEvent received with the request.
|
||||
"""
|
||||
|
||||
print("Sentiment Analysis Received CloudEvent: ", context.cloud_event)
|
||||
|
||||
# Add your business logic here
|
||||
return analyze_sentiment(context.cloud_event.data)
|
|
@ -0,0 +1,11 @@
|
|||
specVersion: 0.36.0
|
||||
name: sentiment-analysis-app
|
||||
runtime: python
|
||||
registry: quay.io/rh-ee-leoli
|
||||
image: quay.io/rh-ee-leoli/sentiment-analysis-app:latest
|
||||
imageDigest: sha256:3565ee0591557eca83f19bafbd5a614c66ff5d863d2521ad7d176c76e8294c93
|
||||
created: 2024-02-01T00:18:00.06485162-04:00
|
||||
build:
|
||||
builder: s2i
|
||||
deploy:
|
||||
namespace: default
|
|
@ -2,9 +2,12 @@ apiVersion: v1
|
|||
kind: Service
|
||||
metadata:
|
||||
name: postgresql
|
||||
labels:
|
||||
app: postgresql
|
||||
spec:
|
||||
ports:
|
||||
- port: 5432
|
||||
selector:
|
||||
app: postgresql
|
||||
type: NodePort
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
||||
clusterIP: None
|
|
@ -1,108 +1,108 @@
|
|||
# Database Service for Bookstore
|
||||
To successfully launch the Bookstore sample application, it's essential to set up a dedicated database populated with specific sample data. This guide provides both the schema for the database and the initial data you'll need to get started.
|
||||
|
||||
In this tutorial, we'll embark on creating a PostgreSQL database using Kubernetes (K8s) StatefulSets and populating it with the sample data provided.
|
||||
|
||||
You might wonder, "Why not leverage Knative Serving to dynamically scale the database service in response to traffic demands?" We'll delve into the optimal scenarios for employing Knative Serving and when it's advantageous for our database service.
|
||||
|
||||
## What does the final deliverable look like?
|
||||
Our goal is to deploy a PostgreSQL pod within Kubernetes, loaded with the sample data outlined in the accompanying SQL file. This pod will serve as the foundational database service for our bookstore application.
|
||||
|
||||
## Overview
|
||||
### The Database Schema
|
||||
The BookReviews table contains all reviews made on the bookstore website.
|
||||
|
||||
See the columns of the BookReviews table below:
|
||||
* `ID (serial)` - Primary Key
|
||||
* `post_time (datetime)` - Posting time of the comment
|
||||
* `content (text)` - The contents of the comment
|
||||
* `sentiment (text)` - The sentiment results (currently, the values it could take on are 'positive' or 'neutral' or 'negative')
|
||||
|
||||
### The Sample Data
|
||||
The sample rows inserted for the BookReviews table are shown below:
|
||||
| id | post_time | content | sentiment |
|
||||
|----|---------------------|------------------------------|-----------|
|
||||
| 1 | 2020-01-01 00:00:00 | This book is great! | positive |
|
||||
| 2 | 2020-01-02 00:02:00 | This book is terrible! | negative |
|
||||
| 3 | 2020-01-03 00:01:30 | This book is okay. | neutral |
|
||||
| 4 | 2020-01-04 00:00:00 | Meh | neutral |
|
||||
|
||||
## Implementation
|
||||
|
||||
### Step 1: Acquire Necessary Files from the Repository
|
||||
The essential files for setting up your database are located within the `db` directory of our repository. Please download these files to proceed.
|
||||
|
||||
### Step 2: Deploying the PostgreSQL Database
|
||||
To deploy the PostgreSQL database and populate it with the provided sample data, you'll apply a series of Kubernetes deployment files. Ensure you're positioned in the `code-sample` directory and not within the `db` subdirectory for this operation.
|
||||
|
||||
Within this directory, you will find 6 YAML files, each serving a distinct purpose in the setup process:
|
||||
- `100-create-configmap.yaml`: Generates a ConfigMap including the SQL file for database initialization.
|
||||
- `100-create-secret.yaml`: Produces a Secret holding the PostgreSQL database password.
|
||||
- `100-create-volume.yaml`: Creates both a PersistentVolume and a PersistentVolumeClaim for database storage.
|
||||
- `200-create-postgre.yaml`: Establishes the StatefulSet for the PostgreSQL database.
|
||||
- `300-expose-service.yaml`: Launches a Service to expose the PostgreSQL database externally.
|
||||
- `400-create-job.yaml`: Executes a Job that populates the database with the sample data.
|
||||
|
||||
Execute the command below to apply all configuration files located in the `db` directory:
|
||||
```bash
|
||||
kubectl apply -f db
|
||||
```
|
||||
The filenames prefixed with numbers dictate the application order, ensuring Kubernetes orchestrates the resource setup accordingly.
|
||||
|
||||
### Step 3: Confirming the Deployment
|
||||
Following the application of the deployment files, initialization of the database may require some time. Monitor the deployment's progress by executing:
|
||||
```bash
|
||||
kubectl get pods
|
||||
|
||||
```
|
||||
A successful deployment is indicated by the `Running` state of the `postgresql-0` pod, as shown below:
|
||||
```bash
|
||||
NAMESPACE NAME READY STATUS RESTARTS AGE
|
||||
default postgresql-0 1/1 Running 0 1m
|
||||
```
|
||||
Upon observing the pod in a `Running` state, access the pod using the command:
|
||||
```bash
|
||||
kubectl exec -it postgresql-0 -- /bin/bash
|
||||
|
||||
```
|
||||
Inside the pod, connect to the database with:
|
||||
```bash
|
||||
psql -U myuser -d mydatabase
|
||||
```
|
||||
A successful connection will present you with:
|
||||
```bash
|
||||
mydatabase=#
|
||||
```
|
||||
To verify the initialization of the `BookReviews` table, execute:
|
||||
```
|
||||
mydatabase=# \dt
|
||||
```
|
||||
If the output lists the `BookReviews` table as follows, your database has been correctly initialized:
|
||||
```bash
|
||||
List of relations
|
||||
Schema | Name | Type | Owner
|
||||
--------+--------------+-------+--------
|
||||
public | book_reviews | table | myuser
|
||||
(1 row)
|
||||
```
|
||||
|
||||
## Question & Discussion
|
||||
1. Why did we choose to deploy our PostgreSQL database using a StatefulSet instead of a Knative Service?
|
||||
|
||||
We use [StatefulSet](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/) for databases instead of Knative Service mainly because databases need to remember data (like a notebook that keeps your notes). StatefulSets are good at remembering things because they can save data and have a special name and place where they live. This is very important for databases.
|
||||
|
||||
Knative Services are more like notebooks that you use and then throw away when you're done. They're great for tasks that don't need to keep data for a long time. You can make them go away when you don't need them and come back when you do. But databases need to always remember information, so they can't just disappear and come back.
|
||||
|
||||
Also, databases often talk in their own special language, not the usual web language (HTTP) that Knative Services are really good at understanding. Because of this, Knative Services aren't the best choice for databases. That's why we choose StatefulSet for databases in Kubernetes.
|
||||
|
||||
---
|
||||
Note box: However, Knative Service supports Volumes and Persistent Volumes, which can be used to store data. You can read more [here](https://knative.dev/docs/serving/services/storage/) about how to use Volumes and Persistent Volumes with Knative Services specially for your use case.
|
||||
|
||||
---
|
||||
|
||||
2. When should I use Knative Service, and what would be the best use case for it?
|
||||
|
||||
You can read more about the best use cases for Knative Service [here](https://knative.dev/docs/serving/samples/)!
|
||||
|
||||
## Conclusion
|
||||
# Database Service for Bookstore
|
||||
To successfully launch the Bookstore sample application, it's essential to set up a dedicated database populated with specific sample data. This guide provides both the schema for the database and the initial data you'll need to get started.
|
||||
|
||||
In this tutorial, we'll embark on creating a PostgreSQL database using Kubernetes (K8s) StatefulSets and populating it with the sample data provided.
|
||||
|
||||
You might wonder, "Why not leverage Knative Serving to dynamically scale the database service in response to traffic demands?" We'll delve into the optimal scenarios for employing Knative Serving and when it's advantageous for our database service.
|
||||
|
||||
## What does the final deliverable look like?
|
||||
Our goal is to deploy a PostgreSQL pod within Kubernetes, loaded with the sample data outlined in the accompanying SQL file. This pod will serve as the foundational database service for our bookstore application.
|
||||
|
||||
## Overview
|
||||
### The Database Schema
|
||||
The BookReviews table contains all reviews made on the bookstore website.
|
||||
|
||||
See the columns of the BookReviews table below:
|
||||
* `ID (serial)` - Primary Key
|
||||
* `post_time (datetime)` - Posting time of the comment
|
||||
* `content (text)` - The contents of the comment
|
||||
* `sentiment (text)` - The sentiment results (currently, the values it could take on are 'positive' or 'neutral' or 'negative')
|
||||
|
||||
### The Sample Data
|
||||
The sample rows inserted for the BookReviews table are shown below:
|
||||
| id | post_time | content | sentiment |
|
||||
|----|---------------------|------------------------------|-----------|
|
||||
| 1 | 2020-01-01 00:00:00 | This book is great! | positive |
|
||||
| 2 | 2020-01-02 00:02:00 | This book is terrible! | negative |
|
||||
| 3 | 2020-01-03 00:01:30 | This book is okay. | neutral |
|
||||
| 4 | 2020-01-04 00:00:00 | Meh | neutral |
|
||||
|
||||
## Implementation
|
||||
|
||||
### Step 1: Acquire Necessary Files from the Repository
|
||||
The essential files for setting up your database are located within the `db` directory of our repository. Please download these files to proceed.
|
||||
|
||||
### Step 2: Deploying the PostgreSQL Database
|
||||
To deploy the PostgreSQL database and populate it with the provided sample data, you'll apply a series of Kubernetes deployment files. Ensure you're positioned in the `code-sample` directory and not within the `db` subdirectory for this operation.
|
||||
|
||||
Within this directory, you will find 6 YAML files, each serving a distinct purpose in the setup process:
|
||||
- `100-create-configmap.yaml`: Generates a ConfigMap including the SQL file for database initialization.
|
||||
- `100-create-secret.yaml`: Produces a Secret holding the PostgreSQL database password.
|
||||
- `100-create-volume.yaml`: Creates both a PersistentVolume and a PersistentVolumeClaim for database storage.
|
||||
- `200-create-postgre.yaml`: Establishes the StatefulSet for the PostgreSQL database.
|
||||
- `300-expose-service.yaml`: Launches a Service to expose the PostgreSQL database externally.
|
||||
- `400-create-job.yaml`: Executes a Job that populates the database with the sample data.
|
||||
|
||||
Execute the command below to apply all configuration files located in the `db` directory:
|
||||
```bash
|
||||
kubectl apply -f db
|
||||
```
|
||||
The filenames prefixed with numbers dictate the application order, ensuring Kubernetes orchestrates the resource setup accordingly.
|
||||
|
||||
### Step 3: Confirming the Deployment
|
||||
Following the application of the deployment files, initialization of the database may require some time. Monitor the deployment's progress by executing:
|
||||
```bash
|
||||
kubectl get pods
|
||||
|
||||
```
|
||||
A successful deployment is indicated by the `Running` state of the `postgresql-0` pod, as shown below:
|
||||
```bash
|
||||
NAMESPACE NAME READY STATUS RESTARTS AGE
|
||||
default postgresql-0 1/1 Running 0 1m
|
||||
```
|
||||
Upon observing the pod in a `Running` state, access the pod using the command:
|
||||
```bash
|
||||
kubectl exec -it postgresql-0 -- /bin/bash
|
||||
|
||||
```
|
||||
Inside the pod, connect to the database with:
|
||||
```bash
|
||||
psql -U myuser -d mydatabase
|
||||
```
|
||||
A successful connection will present you with:
|
||||
```bash
|
||||
mydatabase=#
|
||||
```
|
||||
To verify the initialization of the `BookReviews` table, execute:
|
||||
```
|
||||
mydatabase=# \dt
|
||||
```
|
||||
If the output lists the `BookReviews` table as follows, your database has been correctly initialized:
|
||||
```bash
|
||||
List of relations
|
||||
Schema | Name | Type | Owner
|
||||
--------+--------------+-------+--------
|
||||
public | book_reviews | table | myuser
|
||||
(1 row)
|
||||
```
|
||||
|
||||
## Question & Discussion
|
||||
1. Why did we choose to deploy our PostgreSQL database using a StatefulSet instead of a Knative Service?
|
||||
|
||||
We use [StatefulSet](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/) for databases instead of Knative Service mainly because databases need to remember data (like a notebook that keeps your notes). StatefulSets are good at remembering things because they can save data and have a special name and place where they live. This is very important for databases.
|
||||
|
||||
Knative Services are more like notebooks that you use and then throw away when you're done. They're great for tasks that don't need to keep data for a long time. You can make them go away when you don't need them and come back when you do. But databases need to always remember information, so they can't just disappear and come back.
|
||||
|
||||
Also, databases often talk in their own special language, not the usual web language (HTTP) that Knative Services are really good at understanding. Because of this, Knative Services aren't the best choice for databases. That's why we choose StatefulSet for databases in Kubernetes.
|
||||
|
||||
---
|
||||
Note box: However, Knative Service supports Volumes and Persistent Volumes, which can be used to store data. You can read more [here](https://knative.dev/docs/serving/services/storage/) about how to use Volumes and Persistent Volumes with Knative Services specially for your use case.
|
||||
|
||||
---
|
||||
|
||||
2. When should I use Knative Service, and what would be the best use case for it?
|
||||
|
||||
You can read more about the best use cases for Knative Service [here](https://knative.dev/docs/serving/samples/)!
|
||||
|
||||
## Conclusion
|
||||
By following this guide, you have successfully deployed a PostgreSQL server on a Kubernetes cluster, set up persistent storage, and initialized your database using a Kubernetes job. Congratulations! Your bookstore now has the database service.
|
|
@ -20,4 +20,4 @@ RUN npm run build
|
|||
EXPOSE 3000
|
||||
|
||||
# Define the command to run your app
|
||||
CMD ["npm", "run dev"]
|
||||
CMD ["npm", "run", "start"]
|
|
@ -8,7 +8,7 @@ export const metadata: Metadata = {
|
|||
title: 'Knative Bookstore',
|
||||
description: 'Bookstore Sample Application from Knative',
|
||||
icons: {
|
||||
icon: '/images/knative-logo.png',
|
||||
icon: '/images/favicon.ico',
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import Emoji from "./Emoji";
|
||||
|
||||
const CommentDisplay = ({ comment }) => {
|
||||
// Assume receiving a comment object
|
||||
let emoji;
|
||||
if (comment.emotion === "positive") {
|
||||
emoji = "😃";
|
||||
} else if (comment.emotion === "neutral") {
|
||||
emoji = "😐";
|
||||
} else {
|
||||
emoji = "😡";
|
||||
}
|
||||
return (
|
||||
<div className="flex my-4 p-4 justify-center align-middle items-center">
|
||||
<div className="comment-display w-full w-7/12 flex flex-row rounded-lg p-4 bg-gray-800 text-white dark:bg-white dark:text-black">
|
||||
<div className="flex items-center justify-center md:w-1/12">
|
||||
<img
|
||||
src={comment.avatar}
|
||||
alt="Avatar"
|
||||
className="rounded-full w-8 h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:w-3/12 flex items-center content-center text-gray-200 dark:text-black">
|
||||
{comment.time}
|
||||
</div>
|
||||
<div className="md:w-9/12 ">
|
||||
<span className="h-full flex items-center content-center">
|
||||
{comment.text}
|
||||
</span>
|
||||
</div>
|
||||
<div className="md:w-1/12 text-4xl flex">
|
||||
<Emoji symbol={emoji} label={comment.emotion} size="text-2xl" />
|
||||
</div>
|
||||
<div className="md:w-1/12 text-l flex ">
|
||||
<button
|
||||
type="button"
|
||||
className="text-white-700 border border-white-700 hover:bg-white-700 hover:text-white focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-full text-sm p-2.5 text-center inline-flex items-center dark:border-blue-800 dark:text-blue-800 dark:hover:text-white dark:focus:ring-blue-800 dark:hover:bg-white"
|
||||
disabled={true}
|
||||
>
|
||||
<svg
|
||||
className="w-6 h-6 text-white-800 dark:text-black"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 7h14m-9 3v8m4-8v8M10 3h4a1 1 0 0 1 1 1v3H9V4a1 1 0 0 1 1-1ZM6 7h12v13a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentDisplay;
|
|
@ -0,0 +1,268 @@
|
|||
"use-client";
|
||||
import {useState} from "react";
|
||||
|
||||
const GreenCheckMark = () => {
|
||||
return (
|
||||
<svg
|
||||
className="w-4 h-4 me-2 text-green-500 dark:text-green-400 flex-shrink-0"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const KuackImage = () => {
|
||||
return (
|
||||
<img
|
||||
src="https://i.ibb.co/hD1gG7q/Knative-Bookstore-3.png"
|
||||
alt="Descriptive Alt Text"
|
||||
className="w-24 mr-8 mt-4 mb-4"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const GreyLoadingSpin = () => {
|
||||
return (
|
||||
<div role="status">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="w-4 h-4 me-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600"
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||
fill="currentFill"
|
||||
/>
|
||||
</svg>
|
||||
<span className="sr-only">Loading...</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const RedXMark = () => {
|
||||
return (
|
||||
<svg
|
||||
className="w-4 h-4 me-2 text-red-500 dark:text-red-400 flex-shrink-0"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm2.828 12.828a1 1 0 0 1-1.414 0L10 11.414l-1.414 1.414a1 1 0 0 1-1.414-1.414L8.586 10 7.172 8.586a1 1 0 1 1 1.414-1.414L10 8.586l1.414-1.414a1 1 0 0 1 1.414 1.414L11.414 10l1.414 1.414a1 1 0 0 1 0 1.414Z"/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const GreyCheckMark = () => {
|
||||
return (
|
||||
<svg
|
||||
className="w-4 h-4 me-2 text-grey-500 dark:text-grey-400 flex-shrink-0"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
const StatusProgress = ({
|
||||
comment,
|
||||
loadingState,
|
||||
everSubmit,
|
||||
responseSuccess,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="flex justify-between items-center p-6 bg-white border border-gray-100 rounded-lg shadow-md dark:bg-gray-800 dark:border-gray-800 dark:hover:bg-gray-700">
|
||||
<div className="flex-1">
|
||||
<h2 className="mb-2 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
The process behind your Event Driven Architecture:
|
||||
</h2>
|
||||
{everSubmit ? (
|
||||
<ul className="space-y-2 text-gray-500 list-inside dark:text-gray-400">
|
||||
<li className="flex items-center text-black dark:text-white">
|
||||
<div className="font-bold p-1">The comment you submitted:</div>
|
||||
{comment}
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<GreenCheckMark/>
|
||||
Your comment has been packed as a CloudEvent
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<GreenCheckMark/>
|
||||
The CloudEvent has been sent to Nodejs Server as a POST request
|
||||
via HTTP
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
<GreenCheckMark/>
|
||||
Nodejs Server may forwarded the event to Broker
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
{responseSuccess === "unknown" && <GreyLoadingSpin/>}
|
||||
{responseSuccess === "error" && <RedXMark/>}
|
||||
{responseSuccess === "success" && <GreenCheckMark/>}
|
||||
{responseSuccess === "error"
|
||||
? "The CloudEvent has been dispatched by Broker, but got error response."
|
||||
: "The CloudEvent has been dispatched by Broker, waiting for an acknowledgement."}
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
{responseSuccess === "success" ? (
|
||||
<GreenCheckMark/>
|
||||
) : (
|
||||
<GreyCheckMark/>
|
||||
)}
|
||||
Acknowledgement received, the cycle has been completed!
|
||||
</li>
|
||||
<li className="flex items-center">
|
||||
{responseSuccess === "success" ? (
|
||||
<GreenCheckMark/>
|
||||
) : (
|
||||
<GreyCheckMark/>
|
||||
)}
|
||||
Wait a few seconds until the system finishes processing the
|
||||
comment
|
||||
</li>
|
||||
</ul>
|
||||
) : (
|
||||
<h1>Try submitting something first!</h1>
|
||||
)}
|
||||
</div>
|
||||
<KuackImage/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CommentForm = () => {
|
||||
const [hover, setHover] = useState(false);
|
||||
const [comment, setComment] = useState("");
|
||||
const [loadingState, setLoadingState] = useState(false);
|
||||
const [responseSuccess, setResponseSuccess] = useState("unknown");
|
||||
const [everSubmit, setEverSubmit] = useState(false);
|
||||
|
||||
const handleInputChange = (event) => {
|
||||
setComment(event.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
setLoadingState(true);
|
||||
setEverSubmit(true);
|
||||
setResponseSuccess("unknown");
|
||||
|
||||
fetch("http://localhost:8080/add", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Ce-Type": "new-review-comment", // Assuming CloudEvents standard
|
||||
"Ce-Specversion": "1.0",
|
||||
"Ce-Source": "commentForm",
|
||||
"Ce-Id": "unique-comment-id",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
reviewText: comment,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
setResponseSuccess("error");
|
||||
setLoadingState(true);
|
||||
throw new Error(`HTTP error! Status: ${response.status}`);
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
console.log("Success:", data);
|
||||
setResponseSuccess("success");
|
||||
setLoadingState(true);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setLoadingState(false);
|
||||
setComment("");
|
||||
setResponseSuccess("unknown");
|
||||
setEverSubmit(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<StatusProgress
|
||||
comment={comment}
|
||||
loadingState={loadingState}
|
||||
everSubmit={everSubmit}
|
||||
responseSuccess={responseSuccess}
|
||||
/>
|
||||
|
||||
<div className="flex my-4 p-4 justify-center ">
|
||||
<form
|
||||
className="w-full w-8/12 flex flex-col items-end "
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<textarea
|
||||
className="form-textarea w-full mb-2 p-2 border border-2 border-black rounded-lg p-4"
|
||||
rows="3"
|
||||
placeholder="Leave your comment here..."
|
||||
value={comment}
|
||||
onChange={handleInputChange}
|
||||
disabled={loadingState}
|
||||
></textarea>
|
||||
|
||||
{loadingState ? null : (
|
||||
<button
|
||||
type="submit"
|
||||
className={`font-bold py-2 px-9 rounded ${
|
||||
hover ? "" : "bg-blue-600"
|
||||
}`}
|
||||
disabled={comment === ""}
|
||||
style={{
|
||||
backgroundColor:
|
||||
comment === "" ? "#c3c6c7" : hover ? "#baeafd" : "#A5D8FF",
|
||||
}}
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
)}
|
||||
{responseSuccess !== "unknown" && (
|
||||
<div className="flex flex-col">
|
||||
<button
|
||||
type="button"
|
||||
onMouseEnter={() => setHover(true)}
|
||||
onMouseLeave={() => setHover(false)}
|
||||
onClick={handleReset}
|
||||
className={`font-bold py-2 px-9 rounded ${
|
||||
hover ? "" : "bg-blue-600"
|
||||
}`}
|
||||
style={{backgroundColor: hover ? "#bfcaff" : "#9aa8ff"}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentForm;
|
|
@ -0,0 +1,60 @@
|
|||
import React, {useEffect, useState} from "react";
|
||||
import CommentDisplay from "./CommentDisplay";
|
||||
|
||||
const CommentList = ({setStatus}) => {
|
||||
const [comments, setComments] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const ws = new WebSocket("ws://localhost:8080/comments");
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const newComments = JSON.parse(event.data);
|
||||
setComments(newComments);
|
||||
};
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("Connected to /comments");
|
||||
setStatus("connected");
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log("Disconnected from /comments");
|
||||
setStatus("connecting");
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error("WebSocket error:", error);
|
||||
setStatus("connecting");
|
||||
};
|
||||
|
||||
return () => {
|
||||
ws.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
{comments.length > 0 ? comments.map((comment, index) => (
|
||||
<CommentDisplay
|
||||
key={index}
|
||||
comment={{
|
||||
avatar: "/images/avatar.jpg", // assuming a static avatar for each comment
|
||||
time: new Date(comment.post_time).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
}),
|
||||
text: comment.content,
|
||||
emotion: comment.sentiment,
|
||||
}}
|
||||
/>
|
||||
)) : <p>No comments available</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentList;
|
|
@ -0,0 +1,89 @@
|
|||
"use client";
|
||||
import Header from "../components/Header";
|
||||
import BookDetail from "../components/BookDetail";
|
||||
import CommentForm from "../components/CommentForm";
|
||||
import CommentList from "../components/CommentList";
|
||||
import { useState } from "react";
|
||||
|
||||
const StatusBadge = ({ status }) => {
|
||||
if (status == "connecting") {
|
||||
return (
|
||||
<span className="inline-flex items-center bg-orange-100 text-orange-800 text-xs font-medium px-2.5 py-0.5 rounded-full dark:bg-orange-900 dark:text-orange-300">
|
||||
<span className="w-2 h-2 me-1 bg-orange-500 rounded-full"></span>
|
||||
Connecting
|
||||
</span>
|
||||
);
|
||||
} else if (status == "connected") {
|
||||
return (
|
||||
<span className="inline-flex items-center bg-green-100 text-green-800 text-xs font-medium px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300">
|
||||
<span className="w-2 h-2 me-1 bg-green-500 rounded-full"></span>
|
||||
Connected to node server
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="inline-flex items-center bg-red-100 text-red-800 text-xs font-medium px-2.5 py-0.5 rounded-full dark:bg-red-900 dark:text-red-300">
|
||||
<span class="w-2 h-2 me-1 bg-red-500 rounded-full"></span>
|
||||
Service Unavailable
|
||||
</span>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const InfoAlert = () => {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center p-4 mb-4 text-sm text-blue-800 border border-blue-300 rounded-lg bg-blue-50 dark:bg-gray-800 dark:text-blue-400 dark:border-blue-800"
|
||||
role="alert"
|
||||
>
|
||||
<svg
|
||||
className="flex-shrink-0 inline w-4 h-4 me-3"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z" />
|
||||
</svg>
|
||||
<span className="sr-only">Info</span>
|
||||
<div>
|
||||
<span className="font-medium">Note</span> Try implementing the comment
|
||||
deletion feature yourself!
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default function Main() {
|
||||
/* Example Book object */
|
||||
const book = {
|
||||
img: "/images/Bookcover.jpg",
|
||||
title: "Building serverless applications on Knative",
|
||||
author: "Evan Anderson",
|
||||
ISBN: "978-1098142070",
|
||||
publisher: "Oreilly & Associates Inc",
|
||||
publishedDate: "December 19, 2023",
|
||||
description:
|
||||
"A Guide to Designing and Writing Serverless Cloud Application",
|
||||
price: "$49",
|
||||
};
|
||||
|
||||
const [status, setStatus] = useState("connecting");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
|
||||
<main className="container mx-auto my-8">
|
||||
<BookDetail book={book} />
|
||||
<div className="max-w-4xl mx-auto">
|
||||
<CommentForm />
|
||||
<p className="text-xl font-semibold mt-6 mb-4 text-gray-800 dark:text-gray-200">
|
||||
Comments <StatusBadge status={status} />
|
||||
</p>
|
||||
<InfoAlert />
|
||||
<CommentList setStatus={setStatus} />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: bookstore-frontend
|
||||
labels:
|
||||
app: bookstore-frontend
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: bookstore-frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: bookstore-frontend
|
||||
spec:
|
||||
containers:
|
||||
- name: bookstore-frontend
|
||||
image: quay.io/rh-ee-leoli/bookstore-frontend:latest
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: bookstore-frontend-svc
|
||||
spec:
|
||||
ports:
|
||||
- port: 3000
|
||||
selector:
|
||||
app: bookstore-frontend
|
||||
type: LoadBalancer
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"name": "bookstore-frontend",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"name": "bookstore-frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.1.3",
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"name": "bookstore-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.4 KiB |
After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,34 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: node-server
|
||||
labels:
|
||||
app: node-server
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: node-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: node-server
|
||||
spec:
|
||||
containers:
|
||||
- name: node-server
|
||||
image: quay.io/rh-ee-leoli/node-server:latest
|
||||
ports:
|
||||
- containerPort: 8000
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: node-server-svc
|
||||
spec:
|
||||
selector:
|
||||
app: node-server
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 8000
|
||||
type: LoadBalancer
|
|
@ -0,0 +1,34 @@
|
|||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: event-display
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: event-display
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: event-display
|
||||
spec:
|
||||
containers:
|
||||
- name: event-display
|
||||
image: gcr.io/knative-releases/knative.dev/eventing-contrib/cmd/event_display
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: event-display
|
||||
spec:
|
||||
selector:
|
||||
app: event-display
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
type: ClusterIP
|
|
@ -0,0 +1,43 @@
|
|||
apiVersion: eventing.knative.dev/v1
|
||||
kind: Broker
|
||||
metadata:
|
||||
name: bookstore-broker
|
||||
|
||||
|
||||
---
|
||||
# This Trigger subscribes to the Broker and filters events based on the type and badwordfilter attribute.
|
||||
# Those comments that contain insults are filtered out by the badwordfilter attribute and they will be redirected to the event-display Service.
|
||||
apiVersion: eventing.knative.dev/v1
|
||||
kind: Trigger
|
||||
metadata:
|
||||
name: temp-trigger
|
||||
spec:
|
||||
broker: bookstore-broker
|
||||
filter:
|
||||
attributes:
|
||||
type: new-review-comment # This is the filter that will be applied to the event, only events with the ce-type new-review-comment will be processed
|
||||
subscriber:
|
||||
ref:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
name: event-display
|
||||
|
||||
|
||||
|
||||
---
|
||||
apiVersion: eventing.knative.dev/v1
|
||||
kind: Trigger
|
||||
metadata:
|
||||
name: db-insert-trigger
|
||||
spec:
|
||||
broker: bookstore-broker
|
||||
filter:
|
||||
attributes: # Trigger will filter events based on BOTH the type and badwordfilter attribute
|
||||
type: moderated-comment # This is the filter that will be applied to the event, only events with the ce-type moderated-comment will be processed
|
||||
badwordfilter: good # This is the filter that will be applied to the event, only events with the ce-extension badwordfilter: good will be processed
|
||||
subscriber:
|
||||
ref:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
name: node-server-svc
|
||||
uri: /insert # This is the path where the event will be sent to the subscriber, see /insert in node-server code: index.js
|
|
@ -1,7 +1,7 @@
|
|||
apiVersion: sources.knative.dev/v1
|
||||
kind: SinkBinding
|
||||
metadata:
|
||||
name: eda-sinkbinding
|
||||
name: node-sinkbinding
|
||||
namespace: default
|
||||
spec:
|
||||
subject:
|
||||
|
@ -9,9 +9,9 @@ spec:
|
|||
kind: Deployment
|
||||
selector:
|
||||
matchLabels:
|
||||
app: eda
|
||||
sink:
|
||||
app: node-server
|
||||
sink: # In this case, the sink is our broker, which is the eventing service that will receive the events
|
||||
ref:
|
||||
apiVersion: eventing.knative.dev/v1
|
||||
kind: Broker
|
||||
name: broker
|
||||
name: bookstore-broker
|
|
@ -0,0 +1,140 @@
|
|||
const cors = require('cors');
|
||||
const express = require('express');
|
||||
const {HTTP, CloudEvent} = require('cloudevents');
|
||||
const {Pool} = require('pg');
|
||||
const expressWs = require('express-ws');
|
||||
|
||||
const app = express();
|
||||
const port = 8000;
|
||||
|
||||
|
||||
// Middleware to parse JSON bodies
|
||||
app.use(express.json());
|
||||
app.use(cors());
|
||||
expressWs(app); // Apply WebSocket functionality to Express
|
||||
|
||||
// Configure the PostgreSQL connection pool
|
||||
const pool = new Pool({
|
||||
host: 'postgresql.default.svc.cluster.local',
|
||||
port: 5432,
|
||||
database: 'mydatabase',
|
||||
user: 'myuser',
|
||||
password: 'mypassword', // no password as per your setup, but included for completeness
|
||||
});
|
||||
|
||||
app.ws('/comments', (ws, req) => {
|
||||
console.log('WebSocket connection established on /comments');
|
||||
|
||||
// Function to send all comments to the connected client
|
||||
const sendComments = async () => {
|
||||
try {
|
||||
const {rows} = await pool.query('SELECT * FROM book_reviews ORDER BY post_time DESC;');
|
||||
const data = JSON.stringify(rows);
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error executing query', err.stack);
|
||||
if (ws.readyState === ws.OPEN) {
|
||||
ws.send(JSON.stringify({error: 'Failed to retrieve comments'}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Optionally, you can trigger this function based on certain conditions
|
||||
// Here, we just send data immediately after connection and on an interval
|
||||
sendComments();
|
||||
const interval = setInterval(sendComments, 1000); // Send comments every 10 seconds
|
||||
|
||||
ws.on('close', () => {
|
||||
console.log('WebSocket connection on /comments closed');
|
||||
clearInterval(interval);
|
||||
});
|
||||
|
||||
ws.on('error', error => {
|
||||
console.error('WebSocket error on /comments:', error);
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/insert', async (req, res) => {
|
||||
try {
|
||||
|
||||
// the fields are post_time, content, sentiment
|
||||
// post_time is generated here, in the format of 2020-01-01 00:00:00
|
||||
const receivedEvent = HTTP.toEvent({headers: req.headers, body: req.body});
|
||||
const reviewText = receivedEvent.data.reviewText;
|
||||
const sentimentResult = receivedEvent.data.sentimentResult;
|
||||
const postTime = new Date().toISOString().replace('T', ' ').replace('Z', '');
|
||||
|
||||
// Insert the review into the database
|
||||
await pool.query('INSERT INTO book_reviews (post_time,content, sentiment) VALUES ($1, $2, $3)', [postTime, reviewText, sentimentResult]);
|
||||
|
||||
// Acknowledge the receipt of the event
|
||||
console.log('Review inserted:', reviewText);
|
||||
const event = new CloudEvent({
|
||||
type: "com.example.reviews.inserted",
|
||||
source: "/api/reviews",
|
||||
data: {
|
||||
success: true,
|
||||
message: "Review inserted successfully"
|
||||
}
|
||||
});
|
||||
// Serialize the event for an HTTP response
|
||||
const serializedEvent = HTTP.binary(event);
|
||||
|
||||
// Set headers and send the CloudEvent
|
||||
res.writeHead(200, serializedEvent.headers);
|
||||
res.end(JSON.stringify(serializedEvent.body));
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing request:', error);
|
||||
return res.status(500).json({error: 'Internal server error'});
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/add', async (req, res) => {
|
||||
try {
|
||||
const receivedEvent = HTTP.toEvent({headers: req.headers, body: req.body});
|
||||
const brokerURI = process.env.K_SINK;
|
||||
|
||||
if (receivedEvent.type === 'new-review-comment') {
|
||||
// Forward the event to the broker with the necessary CloudEvent headers
|
||||
const response = await fetch(brokerURI, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'ce-specversion': '1.0',
|
||||
'ce-type': 'new-review-comment',
|
||||
'ce-source': 'bookstore-eda',
|
||||
'ce-id': receivedEvent.id,
|
||||
},
|
||||
body: JSON.stringify(receivedEvent.data),
|
||||
});
|
||||
|
||||
if (!response.ok) { // If the response status code is not 2xx, consider it a failure
|
||||
console.error('Failed to forward event:', receivedEvent);
|
||||
return res.status(500).json({error: 'Failed to forward event'});
|
||||
}
|
||||
|
||||
// If forwarding was successful, acknowledge the receipt of the event
|
||||
console.log('Event forwarded successfully:', receivedEvent);
|
||||
return res.status(200).json({success: true, message: 'Event forwarded successfully'});
|
||||
} else {
|
||||
// Handle unexpected event types
|
||||
console.warn('Unexpected event type:', receivedEvent.type);
|
||||
return res.status(400).json({error: 'Unexpected event type'});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing request:', error);
|
||||
return res.status(500).json({error: 'Internal server error'});
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.send('Hello, world!');
|
||||
});
|
||||
|
||||
// Start the server
|
||||
app.listen(port, () => {
|
||||
console.log(`Server listening at http://localhost:${port}`);
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "bookstore-eda",
|
||||
"name": "node-server",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
|
@ -10,7 +10,10 @@
|
|||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cloudevents": "^8.0.0",
|
||||
"express": "^4.19.2"
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2",
|
||||
"express-ws": "^5.0.2",
|
||||
"pg": "^8.11.5"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
|
@ -180,6 +183,18 @@
|
|||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.5",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
|
||||
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
|
@ -307,6 +322,20 @@
|
|||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-ws": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/express-ws/-/express-ws-5.0.2.tgz",
|
||||
"integrity": "sha512-0uvmuk61O9HXgLhGl3QhNSEtRsQevtmbL94/eILaliEADZBHZOQUAiHFrGPrgsjikohyrmSG5g+sCfASTt0lkQ==",
|
||||
"dependencies": {
|
||||
"ws": "^7.4.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": "^4.0.0 || ^5.0.0-alpha.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
|
@ -618,6 +647,14 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
|
||||
|
@ -650,6 +687,87 @@
|
|||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.11.5",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.5.tgz",
|
||||
"integrity": "sha512-jqgNHSKL5cbDjFlHyYsCXmQDrfIX/3RsNwYqpd4N0Kt8niLuNoRNH+aazv6cOd43gPh9Y4DjQCtb+X0MH0Hvnw==",
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.6.4",
|
||||
"pg-pool": "^3.6.2",
|
||||
"pg-protocol": "^1.6.1",
|
||||
"pg-types": "^2.1.0",
|
||||
"pgpass": "1.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"pg-cloudflare": "^1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pg-cloudflare": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
|
||||
"integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz",
|
||||
"integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA=="
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz",
|
||||
"integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz",
|
||||
"integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg=="
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"postgres-array": "~2.0.0",
|
||||
"postgres-bytea": "~1.0.0",
|
||||
"postgres-date": "~1.0.4",
|
||||
"postgres-interval": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pgpass": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
|
||||
|
@ -658,6 +776,41 @@
|
|||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
|
||||
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
|
@ -834,6 +987,14 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
|
@ -931,6 +1092,34 @@
|
|||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.5.9",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
|
||||
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "bookstore-eda",
|
||||
"name": "node-server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
|
@ -10,6 +10,9 @@
|
|||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cloudevents": "^8.0.0",
|
||||
"express": "^4.19.2"
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2",
|
||||
"express-ws": "^5.0.2",
|
||||
"pg": "^8.11.5"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 165 KiB |
Before Width: | Height: | Size: 488 KiB After Width: | Height: | Size: 488 KiB |
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 222 KiB |
Before Width: | Height: | Size: 187 KiB After Width: | Height: | Size: 187 KiB |
|
@ -0,0 +1,22 @@
|
|||
apiVersion: flows.knative.dev/v1
|
||||
kind: Sequence
|
||||
metadata:
|
||||
name: sequence
|
||||
spec:
|
||||
channelTemplate: # Under the hood, the Sequence will create a Channel for each step in the sequence
|
||||
apiVersion: messaging.knative.dev/v1
|
||||
kind: InMemoryChannel
|
||||
steps:
|
||||
- ref: # This is the first step of the sequence, it will send the event to the bad-word-filter service
|
||||
apiVersion: serving.knative.dev/v1
|
||||
kind: Service
|
||||
name: bad-word-filter
|
||||
- ref: # This is the second step of the sequence, it will send the event to the sentiment-analysis-app service
|
||||
apiVersion: serving.knative.dev/v1
|
||||
kind: Service
|
||||
name: sentiment-analysis-app
|
||||
reply: # This is the last step of the sequence, it will send the event back to the broker as reply
|
||||
ref:
|
||||
kind: Broker
|
||||
apiVersion: eventing.knative.dev/v1
|
||||
name: bookstore-broker
|
|
@ -0,0 +1,33 @@
|
|||
apiVersion: eventing.knative.dev/v1
|
||||
kind: Trigger
|
||||
metadata:
|
||||
name: sequence-trigger
|
||||
spec:
|
||||
broker: bookstore-broker
|
||||
filter:
|
||||
attributes:
|
||||
type: new-review-comment # This is the filter that will be applied to the event, only events with the ce-type new-review-comment will be processed
|
||||
subscriber:
|
||||
ref:
|
||||
apiVersion: flows.knative.dev/v1
|
||||
kind: Sequence
|
||||
name: sequence
|
||||
|
||||
---
|
||||
# This Trigger subscribes to the Broker and filters events based on the type and badwordfilter attribute.
|
||||
# Those comments that contain insults are filtered out by the badwordfilter attribute and they will be redirected to the event-display Service.
|
||||
apiVersion: eventing.knative.dev/v1
|
||||
kind: Trigger
|
||||
metadata:
|
||||
name: seq-reply-trigger
|
||||
spec:
|
||||
broker: bookstore-broker
|
||||
filter:
|
||||
attributes:
|
||||
type: moderated-comment # This is the filter that will be applied to the event, only events with the ce-type moderated-comment will be processed
|
||||
badwordfilter: bad # This is the filter that will be applied to the event, only events with the ce-extension badwordfilter: bad will be processed
|
||||
subscriber:
|
||||
ref:
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
name: event-display
|
|
@ -0,0 +1,104 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Prompt the user to start the installation process
|
||||
echo "This script will install Knative Serving, Knative Eventing, and install the sample app bookstore on your cluster"
|
||||
read -p "Press ENTER to continue or Ctrl+C to abort..."
|
||||
|
||||
# Install Knative Serving
|
||||
echo "Installing Knative Serving..."
|
||||
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.14.0/serving-crds.yaml
|
||||
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.14.0/serving-core.yaml
|
||||
kubectl apply -f https://github.com/knative/net-kourier/releases/download/knative-v1.14.0/kourier.yaml
|
||||
|
||||
# Configure Kourier as the default ingress
|
||||
kubectl patch configmap/config-network --namespace knative-serving --type merge --patch '{"data":{"ingress-class":"kourier.ingress.networking.knative.dev"}}'
|
||||
|
||||
echo "Knative Serving installed successfully."
|
||||
|
||||
# Install Knative Eventing
|
||||
echo "Installing Knative Eventing..."
|
||||
kubectl apply -f https://github.com/knative/eventing/releases/download/knative-v1.14.0/eventing-crds.yaml
|
||||
kubectl apply -f https://github.com/knative/eventing/releases/download/knative-v1.14.0/eventing-core.yaml
|
||||
echo "Knative Eventing installed successfully."
|
||||
|
||||
# Install Knative imc broker
|
||||
kubectl apply -f https://github.com/knative/eventing/releases/download/knative-v1.14.0/in-memory-channel.yaml
|
||||
kubectl apply -f https://github.com/knative/eventing/releases/download/knative-v1.14.0/mt-channel-broker.yaml
|
||||
echo "Knative in-memory channel and broker installed successfully."
|
||||
|
||||
# Detect whether the user has knative function "func" installed
|
||||
if ! command -v func &> /dev/null
|
||||
then
|
||||
echo "Knative CLI 'func' not found. Please install the Knative CLI by following the instructions at https://knative.dev/docs/admin/install/kn-cli/."
|
||||
exit
|
||||
fi
|
||||
|
||||
# Detect whether the user has kamel CLI installed
|
||||
if ! command -v kamel &> /dev/null
|
||||
then
|
||||
echo "Kamel CLI not found. Please install the Kamel CLI by following the instructions at https://camel.apache.org/camel-k/latest/installation/installation.html."
|
||||
exit
|
||||
fi
|
||||
|
||||
|
||||
|
||||
# Prompt for the Docker registry details
|
||||
echo "Please provide the details of your Container registry to install the Camel-K."
|
||||
read -p "Enter the registry hostname (e.g., docker.io or quay.io): " REGISTRY_HOST
|
||||
read -p "Enter the registry username: " REGISTRY_USER
|
||||
read -s -p "Enter the registry password: " REGISTRY_PASSWORD
|
||||
echo "All the required details have been captured and saved locally."
|
||||
|
||||
# Set the registry details as environment variables
|
||||
export REGISTRY_HOST=$REGISTRY_HOST
|
||||
export REGISTRY_USER=$REGISTRY_USER
|
||||
export REGISTRY_PASSWORD=$REGISTRY_PASSWORD
|
||||
|
||||
# Set the KO_DOCKER_REPO environment variable
|
||||
export KO_DOCKER_REPO=$REGISTRY_HOST/$REGISTRY_USER
|
||||
|
||||
# Install the camel-K
|
||||
kamel install --registry $REGISTRY_HOST--organization $REGISTRY_USER --registry-auth-username $REGISTRY_USER --registry-auth-password $REGISTRY_PASSWORD
|
||||
|
||||
|
||||
# Install the Sample Bookstore App
|
||||
echo "Installing the Sample Bookstore App..., but please follow the instruction below to tell us your registry details"
|
||||
read -p "Press ENTER to continue..."
|
||||
|
||||
# Install the front end first
|
||||
cd frontend
|
||||
kubectl apply -f config
|
||||
|
||||
# Install the node-server
|
||||
cd ../node-server
|
||||
kubectl apply -f config
|
||||
|
||||
# Deploy the ML services
|
||||
cd ../ML-bad-word-filter
|
||||
func deploy -b=s2i -v
|
||||
|
||||
cd ../ML-sentiment-analysis
|
||||
func deploy -b=s2i -v
|
||||
|
||||
# Install the db
|
||||
cd ..
|
||||
kubectl apply -f db-service
|
||||
|
||||
# Install the sequence
|
||||
kubectl apply -f sequence/config
|
||||
|
||||
# Ask the user to edit the properties file
|
||||
echo "Please edit slack-sink/application.properties to provide the webhook URL for Slack."
|
||||
read -p "Press ENTER to continue..."
|
||||
|
||||
# Create the secret
|
||||
kubectl create secret generic slack-credentials --from-file=slack-sink/application.properties
|
||||
|
||||
# Install the slack-sink
|
||||
kubectl apply -f slack-sink/config
|
||||
|
||||
# Ask user to open a new terminal to set the minikube tunnel
|
||||
echo "If you are using minikube: Please open a new terminal and run 'sudo minikube tunnel' to expose the services to the outside world."
|
||||
read -p "Press ENTER to continue..."
|
||||
# FIXME: @pierDipi something wrong here. If run minikube tunnel before bookstore is installed, the localhost:80 will not be accessible
|
||||
# End of script
|
|
@ -0,0 +1,19 @@
|
|||
#
|
||||
# Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
# contributor license agreements. See the NOTICE file distributed with
|
||||
# this work for additional information regarding copyright ownership.
|
||||
# The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
# (the "License"); you may not use this file except in compliance with
|
||||
# the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
slack.channel=#bookstore-owner
|
||||
slack.webhook.url=https://hooks.slack.com/services/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
|
|
@ -1,20 +1,22 @@
|
|||
apiVersion: camel.apache.org/v1 # Specifies the API version of Camel K.
|
||||
kind: Pipe # This resource type is a Pipe, a custom Camel K resource for defining integration flows.
|
||||
metadata:
|
||||
name: bookstore-notification-service # The name of the Pipe, which identifies this particular integration flow.
|
||||
name: pipe # The name of the Pipe, which identifies this particular integration flow.
|
||||
annotations:
|
||||
trait.camel.apache.org/mount.configs: "secret:slack-credentials" # An annotation that specifies the secret to mount for the Pipe.
|
||||
spec:
|
||||
source: # Defines the source of events for the Pipe.
|
||||
ref:
|
||||
kind: Broker # Specifies the kind of source, in this case, a Knative Eventing Broker.
|
||||
apiVersion: eventing.knative.dev/v1 # The API version of the Knative Eventing Broker.
|
||||
name: book-review-broker # The name of the Broker, "book-review-broker" in this case
|
||||
name: bookstore-broker # The name of the Broker, "book-review-broker" in this case
|
||||
properties:
|
||||
type: new-review-comment # A filter that specifies the type of events this Pipe will listen for, here it's listening for events of type "new-review-comment". You have to have this type specified.
|
||||
type: moderated-comment # A filter that specifies the type of events this Pipe will listen for, here it's listening for events of type "bad-comment". You have to have this type specified.
|
||||
sink: # Defines the destination for events processed by this Pipe.
|
||||
ref:
|
||||
kind: Kamelet # Specifies that the sink is a Kamelet, a Camel K component for connecting to external services.
|
||||
apiVersion: camel.apache.org/v1 # The API version for Kamelet.
|
||||
name: slack-sink # The name of the Kamelet to use as the sink, in this case, a predefined "slack-sink" Kamelet.
|
||||
properties:
|
||||
channel: “#bookstore-owner” # The Slack channel where notifications will be sent.
|
||||
webhookUrl: "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK" # The Webhook URL provided by Slack for posting messages to a specific channel.
|
||||
channel: ${slack.channel} # The channel to post messages to, specified in the secret.
|
||||
webhookUrl: ${slack.webhook.url} # The webhook URL to use for posting messages, specified in the secret.
|
|
@ -0,0 +1,20 @@
|
|||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: sql-configmap
|
||||
data:
|
||||
sample.sql: |
|
||||
CREATE DATABASE bookstore;
|
||||
CREATE TABLE IF NOT EXISTS book_reviews(
|
||||
id SERIAL PRIMARY KEY,
|
||||
post_time timestamp NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
sentiment TEXT,
|
||||
CONSTRAINT sentiment_check CHECK (sentiment IN ('positive', 'negative', 'neutral'))
|
||||
);
|
||||
|
||||
INSERT INTO book_reviews (post_time, content, sentiment) VALUES
|
||||
('2020-01-01 00:00:00', 'This book is great!', 'positive'),
|
||||
('2020-01-02 00:02:00', 'This book is terrible!', 'negative'),
|
||||
('2020-01-03 00:01:30', 'This book is okay.', 'neutral'),
|
||||
('2020-01-04 00:00:00', 'Meh', 'neutral');
|
|
@ -0,0 +1,9 @@
|
|||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: postgresql-secret
|
||||
type: Opaque
|
||||
data:
|
||||
POSTGRES_DB: bXlkYXRhYmFzZQ==
|
||||
POSTGRES_USER: bXl1c2Vy
|
||||
POSTGRES_PASSWORD: bXlwYXNzd29yZA==
|
|
@ -0,0 +1,10 @@
|
|||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: postgresql-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
|
@ -0,0 +1,42 @@
|
|||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: postgresql
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: postgresql
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: postgresql
|
||||
spec:
|
||||
containers:
|
||||
- name: postgresql
|
||||
image: quay.io/enterprisedb/postgresql
|
||||
env:
|
||||
- name: POSTGRES_DB
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgresql-secret
|
||||
key: POSTGRES_DB
|
||||
- name: POSTGRES_USER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgresql-secret
|
||||
key: POSTGRES_USER
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgresql-secret
|
||||
key: POSTGRES_PASSWORD
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
volumeMounts:
|
||||
- name: postgresql-storage
|
||||
mountPath: /var/lib/postgresql/data
|
||||
volumes:
|
||||
- name: postgresql-storage
|
||||
persistentVolumeClaim:
|
||||
claimName: postgresql-pvc
|
|
@ -0,0 +1,13 @@
|
|||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: postgresql
|
||||
labels:
|
||||
app: postgresql
|
||||
spec:
|
||||
selector:
|
||||
app: postgresql
|
||||
ports:
|
||||
- port: 5432
|
||||
targetPort: 5432
|
||||
clusterIP: None
|
|
@ -0,0 +1,37 @@
|
|||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: postgresql-job
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 50
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: postgresql-client
|
||||
image: quay.io/enterprisedb/postgresql
|
||||
command: ["psql", "-h", "postgresql", "-U", "myuser", "-d", "mydatabase", "-f", "/sql/sample.sql"]
|
||||
env:
|
||||
- name: PGPASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgresql-secret
|
||||
key: POSTGRES_PASSWORD
|
||||
- name: PGUSER
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgresql-secret
|
||||
key: POSTGRES_USER
|
||||
- name: PGDATABASE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: postgresql-secret
|
||||
key: POSTGRES_DB
|
||||
volumeMounts:
|
||||
- name: sql-volume
|
||||
mountPath: /sql
|
||||
restartPolicy: Never
|
||||
volumes:
|
||||
- name: sql-volume
|
||||
configMap:
|
||||
name: sql-configmap
|
||||
backoffLimit: 5
|
|
@ -0,0 +1,108 @@
|
|||
# Database Service for Bookstore
|
||||
To successfully launch the Bookstore sample application, it's essential to set up a dedicated database populated with specific sample data. This guide provides both the schema for the database and the initial data you'll need to get started.
|
||||
|
||||
In this tutorial, we'll embark on creating a PostgreSQL database using Kubernetes (K8s) StatefulSets and populating it with the sample data provided.
|
||||
|
||||
You might wonder, "Why not leverage Knative Serving to dynamically scale the database service in response to traffic demands?" We'll delve into the optimal scenarios for employing Knative Serving and when it's advantageous for our database service.
|
||||
|
||||
## What does the final deliverable look like?
|
||||
Our goal is to deploy a PostgreSQL pod within Kubernetes, loaded with the sample data outlined in the accompanying SQL file. This pod will serve as the foundational database service for our bookstore application.
|
||||
|
||||
## Overview
|
||||
### The Database Schema
|
||||
The BookReviews table contains all reviews made on the bookstore website.
|
||||
|
||||
See the columns of the BookReviews table below:
|
||||
* `ID (serial)` - Primary Key
|
||||
* `post_time (datetime)` - Posting time of the comment
|
||||
* `content (text)` - The contents of the comment
|
||||
* `sentiment (text)` - The sentiment results (currently, the values it could take on are 'positive' or 'neutral' or 'negative')
|
||||
|
||||
### The Sample Data
|
||||
The sample rows inserted for the BookReviews table are shown below:
|
||||
| id | post_time | content | sentiment |
|
||||
|----|---------------------|------------------------------|-----------|
|
||||
| 1 | 2020-01-01 00:00:00 | This book is great! | positive |
|
||||
| 2 | 2020-01-02 00:02:00 | This book is terrible! | negative |
|
||||
| 3 | 2020-01-03 00:01:30 | This book is okay. | neutral |
|
||||
| 4 | 2020-01-04 00:00:00 | Meh | neutral |
|
||||
|
||||
## Implementation
|
||||
|
||||
### Step 1: Acquire Necessary Files from the Repository
|
||||
The essential files for setting up your database are located within the `db` directory of our repository. Please download these files to proceed.
|
||||
|
||||
### Step 2: Deploying the PostgreSQL Database
|
||||
To deploy the PostgreSQL database and populate it with the provided sample data, you'll apply a series of Kubernetes deployment files. Ensure you're positioned in the `code-sample` directory and not within the `db` subdirectory for this operation.
|
||||
|
||||
Within this directory, you will find 6 YAML files, each serving a distinct purpose in the setup process:
|
||||
- `100-create-configmap.yaml`: Generates a ConfigMap including the SQL file for database initialization.
|
||||
- `100-create-secret.yaml`: Produces a Secret holding the PostgreSQL database password.
|
||||
- `100-create-volume.yaml`: Creates both a PersistentVolume and a PersistentVolumeClaim for database storage.
|
||||
- `200-create-postgre.yaml`: Establishes the StatefulSet for the PostgreSQL database.
|
||||
- `300-expose-service.yaml`: Launches a Service to expose the PostgreSQL database externally.
|
||||
- `400-create-job.yaml`: Executes a Job that populates the database with the sample data.
|
||||
|
||||
Execute the command below to apply all configuration files located in the `db` directory:
|
||||
```bash
|
||||
kubectl apply -f db
|
||||
```
|
||||
The filenames prefixed with numbers dictate the application order, ensuring Kubernetes orchestrates the resource setup accordingly.
|
||||
|
||||
### Step 3: Confirming the Deployment
|
||||
Following the application of the deployment files, initialization of the database may require some time. Monitor the deployment's progress by executing:
|
||||
```bash
|
||||
kubectl get pods
|
||||
|
||||
```
|
||||
A successful deployment is indicated by the `Running` state of the `postgresql-0` pod, as shown below:
|
||||
```bash
|
||||
NAMESPACE NAME READY STATUS RESTARTS AGE
|
||||
default postgresql-0 1/1 Running 0 1m
|
||||
```
|
||||
Upon observing the pod in a `Running` state, access the pod using the command:
|
||||
```bash
|
||||
kubectl exec -it postgresql-0 -- /bin/bash
|
||||
|
||||
```
|
||||
Inside the pod, connect to the database with:
|
||||
```bash
|
||||
psql -U myuser -d mydatabase
|
||||
```
|
||||
A successful connection will present you with:
|
||||
```bash
|
||||
mydatabase=#
|
||||
```
|
||||
To verify the initialization of the `BookReviews` table, execute:
|
||||
```
|
||||
mydatabase=# \dt
|
||||
```
|
||||
If the output lists the `BookReviews` table as follows, your database has been correctly initialized:
|
||||
```bash
|
||||
List of relations
|
||||
Schema | Name | Type | Owner
|
||||
--------+--------------+-------+--------
|
||||
public | book_reviews | table | myuser
|
||||
(1 row)
|
||||
```
|
||||
|
||||
## Question & Discussion
|
||||
1. Why did we choose to deploy our PostgreSQL database using a StatefulSet instead of a Knative Service?
|
||||
|
||||
We use [StatefulSet](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/) for databases instead of Knative Service mainly because databases need to remember data (like a notebook that keeps your notes). StatefulSets are good at remembering things because they can save data and have a special name and place where they live. This is very important for databases.
|
||||
|
||||
Knative Services are more like notebooks that you use and then throw away when you're done. They're great for tasks that don't need to keep data for a long time. You can make them go away when you don't need them and come back when you do. But databases need to always remember information, so they can't just disappear and come back.
|
||||
|
||||
Also, databases often talk in their own special language, not the usual web language (HTTP) that Knative Services are really good at understanding. Because of this, Knative Services aren't the best choice for databases. That's why we choose StatefulSet for databases in Kubernetes.
|
||||
|
||||
---
|
||||
Note box: However, Knative Service supports Volumes and Persistent Volumes, which can be used to store data. You can read more [here](https://knative.dev/docs/serving/services/storage/) about how to use Volumes and Persistent Volumes with Knative Services specially for your use case.
|
||||
|
||||
---
|
||||
|
||||
2. When should I use Knative Service, and what would be the best use case for it?
|
||||
|
||||
You can read more about the best use cases for Knative Service [here](https://knative.dev/docs/serving/samples/)!
|
||||
|
||||
## Conclusion
|
||||
By following this guide, you have successfully deployed a PostgreSQL server on a Kubernetes cluster, set up persistent storage, and initialized your database using a Kubernetes job. Congratulations! Your bookstore now has the database service.
|
|
@ -0,0 +1,14 @@
|
|||
CREATE DATABASE bookstore;
|
||||
CREATE TABLE IF NOT EXISTS book_reviews(
|
||||
id SERIAL PRIMARY KEY,
|
||||
post_time timestamp NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
sentiment TEXT,
|
||||
CONSTRAINT sentiment_check CHECK (sentiment IN ('positive', 'negative', 'neutral'))
|
||||
);
|
||||
|
||||
INSERT INTO book_reviews (post_time, content, sentiment) VALUES
|
||||
('2020-01-01 00:00:00', 'This book is great!', 'positive'),
|
||||
('2020-01-02 00:02:00', 'This book is terrible!', 'negative'),
|
||||
('2020-01-03 00:01:30', 'This book is okay.', 'neutral'),
|
||||
('2020-01-04 00:00:00', 'Meh', 'neutral');
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
|
@ -0,0 +1,23 @@
|
|||
# Use a base image with Node.js LTS
|
||||
FROM node:lts-alpine
|
||||
|
||||
# Set the working directory inside the container
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package.json and package-lock.json to the working directory
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy the rest of your application code to the working directory
|
||||
COPY . .
|
||||
|
||||
# Build the Next.js application
|
||||
RUN npm run build
|
||||
|
||||
# Expose the port your app runs on
|
||||
EXPOSE 3000
|
||||
|
||||
# Define the command to run your app
|
||||
CMD ["npm", "run", "start"]
|
|
@ -0,0 +1,57 @@
|
|||
# Getting Started
|
||||
|
||||
This app use Next.js and TailwindCSS as main packages. Use this command to install all dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
To run application, use:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
# Project Structures
|
||||
|
||||
- app/: Contains the main layout, page, and global styling.
|
||||
- client/: Contains components and pages used in the application.
|
||||
- public/images/: Contains image files.
|
||||
- next-env.d.ts, next.config.mjs, package-lock.json, package.json, postcss.config.js, tailwind.config.js, tsconfig.json: Configuration files for Next.js, Tailwind CSS, and TypeScript.
|
||||
|
||||
# Containerize Application
|
||||
|
||||
This repository contains a Next.js application that utilizes next-themes and Tailwind CSS. This README file provides instructions on how to containerize the application using Docker.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker installed on your machine. You can download and install Docker from [here](https://www.docker.com/get-started).
|
||||
|
||||
## Dockerization Steps
|
||||
|
||||
1. Clone this repository to your local machine.
|
||||
2. Navigate to the root directory of the cloned repository.
|
||||
|
||||
### Building the Docker Image
|
||||
|
||||
Run the following command to build the Docker image:
|
||||
|
||||
```bash
|
||||
docker build -t frontend .
|
||||
```
|
||||
|
||||
## Running the Docker Container
|
||||
|
||||
Once the image is built, you can run a container using the following command:
|
||||
|
||||
```bash
|
||||
docker run -d -p 3000:3000 frontend
|
||||
```
|
|
@ -0,0 +1,22 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap');
|
||||
|
||||
#__next {
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Poppins', sans-serif;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
import './globals.css';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Knative Bookstore',
|
||||
description: 'Bookstore Sample Application from Knative',
|
||||
icons: {
|
||||
icon: '/images/favicon.ico',
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang='en'>
|
||||
<body className={inter.className}>{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
'use client';
|
||||
import Main from '../client/pages/Main';
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<ThemeProvider attribute='class'>
|
||||
<Main />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|