Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ on:
- completed
branches:
- dev

workflow_dispatch:
jobs:

build-and-push:
Expand Down
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ name: CI Pipeline
on:
push:
branches: [ "main", "dev" ]
paths-ignore:
- '**/*.md'
- '.gitignore'
- '**/.gitignore'
pull_request:
branches: [ "main", "dev" ]

paths-ignore:
- '**/*.md'
- '.gitignore'
- '**/.gitignore'
workflow_dispatch:
jobs:
build:
name: Build and Test
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ COPY --from=builder /app/target/*.jar app.jar

EXPOSE 1337

ENTRYPOINT ["java", "-jar", "app.jar", "--SERVER_HOST=0.0.0.0", "--SERVER_PORT=1337"]
ENTRYPOINT ["java", "-jar", "app.jar", "0.0.0.0", "1337"]
37 changes: 37 additions & 0 deletions README-devops.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
## DevOps & CI/CD Workflow

This project implements a robust, modern DevOps pipeline that automates software delivery following several key best practices:

### Branching Strategy

All development work is done on feature branches, which are then merged into the `dev` branch after review and testing.

Once features are considered stable and ready for release, changes from `dev` are merged into `main`, which is the protected branch and represents production-ready code.

### CI/CD Pipeline Overview

The delivery process is fully automated using GitHub Actions.
Key pipeline steps include:

1. **Open Issue & Feature Branch Creation:**
- Development starts by opening an issue and creating a feature branch from `dev`.
2. **Continuous Integration on PRs and Pushes to main/dev:**
- Triggered by push or pull request on `main` or `dev` branches.
- **Build & Unit Test:** Automatically compiles code and runs Java unit tests (JUnit via Maven).
- **Linter & Style Check:** Runs Checkstyle and SpotBugs for static code analysis and style enforcement.
- **SAST (Static Application Security Testing):** Uses Snyk for security scanning with enforcement (pipeline fails on severe issues).
- **Build Docker Image & Scan:**
- Builds Docker image.
- Runs Trivy for Docker vulnerability scanning.
3. **Continuous Delivery (CD) to Kubernetes (K8s):**
- Triggered after a successful CI run on `dev`.
- **Docker Push:** Pushes the Docker image to Docker Hub.
- **Kubernetes Deployment:**
- Uses Minikube for testing.
- Deploys the built image using Kubernetes manifests from the `/k8s` directory.
- Monitors rollout and enables rollback on deployment failure.

### Future Improvements
- Add a relational database (e.g., PostgreSQL) for user profiles, playlists, or playback history.
- Store uploaded songs in AWS S3 for scalable, cloud-based storage.
- Deploy a test environment on AWS EC2 for integration and end-to-end test automation.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,8 @@ The server will send the appropriate response if a command was successful or not
- Music streaming occurs on a separate thread, allowing you to contiunue sending new commands.
- If something crashes or goes wrong, a logger will output a `.txt` file with the error log and additional details.

## DevOps & CI/CD
For a detailed description of the automated delivery pipeline, branching strategy, and DevOps practices implemented in this project, see [DevOps & CI/CD Workflow](README-devops.md).

# Credits
Special thanks to Stoyan Velev and his team for their exceptional course! Be sure to check it out!
2 changes: 1 addition & 1 deletion k8s/service.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ spec:
- protocol: TCP
port: 1337
targetPort: 1337
nodePort: 30007
nodePort: 30008

9 changes: 5 additions & 4 deletions src/main/java/com/spotifyremastered/client/SpotifyClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,18 @@ public class SpotifyClient {
public static void main(String[] args) {
SpotifyClient client = new SpotifyClient();

final int maxArg = 3;
final int maxArg = 2;
if (args.length >= maxArg) {
client.startClient(args[1], Integer.parseInt(args[2]));
client.startClient(args[0], Integer.parseInt(args[1]));
} else {
client.startClient(SERVER_HOST, SERVER_PORT);
}

client.startClient(SERVER_HOST, SERVER_PORT);
}

private void startClient(String ipAddress, int port) {
try (SocketChannel socketChannel = SocketChannel.open();
Scanner scanner = new Scanner(System.in)) {
System.out.println("Attempting to connect to: " + ipAddress + ":" + port + ".");
socketChannel.connect(new InetSocketAddress(ipAddress, port));
System.out.println("Connected to the server. Type 'help' for available commands.");

Expand Down
42 changes: 20 additions & 22 deletions src/main/java/com/spotifyremastered/server/SpotifyManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,28 @@

import com.spotifyremastered.server.music.MusicManager;
import com.spotifyremastered.server.user.UserManager;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class SpotifyManager {

private final UserManager userManager;
private final MusicManager musicManager;
private final Map<Client, Integer> portsManager;
private final Object lock = new Object();
private final Map<Client, Integer> portsManager = new ConcurrentHashMap<>();

private static final short FIRST_AVAILABLE_PORT = 1338;
private static final short MAX_PORTS = 1000;

public SpotifyManager() {
this.userManager = new UserManager(new HashMap<>(), new HashMap<>(), UserManager.USER_DATA_FILE_PATH);
this.musicManager = new MusicManager();
this.portsManager = new HashMap<>();
}

public SpotifyManager(UserManager userManager, MusicManager musicManager) {
this.userManager = userManager;
this.musicManager = musicManager;
this.portsManager = new HashMap<>();
}

public UserManager getUserManager() {
Expand All @@ -36,39 +35,38 @@ public MusicManager getMusicManager() {
}

public Map<Client, Integer> getPortsManager() {
synchronized (lock) {
return new HashMap<>(portsManager);
}
return Map.copyOf(portsManager);
}

public int getNewPort(Client client) {
synchronized (lock) {
int maxPort = FIRST_AVAILABLE_PORT + MAX_PORTS;
for (int i = FIRST_AVAILABLE_PORT; i < maxPort; i++) {
if (isPortAvailable(i)) {
portsManager.put(client, i);
Integer existingPort = portsManager.get(client);
if (existingPort != null) {
return existingPort;
}

int maxPort = FIRST_AVAILABLE_PORT + MAX_PORTS;
for (int i = FIRST_AVAILABLE_PORT; i < maxPort; i++) {
if (isPortAvailable(i)) {
Integer previous = portsManager.putIfAbsent(client, i);
if (previous == null) {
return i;
} else {
return previous;
}
}
throw new RuntimeException("No available ports");
}
throw new RuntimeException("No available ports");
}

public void removePort(Client client) {
synchronized (lock) {
portsManager.remove(client);
}
portsManager.remove(client);
}

public boolean isPortAvailable(int port) {
synchronized (lock) {
return !portsManager.containsValue(port);
}
return !portsManager.containsValue(port);
}

public boolean isPortAvailable(Client client) {
synchronized (lock) {
return portsManager.containsValue(portsManager.get(client));
}
return portsManager.containsKey(client);
}
}
23 changes: 15 additions & 8 deletions src/main/java/com/spotifyremastered/server/SpotifyServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.spotifyremastered.server.command.executor.CommandExecutor;
import com.spotifyremastered.server.command.factory.CommandFactory;
import com.spotifyremastered.server.user.User;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketException;
Expand All @@ -17,32 +18,38 @@
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class SpotifyServer {

private static final String SERVER_HOST = "localhost";
private static final int SERVER_PORT = 1337;
private static final String SERVER_HOST = System.getenv().getOrDefault("SERVER_HOST", "0.0.0.0");
private static final int SERVER_PORT = Integer.parseInt(System.getenv().getOrDefault("SERVER_PORT", "1337"));
private static final int BUFFER_SIZE = 1024;

private static boolean online = true;

private final SpotifyManager spotifyManager;
private final Map<SocketChannel, Client> clients;
private final Map<SocketChannel, Client> clients = new ConcurrentHashMap<>();

public SpotifyServer() {
this.clients = new HashMap<>();
this.spotifyManager = new SpotifyManager();
}

public static void main(String[] args) {
SpotifyServer server = new SpotifyServer();
server.startServer();

final int maxArg = 2;
if (args.length >= maxArg) {
server.startServer(args[0], Integer.parseInt(args[1]));
} else {
server.startServer(SERVER_HOST, SERVER_PORT);
}
}

public void startServer() {
public void startServer(String ipAddress, int port) {
try (ServerSocketChannel serverSocketChannel = ServerSocketChannel.open()) {
System.out.println("Server listening on port " + SERVER_PORT);
serverSocketChannel.bind(new InetSocketAddress(SERVER_HOST, SERVER_PORT));
System.out.println("Server listening on address: " + ipAddress + ":" + port + ".");
serverSocketChannel.bind(new InetSocketAddress(ipAddress, port));
serverSocketChannel.configureBlocking(false);

Selector selector = Selector.open();
Expand Down
Loading