Skip to content

Commit 965ae4a

Browse files
authored
Merge branch 'devel' into doc-play-ocp-aws-disk-bench
2 parents 486c289 + fdc90b4 commit 965ae4a

File tree

272 files changed

+49338
-112
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

272 files changed

+49338
-112
lines changed

.env

Lines changed: 0 additions & 1 deletion
This file was deleted.

.github/workflows/linters.yaml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: linters
2+
on: [push]
3+
jobs:
4+
5+
# https://github.com/marketplace/actions/shell-linter
6+
ShellScript:
7+
name: ShellScript
8+
runs-on: ubuntu-latest
9+
steps:
10+
- name: Checkout code
11+
uses: actions/checkout@v3
12+
with:
13+
fetch-depth: 0 # Fetch all history for all branches and tags
14+
15+
- name: Install shellcheck
16+
run: sudo apt-get install shellcheck
17+
18+
- name: Find changed shell scripts
19+
id: files
20+
run: |
21+
echo "::set-output name=shell_files::$(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep '\.sh$')"
22+
23+
- name: Run shellcheck
24+
if: steps.files.outputs.shell_files != ''
25+
run: |
26+
for file in ${{ steps.files.outputs.shell_files }}
27+
do
28+
shellcheck "$file"
29+
done
30+
31+
# https://github.com/marketplace/actions/yaml-lint
32+
# linter-yaml:
33+
# runs-on: ubuntu-latest
34+
# steps:
35+
# - uses: actions/checkout@v2
36+
# - name: yaml-lint-config
37+
# uses: ibiqlik/action-yamllint@v3
38+
# with:
39+
# file_or_dir: config.yaml
40+
# config_file: .github/workflows/yaml-linter-config.yml
41+
# - name: yaml-lint-ci
42+
# uses: ibiqlik/action-yamllint@v3
43+
# with:
44+
# file_or_dir: .github/workflows/*.yaml
45+
# config_file: .github/workflows/yaml-linter-config.yml
46+
47+
# https://github.com/marketplace/actions/cfn-lint-action
48+
CloudFormation:
49+
runs-on: ubuntu-latest
50+
steps:
51+
- name: Checkout code
52+
uses: actions/checkout@v3
53+
54+
- name: Setup Cloud Formation Linter with Latest Version
55+
uses: scottbrenner/cfn-lint-action@v2
56+
57+
- name: Run
58+
run: |
59+
cfn-lint --version
60+
prefix_templates=labs/ocp-install-iac/aws-cloudformation-templates/
61+
62+
echo "Checking CloudFormation templates for changes..."
63+
echo "${FILES_CHANGED_YAML}"
64+
for file in $(git diff --name-only ${{ github.event.before }} ${{ github.sha }} | grep '\.yaml$')
65+
do
66+
if [[ $file == $prefix_templates* ]]; then
67+
echo "Running CloudFormation link to file ${TPL}"
68+
cfn-lint -t "${TPL}"
69+
fi
70+
done
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
config_data: |
2+
extends: default
3+
rules:
4+
new-line-at-end-of-file:
5+
level: warning
6+
trailing-spaces:
7+
level: warning

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
.venv
2+
.env
3+
.vscode/
4+
!.vscode
5+
!.vscode/mcp.json
6+
tmp/

.vscode/mcp.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"servers": {
3+
"cep": {
4+
"path": "labs/mcp-servers/cep",
5+
"description": "A simple HTTP server that serves CEP from a directory",
6+
"command": "$HOME/go/src/github.com/mtulio/mtulio.labs-devel/labs/mcp-servers/cep/cep"
7+
},
8+
"aws-news-service": {
9+
"description": "A simple tool that fetches latest AWS annoucements.",
10+
"command": "$HOME/go/src/github.com/mtulio/mtulio.labs-devel/labs/mcp-servers/cloud-news/aws-client"
11+
},
12+
"remote-aws-news-service": {
13+
"url": "https://labs-git-api-mcp-news-aws-marco-bragas-projects.vercel.app/api/news/aws"
14+
},
15+
"local-aws-news-service": {
16+
"url": "http://localhost:8080/api/news/aws"
17+
}
18+
}
19+
}

api/news/aws.go

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
package handler
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"strings"
9+
)
10+
11+
type NewsItem struct {
12+
ID string `json:"id"`
13+
Date string `json:"date"`
14+
Headline string `json:"headline"`
15+
Body string `json:"body"`
16+
Category string `json:"category"`
17+
Products []string `json:"products"`
18+
}
19+
20+
func fetchNews(category string) ([]NewsItem, error) {
21+
// Define the API endpoint
22+
url := "https://aws.amazon.com/api/dirs/items/search"
23+
24+
// Define the query parameters
25+
params := map[string]string{
26+
"item.directoryId": "whats-new-v2",
27+
"sort_by": "item.additionalFields.postDateTime",
28+
"sort_order": "desc",
29+
"size": "50",
30+
"item.locale": "en_US",
31+
}
32+
33+
// Optional: Add category filter
34+
if category != "" {
35+
params["tags.id"] = fmt.Sprintf("whats-new-v2#marketing-marchitecture#%s", category)
36+
}
37+
38+
// Build query string
39+
query := []string{}
40+
for key, value := range params {
41+
query = append(query, fmt.Sprintf("%s=%s", key, value))
42+
}
43+
queryString := strings.Join(query, "&")
44+
45+
// Define the headers
46+
headers := map[string]string{
47+
"accept": "*/*",
48+
"accept-language": "en-US,en;q=0.9",
49+
"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
50+
"x-requested-with": "XMLHttpRequest",
51+
"sec-ch-ua": `"Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"`,
52+
"sec-ch-ua-mobile": "?0",
53+
"sec-ch-ua-platform": `"Linux"`,
54+
"sec-fetch-dest": "empty",
55+
"sec-fetch-mode": "cors",
56+
"sec-fetch-site": "same-origin",
57+
"priority": "u=1, i",
58+
"referer": "https://aws.amazon.com/new/?whats-new-content-all.sort-by=item.additionalFields.postDateTime&whats-new-content-all.sort-order=desc&awsf.whats-new-categories=marketing-marchitecture%23compute",
59+
}
60+
61+
// Create HTTP request
62+
req, err := http.NewRequest("GET", fmt.Sprintf("%s?%s", url, queryString), nil)
63+
if err != nil {
64+
return nil, err
65+
}
66+
67+
// Add headers to the request
68+
for key, value := range headers {
69+
req.Header.Add(key, value)
70+
}
71+
72+
// Send the request
73+
client := &http.Client{}
74+
resp, err := client.Do(req)
75+
if err != nil {
76+
return nil, err
77+
}
78+
defer resp.Body.Close()
79+
// fmt.Println(resp)
80+
// Check response status
81+
if resp.StatusCode != http.StatusOK {
82+
return nil, fmt.Errorf("failed to fetch data. Status code: %d", resp.StatusCode)
83+
}
84+
85+
// Parse response body
86+
body, err := io.ReadAll(resp.Body)
87+
if err != nil {
88+
return nil, err
89+
}
90+
// fmt.Println(body)
91+
var responseData map[string]interface{}
92+
err = json.Unmarshal(body, &responseData)
93+
if err != nil {
94+
fmt.Println("ERROR")
95+
return nil, err
96+
}
97+
// fmt.Println()
98+
// fmt.Println(responseData)
99+
// Extract and transform data
100+
items, ok := responseData["items"].([]interface{})
101+
if !ok {
102+
return nil, fmt.Errorf("invalid response format")
103+
}
104+
105+
var newsItems []NewsItem
106+
for _, item := range items {
107+
itemMap := item.(map[string]interface{})
108+
header := itemMap["item"].(map[string]interface{})
109+
tags := itemMap["tags"].([]interface{})
110+
111+
categoryData := "TBD"
112+
var products []string
113+
for _, tag := range tags {
114+
tagMap := tag.(map[string]interface{})
115+
if tagMap["tagNamespaceId"] == "whats-new-v2#marketing-marchitecture" {
116+
categoryData = tagMap["name"].(string)
117+
}
118+
if tagMap["tagNamespaceId"] == "whats-new-v2#general-products" {
119+
products = append(products, tagMap["name"].(string))
120+
}
121+
}
122+
123+
additionalFields := header["additionalFields"].(map[string]interface{})
124+
headline := additionalFields["headline"].(string)
125+
postBody := additionalFields["postBody"].(string)
126+
127+
// Ignore empty rows
128+
if headline == "" {
129+
continue
130+
}
131+
132+
newsItems = append(newsItems, NewsItem{
133+
ID: header["name"].(string),
134+
Date: header["dateCreated"].(string),
135+
Headline: headline,
136+
Body: postBody,
137+
Category: categoryData,
138+
Products: products,
139+
})
140+
141+
}
142+
143+
return newsItems, nil
144+
}
145+
146+
func Handler(w http.ResponseWriter, r *http.Request) {
147+
w.Header().Set("Content-Type", "text/event-stream")
148+
w.Header().Set("Cache-Control", "no-cache")
149+
w.Header().Set("Connection", "keep-alive")
150+
w.Header().Set("Access-Control-Allow-Origin", "*")
151+
152+
// Check if the query string ?json is added
153+
outputJson := false
154+
query := r.URL.Query()
155+
if _, ok := query["json"]; ok {
156+
outputJson = true
157+
}
158+
writeData := func(content []byte) {
159+
if outputJson {
160+
fmt.Fprintf(w, "%s", content)
161+
} else {
162+
fmt.Fprintf(w, "data: %s\n\n", content)
163+
}
164+
}
165+
166+
// Allow browsers which does not support SSE to run through arg no-sse.
167+
unsupportedSSE := false
168+
169+
// Disable manually by query string added by user
170+
if _, ok := query["no-sse"]; ok {
171+
unsupportedSSE = true
172+
}
173+
// Automatically disable on Vercel serverless as it does not support Server-Sent Events (SSE).
174+
if r.Header.Get("X-Vercel-Id") != "" {
175+
unsupportedSSE = true
176+
}
177+
flusher, ok := w.(http.Flusher)
178+
if !unsupportedSSE && !ok {
179+
http.Error(w, "SSE not supported", http.StatusInternalServerError)
180+
return
181+
}
182+
flusherFunc := func() {
183+
if !unsupportedSSE {
184+
flusher.Flush()
185+
}
186+
}
187+
188+
// 1. Send the correct initialization message
189+
initResp := map[string]interface{}{
190+
"jsonrpc": "2.0",
191+
"id": 1,
192+
"result": map[string]interface{}{
193+
"capabilities": map[string]interface{}{
194+
"completion": true,
195+
},
196+
"tools": []map[string]interface{}{
197+
{
198+
"name": "aws_news",
199+
"description": "Fetches latest AWS news and announcements",
200+
"parameters": map[string]interface{}{
201+
"type": "object",
202+
"properties": map[string]interface{}{
203+
"category": map[string]interface{}{
204+
"type": "string",
205+
"description": "Optional category to filter news",
206+
},
207+
},
208+
},
209+
},
210+
},
211+
},
212+
}
213+
initJSON, _ := json.Marshal(initResp)
214+
writeData(initJSON)
215+
flusherFunc()
216+
217+
category := ""
218+
var req struct {
219+
ID int `json:"id"`
220+
Method string `json:"method"`
221+
Params map[string]interface{} `json:"params"`
222+
}
223+
if r.Method == http.MethodPost {
224+
body, _ := io.ReadAll(r.Body)
225+
json.Unmarshal(body, &req)
226+
if c, ok := req.Params["category"].(string); ok {
227+
category = c
228+
}
229+
} else {
230+
if _, ok := query["category"]; ok {
231+
category = query["category"][0]
232+
}
233+
}
234+
235+
news, err := fetchNews(category)
236+
if err != nil {
237+
errResp := map[string]interface{}{
238+
"jsonrpc": "2.0",
239+
"id": req.ID,
240+
"error": map[string]interface{}{
241+
"code": -32000,
242+
"message": fmt.Sprintf("Failed to fetch news: %v", err),
243+
},
244+
}
245+
errJSON, _ := json.Marshal(errResp)
246+
writeData(errJSON)
247+
flusherFunc()
248+
return
249+
}
250+
for _, item := range news {
251+
newsResp := map[string]interface{}{
252+
"jsonrpc": "2.0",
253+
"id": req.ID,
254+
"result": map[string]interface{}{
255+
"message": map[string]interface{}{
256+
"role": "assistant",
257+
"content": fmt.Sprintf("📢 AWS News Update (%s)\n\n**%s**\n\n%s\n\nCategory: %s\nProducts: %s",
258+
item.Date,
259+
item.Headline,
260+
item.Body,
261+
item.Category,
262+
strings.Join(item.Products, ", ")),
263+
},
264+
},
265+
}
266+
newsJSON, _ := json.Marshal(newsResp)
267+
writeData(newsJSON)
268+
flusherFunc()
269+
}
270+
}

api/news/go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/mtulio/mtulio.labs-devel/api/news
2+
3+
go 1.23

0 commit comments

Comments
 (0)