diff --git a/code-challenge/README.md b/code-challenge/README.md new file mode 100644 index 000000000..d6a228b50 --- /dev/null +++ b/code-challenge/README.md @@ -0,0 +1,56 @@ +# WSO2 APK Code Challenge ToDo Service + +## Docker Image + +docker pull sampathrajapakse/todo-service:latest + +## Docker Image Creation + +Use the above provided docker image or you can build using following commands. + +docker buildx create --use + +docker buildx inspect --bootstrap + +docker buildx build --platform linux/amd64,linux/arm64 -t sampathrajapakse/todo-service:latest --push . + +## Deploy in K8s Cluster + +kubectl create ns apk + +kubectl apply -f k8s-artifacts/deployment.yaml -n apk + +## Access the pod locally + +kubectl port-forward pod/todo-app- 8080:8080 -n apk + +## Invoke the services + +### Retrieve all to-dos + +curl http://localhost:8080/todos + +### Create a new to-do + +curl -X POST http://localhost:8080/todos \ +-H "Content-Type: application/json" \ +-d '{"task": "Buy groceries", "done": false}' + +### Retrieve a single to-do + +curl http://localhost:8080/todos/1 + +### Update an existing to-do + +curl -X PUT http://localhost:8080/todos/1 \ +-H "Content-Type: application/json" \ +-d '{"task": "Buy groceries", "done": true}' + +### Delete a to-do + +curl -X DELETE http://localhost:8080/todos/1 + +### Register a user with user-reg header + +curl -X POST http://localhost:8080/register \ +-H "user-reg: sampath" diff --git a/code-challenge/k8s-artifacts/deployment.yaml b/code-challenge/k8s-artifacts/deployment.yaml new file mode 100644 index 000000000..41d049143 --- /dev/null +++ b/code-challenge/k8s-artifacts/deployment.yaml @@ -0,0 +1,41 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: todo-app + labels: + app: todo-app +spec: + replicas: 1 + selector: + matchLabels: + app: todo-app + template: + metadata: + labels: + app: todo-app + spec: + containers: + - name: todo-app + image: sampathrajapakse/todo-service:latest + ports: + - containerPort: 8080 + resources: + limits: + memory: "128Mi" + cpu: "500m" + requests: + memory: "64Mi" + cpu: "250m" +--- +apiVersion: v1 +kind: Service +metadata: + name: todo-app-service +spec: + selector: + app: todo-app + ports: + - protocol: TCP + port: 80 + targetPort: 8080 + type: LoadBalancer diff --git a/code-challenge/open-api/oas.yaml b/code-challenge/open-api/oas.yaml new file mode 100644 index 000000000..18bda7cc6 --- /dev/null +++ b/code-challenge/open-api/oas.yaml @@ -0,0 +1,144 @@ +openapi: 3.0.3 +info: + title: To-Do API + description: A simple to-do service API + version: 1.0.0 +servers: + - url: http://todo-app-service:80 + description: Local development server +paths: + /todos: + get: + summary: Retrieve all to-dos + description: Returns a list of all to-do items + responses: + '200': + description: A list of to-do items + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ToDo' + post: + summary: Create a new to-do + description: Adds a new to-do item + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NewToDo' + responses: + '201': + description: To-do created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ToDo' + '400': + description: Invalid input + /todos/{id}: + get: + summary: Retrieve a single to-do item + description: Returns the details of a specific to-do item by its ID + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: The requested to-do item + content: + application/json: + schema: + $ref: '#/components/schemas/ToDo' + '404': + description: To-do not found + put: + summary: Update a to-do item + description: Updates an existing to-do item by its ID + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/NewToDo' + responses: + '200': + description: To-do updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ToDo' + '400': + description: Invalid input + '404': + description: To-do not found + delete: + summary: Delete a to-do item + description: Deletes a to-do item by its ID + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: To-do deleted successfully + '404': + description: To-do not found + /register: + post: + summary: Register a user + description: Registers a new user and expects a custom header `user-reg` + parameters: + - name: user-reg + in: header + required: true + schema: + type: string + responses: + '201': + description: User registered successfully + '400': + description: Missing or invalid header +components: + schemas: + ToDo: + type: object + properties: + id: + type: integer + description: Unique identifier of the to-do item + task: + type: string + description: The task description + done: + type: boolean + description: Status of the to-do (completed or not) + required: + - id + - task + - done + NewToDo: + type: object + properties: + task: + type: string + description: The task description + done: + type: boolean + description: Status of the to-do (completed or not) + required: + - task + - done diff --git a/code-challenge/todo-app/Dockerfile b/code-challenge/todo-app/Dockerfile new file mode 100644 index 000000000..60e72a625 --- /dev/null +++ b/code-challenge/todo-app/Dockerfile @@ -0,0 +1,23 @@ +# Build Stage +FROM golang:1.22 AS build + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . ./ + +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . + +# Deployment Stage +FROM alpine:3.19.1 + +WORKDIR /root/ + +COPY --from=build /app/main . + +# Expose the application port +EXPOSE 8080 + +CMD ["./main"] diff --git a/code-challenge/todo-app/go.mod b/code-challenge/todo-app/go.mod new file mode 100644 index 000000000..ab693cdad --- /dev/null +++ b/code-challenge/todo-app/go.mod @@ -0,0 +1,5 @@ +module github.com/wso2/apk/code-challenge/todo-app + +go 1.22.6 + +require github.com/gorilla/mux v1.8.1 diff --git a/code-challenge/todo-app/go.sum b/code-challenge/todo-app/go.sum new file mode 100644 index 000000000..712833743 --- /dev/null +++ b/code-challenge/todo-app/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= diff --git a/code-challenge/todo-app/main.go b/code-challenge/todo-app/main.go new file mode 100644 index 000000000..f44ac3431 --- /dev/null +++ b/code-challenge/todo-app/main.go @@ -0,0 +1,102 @@ +package main + +import ( + "encoding/json" + "log" + "net/http" + "strconv" + + "github.com/gorilla/mux" +) + +type Todo struct { + ID int `json:"id"` + Task string `json:"task"` + Done bool `json:"done"` +} + +var todos []Todo +var nextID = 1 + +// Get all to-dos +func getTodos(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(todos) +} + +// Create a new to-do +func createTodo(w http.ResponseWriter, r *http.Request) { + var todo Todo + _ = json.NewDecoder(r.Body).Decode(&todo) + todo.ID = nextID + nextID++ + todos = append(todos, todo) + json.NewEncoder(w).Encode(todo) +} + +// Get a single to-do by ID +func getTodoByID(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id, _ := strconv.Atoi(params["id"]) + for _, todo := range todos { + if todo.ID == id { + json.NewEncoder(w).Encode(todo) + return + } + } + http.Error(w, "To-do not found", http.StatusNotFound) +} + +// Update a to-do by ID +func updateTodo(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id, _ := strconv.Atoi(params["id"]) + for index, todo := range todos { + if todo.ID == id { + todos = append(todos[:index], todos[index+1:]...) + var updatedTodo Todo + _ = json.NewDecoder(r.Body).Decode(&updatedTodo) + updatedTodo.ID = id + todos = append(todos, updatedTodo) + json.NewEncoder(w).Encode(updatedTodo) + return + } + } + http.Error(w, "To-do not found", http.StatusNotFound) +} + +// Delete a to-do by ID +func deleteTodo(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id, _ := strconv.Atoi(params["id"]) + for index, todo := range todos { + if todo.ID == id { + todos = append(todos[:index], todos[index+1:]...) + json.NewEncoder(w).Encode(todos) + return + } + } + http.Error(w, "To-do not found", http.StatusNotFound) +} + +// Register a user +func registerUser(w http.ResponseWriter, r *http.Request) { + regHeader := r.Header.Get("user-reg") + if regHeader == "" { + http.Error(w, "Missing user-reg header", http.StatusBadRequest) + return + } + w.WriteHeader(http.StatusCreated) + w.Write([]byte("User registered with header: " + regHeader)) +} + +func main() { + router := mux.NewRouter() + router.HandleFunc("/todos", getTodos).Methods("GET") + router.HandleFunc("/todos", createTodo).Methods("POST") + router.HandleFunc("/todos/{id}", getTodoByID).Methods("GET") + router.HandleFunc("/todos/{id}", updateTodo).Methods("PUT") + router.HandleFunc("/todos/{id}", deleteTodo).Methods("DELETE") + router.HandleFunc("/register", registerUser).Methods("POST") + + log.Fatal(http.ListenAndServe(":8080", router)) +}