From de2028a073185dfd5868df539b2df9bafb79333b Mon Sep 17 00:00:00 2001 From: HarkuLi Date: Sat, 11 Apr 2026 09:08:22 +0000 Subject: [PATCH 1/9] Add EnvironmentVariables wrapper --- src/main/java/tools/EnvironmentVariables.java | 35 +++++++++++++++ .../java/tools/EnvironmentVariablesTest.java | 44 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 src/main/java/tools/EnvironmentVariables.java create mode 100644 src/test/java/tools/EnvironmentVariablesTest.java diff --git a/src/main/java/tools/EnvironmentVariables.java b/src/main/java/tools/EnvironmentVariables.java new file mode 100644 index 00000000000..c517fb16c89 --- /dev/null +++ b/src/main/java/tools/EnvironmentVariables.java @@ -0,0 +1,35 @@ +package tools; + +import java.util.Map; + +/** + * Wrapper class for accessing environment variables. + * + * Using this class instead of calling built-in functions like `System.getenv()` + * make it possible to mock environment variables in tests. + * + * Note that environment variables should only be accessed in limited places, + * like configuration loading. In most places, you should use config instead + * of environment variables. + */ +public class EnvironmentVariables { + private static EnvironmentVariables instance; + + private EnvironmentVariables() {} + + public static synchronized EnvironmentVariables instance() { + if (instance == null) { + instance = new EnvironmentVariables(); + } + + return instance; + } + + public static void setInstance(EnvironmentVariables instance) { + EnvironmentVariables.instance = instance; + } + + public Map getAll() { + return System.getenv(); + } +} diff --git a/src/test/java/tools/EnvironmentVariablesTest.java b/src/test/java/tools/EnvironmentVariablesTest.java new file mode 100644 index 00000000000..3e99b01756e --- /dev/null +++ b/src/test/java/tools/EnvironmentVariablesTest.java @@ -0,0 +1,44 @@ +package tools; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.Map; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +public class EnvironmentVariablesTest { + @AfterEach + void tearDown() { + EnvironmentVariables.setInstance(null); + } + + @Test + void instance_providesDefaultImplementation() { + EnvironmentVariables instance = EnvironmentVariables.instance(); + assertNotNull(instance, "Instance should not be null"); + assertEquals(instance.getAll(), System.getenv(), "getAll() should return all environment variables"); + } + + @Test + void instance_returnsSameSingleton() { + EnvironmentVariables first = EnvironmentVariables.instance(); + EnvironmentVariables second = EnvironmentVariables.instance(); + assertSame(first, second, "The singleton should return the same instance on subsequent calls"); + } + + @Test + void setInstance_allowsMocking() { + EnvironmentVariables mock = Mockito.mock(EnvironmentVariables.class); + String dbUser = "user"; + String dbPass = "pass"; + when(mock.getAll()).thenReturn(Map.of("DB_USER", dbUser, "DB_PASS", dbPass)); + + EnvironmentVariables.setInstance(mock); + + Map vars = EnvironmentVariables.instance().getAll(); + assertEquals(dbUser, vars.get("DB_USER")); + assertEquals(dbPass, vars.get("DB_PASS")); + } +} From d262ac4757b2dc6b50de86f4e1ee135b911c0025 Mon Sep 17 00:00:00 2001 From: HarkuLi Date: Wed, 8 Apr 2026 18:32:52 +0000 Subject: [PATCH 2/9] Add environment variable support to config.yaml --- pom.xml | 6 +++++ src/main/java/config/YamlConfig.java | 39 +++++++++++++++++++++------- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index 945a417fd89..a4aca1b10d9 100644 --- a/pom.xml +++ b/pom.xml @@ -63,6 +63,7 @@ 4.2.2.Final 1.17 1.0 + 1.15.0 6.3.0 9.3.0 3.49.5 @@ -82,6 +83,11 @@ jcip-annotations ${jcip-annotations.version} + + org.apache.commons + commons-text + ${commons-text.version} + diff --git a/src/main/java/config/YamlConfig.java b/src/main/java/config/YamlConfig.java index 2263eb664e7..dfdb685b908 100644 --- a/src/main/java/config/YamlConfig.java +++ b/src/main/java/config/YamlConfig.java @@ -2,31 +2,50 @@ import com.esotericsoftware.yamlbeans.YamlReader; import constants.string.CharsetConstants; +import tools.EnvironmentVariables; -import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import org.apache.commons.text.StringSubstitutor; + public class YamlConfig { public static final String CONFIG_FILE_NAME = "config.yaml"; - public static final YamlConfig config = loadConfig(); + public static final YamlConfig config = load(Path.of(CONFIG_FILE_NAME)); public List worlds; public ServerConfig server; - private static YamlConfig loadConfig() { + static YamlConfig load(Path path) { + String content = replaceEnvironmentVariables(readFile(path)); + + try (YamlReader reader = new YamlReader(content)) { + return reader.read(YamlConfig.class); + } catch (IOException e) { + throw new RuntimeException( + "Failed to parse config file " + path.toString() + + ": " + e.getMessage() + ); + } + } + + private static String readFile(Path path) { try { - YamlReader reader = new YamlReader(Files.newBufferedReader(Path.of(CONFIG_FILE_NAME), CharsetConstants.CHARSET)); - YamlConfig config = reader.read(YamlConfig.class); - reader.close(); - return config; - } catch (FileNotFoundException e) { - throw new RuntimeException("Could not read config file " + YamlConfig.CONFIG_FILE_NAME + ": " + e.getMessage()); + return Files.readString(path, CharsetConstants.CHARSET); } catch (IOException e) { - throw new RuntimeException("Could not successfully parse config file " + YamlConfig.CONFIG_FILE_NAME + ": " + e.getMessage()); + throw new RuntimeException( + "Failed to read config file " + path.toString() + + ": " + e.getMessage() + ); } } + + private static String replaceEnvironmentVariables(String content) { + return new StringSubstitutor(EnvironmentVariables.instance().getAll()) + .setValueDelimiter(':') + .replace(content); + } } From 97a85997bb24e581a0df1c2204ae2c38f76f42a5 Mon Sep 17 00:00:00 2001 From: HarkuLi Date: Fri, 10 Apr 2026 14:22:35 +0000 Subject: [PATCH 3/9] Add unit tests for `YamlConfig` --- pom.xml | 7 ++ src/test/java/config/YamlConfigTest.java | 153 +++++++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 src/test/java/config/YamlConfigTest.java diff --git a/pom.xml b/pom.xml index a4aca1b10d9..44809ad9589 100644 --- a/pom.xml +++ b/pom.xml @@ -70,6 +70,7 @@ 4.32.0 5.13.1 5.18.0 + 1.3.1 @@ -198,6 +199,12 @@ ${mockito.version} test + + com.google.jimfs + jimfs + ${jimfs.version} + test + diff --git a/src/test/java/config/YamlConfigTest.java b/src/test/java/config/YamlConfigTest.java new file mode 100644 index 00000000000..798bea585ad --- /dev/null +++ b/src/test/java/config/YamlConfigTest.java @@ -0,0 +1,153 @@ +package config; + +import com.google.common.jimfs.Configuration; +import com.google.common.jimfs.Jimfs; + +import constants.string.CharsetConstants; +import tools.EnvironmentVariables; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +class YamlConfigTest { + private FileSystem fs; + + @BeforeEach + void setUp() { + fs = Jimfs.newFileSystem(Configuration.unix()); + } + + @AfterEach + void tearDown() throws IOException { + fs.close(); + } + + @Test + void load_noEnvironmentVariables() throws IOException { + int flag = 1; + String serverMessage = "Hello World"; + int channels = 2; + String dbHost = "localhost"; + String dbUser = "user"; + String dbPass = "pass"; + int worlds = 2; + String yamlContent = String.format( + """ + worlds: + - flag: %d + server_message: "%s" + channels: %d + - flag: 0 + server_message: "Another World" + channels: 1 + server: + DB_HOST: "%s" + DB_USER: "%s" + DB_PASS: "%s" + WORLDS: %d + """, + flag, serverMessage, channels, dbHost, dbUser, dbPass, worlds + ); + Path configPath = fs.getPath("/config.yaml"); + Files.writeString(configPath, yamlContent, CharsetConstants.CHARSET); + + YamlConfig config = YamlConfig.load(configPath); + + assertNotNull(config.worlds); + assertEquals(2, config.worlds.size()); + assertEquals(1, config.worlds.get(0).flag); + assertEquals(serverMessage, config.worlds.get(0).server_message); + assertEquals(channels, config.worlds.get(0).channels); + + assertNotNull(config.server); + assertEquals(dbHost, config.server.DB_HOST); + assertEquals(dbUser, config.server.DB_USER); + assertEquals(dbPass, config.server.DB_PASS); + assertEquals(worlds, config.server.WORLDS); + } + + @Test + void load_replaceEnvironmentVariables() throws IOException { + String dbUser = "user"; + String dbPass = "pass"; + mockEnvironmentVariables(Map.of( + "DB_USER", dbUser, + "DB_PASS", dbPass + )); + + String yamlContent = + """ + server: + DB_USER: "${DB_USER}" + DB_PASS: "${DB_PASS}" + """; + Path configPath = fs.getPath("/config.yaml"); + Files.writeString(configPath, yamlContent, CharsetConstants.CHARSET); + + YamlConfig config = YamlConfig.load(configPath); + assertNotNull(config.server); + assertEquals(dbUser, config.server.DB_USER); + assertEquals(dbPass, config.server.DB_PASS); + } + + private void mockEnvironmentVariables(Map envVars) { + EnvironmentVariables mock = Mockito.mock(EnvironmentVariables.class); + Mockito.when(mock.getAll()).thenReturn(envVars); + EnvironmentVariables.setInstance(mock); + } + + @Test + void load_useDefaultValueForEnvironmentVariablesNotPresent() throws IOException { + EnvironmentVariables.setInstance(null); + + String dbUser = "user"; + String dbPass = "pass"; + String yamlContent = String.format( + """ + server: + DB_USER: "${DB_USER:%s}" + DB_PASS: "${DB_PASS:%s}" + """, + dbUser, dbPass + ); + Path configPath = fs.getPath("/config.yaml"); + Files.writeString(configPath, yamlContent, CharsetConstants.CHARSET); + + YamlConfig config = YamlConfig.load(configPath); + assertNotNull(config.server); + assertEquals(dbUser, config.server.DB_USER); + assertEquals(dbPass, config.server.DB_PASS); + } + + @Test + void load_throwExceptionWhenFileDoesNotExist() { + Path configPath = fs.getPath("/config.yaml"); + + assertThrows(RuntimeException.class, () -> { + YamlConfig.load(configPath); + }); + } + + @Test + void load_throwExceptionWhenFailedToParseConfig() throws IOException { + Path configPath = fs.getPath("/config.yaml"); + Files.writeString(configPath, "invalid_field: value", CharsetConstants.CHARSET); + + assertThrows(RuntimeException.class, () -> { + YamlConfig.load(configPath); + }); + } +} From 5fdc5a544b819e3e113e91f6c0be4f61b2a6e351 Mon Sep 17 00:00:00 2001 From: HarkuLi Date: Sat, 11 Apr 2026 10:31:40 +0000 Subject: [PATCH 4/9] Extract DB settings as environment variables --- .env.example | 5 +++++ .gitignore | 1 + config.yaml | 9 +++++---- docker-compose.yml | 12 ++++++------ src/main/java/config/ServerConfig.java | 3 ++- src/main/java/tools/DatabaseConnection.java | 12 ++++++------ 6 files changed, 25 insertions(+), 17 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 00000000000..35d6238b2e5 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +DB_HOST=db +DB_PORT=3306 +DB_NAME=cosmic +DB_USER=root +DB_PASS= diff --git a/.gitignore b/.gitignore index 27980c65f98..45251782214 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ !.idea/codeStyles/ *.iml /target +.env # build files /build/ diff --git a/config.yaml b/config.yaml index f46841921b3..192534e5929 100644 --- a/config.yaml +++ b/config.yaml @@ -159,10 +159,11 @@ worlds: server: #Database Configuration - DB_URL_FORMAT: "jdbc:mysql://%s:3306/cosmic" # If the docker ENV for DB_HOST is anything but "db", this string format should be changed from 3306 to 3307 (or whichever port it was changed to in docker) - DB_HOST: "localhost" - DB_USER: "root" - DB_PASS: "" + DB_HOST: "${DB_HOST:db}" + DB_PORT: "${DB_PORT:3306}" + DB_NAME: "${DB_NAME:cosmic}" + DB_USER: "${DB_USER:root}" + DB_PASS: "${DB_PASS:}" INIT_CONNECTION_POOL_TIMEOUT: 90 # Seconds #Login Configuration diff --git a/docker-compose.yml b/docker-compose.yml index a872b6dc839..869b820033b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,16 +17,16 @@ services: - ./config.yaml:/opt/server/config.yaml - ./scripts:/opt/server/scripts - ./wz:/opt/server/wz - environment: - DB_HOST: "db" ## Remember if this is present it will OVERRIDE the host in the config.yaml, if you put here anything other than db, you'll need to change the config.yaml jdbc string to port 3307, and not port 3306 - + env_file: + - ./.env + db: image: mysql:8.4.0 environment: - MYSQL_DATABASE: "cosmic" - MYSQL_ROOT_PASSWORD: "" + MYSQL_DATABASE: "${DB_NAME}" + MYSQL_ROOT_PASSWORD: "${DB_PASS}" MYSQL_ALLOW_EMPTY_PASSWORD: yes ports: - - "3307:3306" + - "${DB_PORT}:3306" volumes: - ./database/docker-db-data:/var/lib/mysql diff --git a/src/main/java/config/ServerConfig.java b/src/main/java/config/ServerConfig.java index 036dae258d7..a79a3e69451 100644 --- a/src/main/java/config/ServerConfig.java +++ b/src/main/java/config/ServerConfig.java @@ -6,8 +6,9 @@ public class ServerConfig { //Database Configuration - public String DB_URL_FORMAT; public String DB_HOST; + public int DB_PORT; + public String DB_NAME; public String DB_USER; public String DB_PASS; public int INIT_CONNECTION_POOL_TIMEOUT; diff --git a/src/main/java/tools/DatabaseConnection.java b/src/main/java/tools/DatabaseConnection.java index b0313b92fac..e60677a0ebe 100644 --- a/src/main/java/tools/DatabaseConnection.java +++ b/src/main/java/tools/DatabaseConnection.java @@ -44,12 +44,12 @@ public static Handle getHandle() { } private static String getDbUrl() { - // Environment variables override what's defined in the config file - // This feature is used for the Docker support - String hostOverride = System.getenv("DB_HOST"); - String host = hostOverride != null ? hostOverride : YamlConfig.config.server.DB_HOST; - String dbUrl = String.format(YamlConfig.config.server.DB_URL_FORMAT, host); - return dbUrl; + return String.format( + "jdbc:mysql://%s:%d/%s", + YamlConfig.config.server.DB_HOST, + YamlConfig.config.server.DB_PORT, + YamlConfig.config.server.DB_NAME + ); } private static HikariConfig getConfig() { From e1f671863f3f7fe04048f986746146d2960c1d49 Mon Sep 17 00:00:00 2001 From: HarkuLi Date: Thu, 19 Mar 2026 15:37:10 +0800 Subject: [PATCH 5/9] README: Reorganize contents 1. Extract the part of Docker as an independent section (Quick start). 2. Getting started => Local setup. And make it focus on server part. 3. Extract the part of "Client" and "Getting into the game" as independent sections. --- README.md | 69 +++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 366ab226247..2b841283a73 100644 --- a/README.md +++ b/README.md @@ -48,15 +48,57 @@ The project follows the [semantic versioning](https://semver.org/) scheme using * *General changes or improvements* are treated as MINOR: 1.__2__.3 -> 1.__3.0__ * *Major changes* are treated as MAJOR: __1__.2.3 -> __2.0.0__ -## Getting started -Follow along as I go through the steps to play the game on your local computer from start to finish. I won't go into extreme detail, so if you don't have prior experience with Java or git, you might struggle. +## Quick start + +### Prerequisite + +1. [Docker](https://www.docker.com) +2. [Docker Compose](https://docs.docker.com/compose/install) + +### Start services + +To start all services, run: + +```bash +docker compose up -d +``` + +Then you can use + +```bash +docker compose logs -f maplestory +``` + +to check the logs of Cosmic server. + +Once the server is ready, you will see a message like: + +```log +07:24:20.269 [main] INFO server.Server - Cosmic is now online after 14547 ms. +``` + +### Stop services + +```bash +docker compose down +``` + +### Rebuild + +You must rebuild images after any code changes: + +```bash +docker compose build +``` + +## Local setup +You can also run Cosmic on your actual machine. We will set up the following: - Database - the database is used by the server to store game data such as accounts, characters and inventory items. - Server - the server is the "brain" and routes network traffic between the clients. -- Client - the client is the application used to _play the game_, i.e. MapleStory.exe. -### 1 - Database +### 1 - Database You will start by installing the database server and database client. Then you will connect to the server with the client to create a new database schema. #### Steps @@ -88,11 +130,6 @@ You will start by cloning the repository, then configure the database properties Below, I list other ways of running the server which are completely optional. -#### Docker -Support for Docker is also provided out of the box, as an alternative to running straight in the IDE. If you have [Docker Desktop](https://www.docker.com/products/docker-desktop/) installed it's as easy as running `docker compose up`. - -Making changes becomes a bit more tedious though as you have to rebuild the server image via `docker compose up --build`. - #### Jar Another option is to start the server from a terminal by running a jar file. You first need to build the jar file from source which requires [Maven](https://maven.apache.org/). Fortunately, [Maven Wrapper](https://maven.apache.org/wrapper/) is provided so you don't have to install Maven separately. @@ -102,15 +139,17 @@ To run the jar, a ``launch.bat`` file is provided for convenience. Simply double Alternatively, run the jar file from the terminal. Just remember to provide the `wz-path` system property pointing to your wz directory. -### 3 - Client -The client files are located in a separate repository: https://github.com/P0nk/Cosmic-client +## Client +Client is the application used to _play the game_, i.e. MapleStory.exe. + +The files are located in a separate repository: https://github.com/P0nk/Cosmic-client Follow the installation guide in the README. -### 4 - Getting into the game -You have successfully started the client, and you're looking at the login screen. +## Getting into the game +You have successfully started the client, and you're looking at the login screen. -#### Logging in +### Logging in At this point, you can log in to the admin account using the following credentials: * Username: "admin" * Password: "admin" @@ -119,7 +158,7 @@ At this point, you can log in to the admin account using the following credentia You can also create a new regular account by typing in your desired username & password and attempting to log in. This "automatic registration" feature lets you create new accounts to play around with. It is enabled by default (see _config.yaml_). -#### Entering the game +### Entering the game Create a new character as you normally would, and then select it to enter the game. Hooray, finally we're in! If you log in to the "Admin" character, you'll notice that the character looks almost invisible. This is hide mode, which is enabled by default when you log in to a GM character. You won't be visible to normal players and no mobs will move if you're alone on the map. Toggle hide mode on or off by typing "@hide" in the in-game chat. From e1c197301b2b20e003e85e422e72ca3f0feae79d Mon Sep 17 00:00:00 2001 From: HarkuLi Date: Sat, 11 Apr 2026 11:14:50 +0000 Subject: [PATCH 6/9] README: Add instructions for environment variables --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2b841283a73..ca2be7de9d6 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,20 @@ The project follows the [semantic versioning](https://semver.org/) scheme using 1. [Docker](https://www.docker.com) 2. [Docker Compose](https://docs.docker.com/compose/install) +### Set up environment variables + +Create `.env` file by copying the `.env.example` file: + +``` +cp .env.example .env +``` + +Then, check and edit the `.env` file if necessary. + +> Note: Though `.env.example` already contains default settings that make +> Cosmic work, it is recommended to set settings like `DB_PASS` with reasonable +> values. + ### Start services To start all services, run: @@ -124,7 +138,7 @@ You will start by cloning the repository, then configure the database properties #### Steps 1. Clone Cosmic into a new project. In IntelliJ, you would create a new project from version control. -2. Open _config.yaml_. Find "DB_PASS" and set it to your database root user password. +2. [Create `.env` file](#set-up-environment-variables) and then load it into current shell session by `. .env` command. 3. Start the server. The main method is located in `net.server.Server`. 4. If you see "Cosmic is now online" in the console, it means the server is online and ready to serve traffic. Yay! From 4fa4f29e45e6f30ab95c48d379240a0668bac6a0 Mon Sep 17 00:00:00 2001 From: HarkuLi Date: Sat, 2 May 2026 21:34:21 +0800 Subject: [PATCH 7/9] Add Helm chart --- charts/cosmic/.gitignore | 1 + charts/cosmic/.helmignore | 23 ++++ charts/cosmic/Chart.lock | 6 + charts/cosmic/Chart.yaml | 11 ++ charts/cosmic/templates/NOTES.txt | 16 +++ charts/cosmic/templates/_helpers.tpl | 62 +++++++++ charts/cosmic/templates/deployment.yaml | 130 ++++++++++++++++++ charts/cosmic/templates/service.yaml | 23 ++++ charts/cosmic/templates/serviceaccount.yaml | 13 ++ .../templates/tests/test-connection.yaml | 15 ++ charts/cosmic/values.yaml | 109 +++++++++++++++ 11 files changed, 409 insertions(+) create mode 100644 charts/cosmic/.gitignore create mode 100644 charts/cosmic/.helmignore create mode 100644 charts/cosmic/Chart.lock create mode 100644 charts/cosmic/Chart.yaml create mode 100644 charts/cosmic/templates/NOTES.txt create mode 100644 charts/cosmic/templates/_helpers.tpl create mode 100644 charts/cosmic/templates/deployment.yaml create mode 100644 charts/cosmic/templates/service.yaml create mode 100644 charts/cosmic/templates/serviceaccount.yaml create mode 100644 charts/cosmic/templates/tests/test-connection.yaml create mode 100644 charts/cosmic/values.yaml diff --git a/charts/cosmic/.gitignore b/charts/cosmic/.gitignore new file mode 100644 index 00000000000..ee3892e8794 --- /dev/null +++ b/charts/cosmic/.gitignore @@ -0,0 +1 @@ +charts/ diff --git a/charts/cosmic/.helmignore b/charts/cosmic/.helmignore new file mode 100644 index 00000000000..0e8a0eb36f4 --- /dev/null +++ b/charts/cosmic/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/cosmic/Chart.lock b/charts/cosmic/Chart.lock new file mode 100644 index 00000000000..afde90b5deb --- /dev/null +++ b/charts/cosmic/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: mysql + repository: oci://registry-1.docker.io/bitnamicharts + version: 11.1.4 +digest: sha256:d94b54340014411a896f39f1ae33c71b3b935fe2c92ec11b9bab8dd738baffc8 +generated: "2026-05-09T11:51:40.568682Z" diff --git a/charts/cosmic/Chart.yaml b/charts/cosmic/Chart.yaml new file mode 100644 index 00000000000..10903a3dae9 --- /dev/null +++ b/charts/cosmic/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: cosmic +description: A Helm chart for Cosmic MapleStory server +type: application +version: 0.1.0 +appVersion: "1.1.3" + +dependencies: + - name: mysql + version: "11.1.4" + repository: "oci://registry-1.docker.io/bitnamicharts" diff --git a/charts/cosmic/templates/NOTES.txt b/charts/cosmic/templates/NOTES.txt new file mode 100644 index 00000000000..e58381043ff --- /dev/null +++ b/charts/cosmic/templates/NOTES.txt @@ -0,0 +1,16 @@ +1. Get the application URL by running these commands: +{{- if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "cosmic.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "cosmic.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "cosmic.fullname" . }} --template "{{ "{{ (index .status.loadBalancer.ingress 0).ip }}" }}") + echo http://$SERVICE_IP:{{ .Values.service.ports.login }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "cosmic.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/charts/cosmic/templates/_helpers.tpl b/charts/cosmic/templates/_helpers.tpl new file mode 100644 index 00000000000..1ad22dd1890 --- /dev/null +++ b/charts/cosmic/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "cosmic.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "cosmic.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "cosmic.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "cosmic.labels" -}} +helm.sh/chart: {{ include "cosmic.chart" . }} +{{ include "cosmic.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "cosmic.selectorLabels" -}} +app.kubernetes.io/name: {{ include "cosmic.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "cosmic.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "cosmic.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/cosmic/templates/deployment.yaml b/charts/cosmic/templates/deployment.yaml new file mode 100644 index 00000000000..885db35135f --- /dev/null +++ b/charts/cosmic/templates/deployment.yaml @@ -0,0 +1,130 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "cosmic.fullname" . }} + labels: + {{- include "cosmic.labels" . | nindent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + {{- include "cosmic.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "cosmic.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "cosmic.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: login + containerPort: {{ .Values.cosmic.containerPorts.login }} + protocol: TCP + {{- range .Values.cosmic.containerPorts.channels }} + {{- range $i := untilStep (int .start) (int .end) 1 }} + - name: channel-{{ $i }} + containerPort: {{ $i }} + protocol: TCP + {{- end }} + {{- end }} + env: + - name: DB_HOST + value: {{ include "mysql.primary.fullname" .Subcharts.mysql }} + - name: DB_PORT + value: {{ .Values.mysql.primary.service.ports.mysql | quote }} + - name: DB_NAME + value: {{ .Values.mysql.auth.database | quote }} + - name: DB_USER + value: {{ .Values.mysql.auth.username | quote }} + - name: DB_PASS + valueFrom: + secretKeyRef: + name: {{ include "mysql.secretName" .Subcharts.mysql }} + key: mysql-password + {{- with .Values.livenessProbe }} + livenessProbe: + tcpSocket: + port: login + initialDelaySeconds: {{ .initialDelaySeconds }} + periodSeconds: {{ .periodSeconds }} + timeoutSeconds: {{ .timeoutSeconds }} + successThreshold: {{ .successThreshold }} + failureThreshold: {{ .failureThreshold }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + tcpSocket: + port: login + initialDelaySeconds: {{ .initialDelaySeconds }} + periodSeconds: {{ .periodSeconds }} + timeoutSeconds: {{ .timeoutSeconds }} + successThreshold: {{ .successThreshold }} + failureThreshold: {{ .failureThreshold }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + initContainers: + - name: mysql-isalive + image: {{ include "mysql.image" .Subcharts.mysql }} + env: + - name: DB_HOST + value: {{ include "mysql.primary.fullname" .Subcharts.mysql }} + - name: DB_PORT + value: {{ .Values.mysql.primary.service.ports.mysql | quote }} + - name: MYSQL_USER + value: {{ .Values.mysql.auth.username | quote }} + {{- if .Values.mysql.auth.usePasswordFiles }} + - name: MYSQL_PASSWORD_FILE + value: {{ default "/opt/bitnami/mysql/secrets/mysql-password" .Values.mysql.auth.customPasswordFiles.user }} + {{- else }} + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "mysql.secretName" .Subcharts.mysql }} + key: mysql-password + {{- end }} + command: + - /bin/bash + - -ec + - | + password_aux="${MYSQL_PASSWORD:-}" + if [[ -f "${MYSQL_PASSWORD_FILE:-}" ]]; then + password_aux=$(cat "${MYSQL_PASSWORD_FILE}") + fi + mysqladmin ping -h ${DB_HOST} -P ${DB_PORT} -u"${MYSQL_USER}" -p"${password_aux}" | grep "mysqld is alive" + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/cosmic/templates/service.yaml b/charts/cosmic/templates/service.yaml new file mode 100644 index 00000000000..e06c4cf0879 --- /dev/null +++ b/charts/cosmic/templates/service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "cosmic.fullname" . }} + labels: + {{- include "cosmic.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.ports.login }} + targetPort: login + protocol: TCP + name: login + {{- range .Values.cosmic.containerPorts.channels }} + {{- range $i := untilStep (int .start) (int .end) 1 }} + - port: {{ $i }} + targetPort: channel-{{ $i }} + protocol: TCP + name: channel-{{ $i }} + {{- end }} + {{- end }} + selector: + {{- include "cosmic.selectorLabels" . | nindent 4 }} diff --git a/charts/cosmic/templates/serviceaccount.yaml b/charts/cosmic/templates/serviceaccount.yaml new file mode 100644 index 00000000000..a527a75fcb7 --- /dev/null +++ b/charts/cosmic/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "cosmic.serviceAccountName" . }} + labels: + {{- include "cosmic.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/charts/cosmic/templates/tests/test-connection.yaml b/charts/cosmic/templates/tests/test-connection.yaml new file mode 100644 index 00000000000..f0e015da27c --- /dev/null +++ b/charts/cosmic/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "cosmic.fullname" . }}-test-connection" + labels: + {{- include "cosmic.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "cosmic.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/charts/cosmic/values.yaml b/charts/cosmic/values.yaml new file mode 100644 index 00000000000..74c19531893 --- /dev/null +++ b/charts/cosmic/values.yaml @@ -0,0 +1,109 @@ +# Ref: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: P0nk/cosmic + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +# This is for the secrets for pulling an image from a private repository. +# Ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# Ref: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created. + create: false + # Automatically mount a ServiceAccount's API credentials? + automount: false + # Annotations to add to the service account. + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template. + name: "" + +# Ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: {} +# Ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# Ref: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # Ref: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: LoadBalancer + ports: + login: 8484 + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# Ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 3 +readinessProbe: + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 5 + successThreshold: 1 + failureThreshold: 3 + +nodeSelector: {} + +affinity: {} + +tolerations: [] + +cosmic: + containerPorts: + login: 8484 + # Channels are exclusive of "end" values. + channels: + - start: 7575 + end: 7578 + +mysql: + # Ref: https://github.com/bitnami/charts/blob/48dbb30c/bitnami/mysql/values.yaml#L122 + image: + registry: public.ecr.aws + repository: bitnami/mysql + auth: + database: cosmic + rootPassword: changeme + username: cosmic + password: changeme + existingSecret: "" + primary: + startupProbe: + # If the MySQL subchart fall into a restart loop due to low storage + # performance, increase the `failureThreshold` might help. + failureThreshold: 10 + persistence: + existingClaim: "" + size: 8Gi From f4d1d2c3cb67d5f3373211ad76c3d88eea3d588e Mon Sep 17 00:00:00 2001 From: HarkuLi Date: Sat, 9 May 2026 04:12:42 +0800 Subject: [PATCH 8/9] Add ConfigMap setting for config.yaml --- charts/cosmic/templates/configmap.yaml | 11 +++++++++++ charts/cosmic/templates/deployment.yaml | 8 ++++++++ charts/cosmic/values.yaml | 2 ++ 3 files changed, 21 insertions(+) create mode 100644 charts/cosmic/templates/configmap.yaml diff --git a/charts/cosmic/templates/configmap.yaml b/charts/cosmic/templates/configmap.yaml new file mode 100644 index 00000000000..5c0da838c19 --- /dev/null +++ b/charts/cosmic/templates/configmap.yaml @@ -0,0 +1,11 @@ +{{- if .Values.cosmic.configuration -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "cosmic.fullname" . }} + labels: + {{- include "cosmic.labels" . | nindent 4 }} +data: + config.yaml: |- + {{ .Values.cosmic.configuration | nindent 4 }} +{{- end -}} diff --git a/charts/cosmic/templates/deployment.yaml b/charts/cosmic/templates/deployment.yaml index 885db35135f..99880ea3431 100644 --- a/charts/cosmic/templates/deployment.yaml +++ b/charts/cosmic/templates/deployment.yaml @@ -87,6 +87,10 @@ spec: resources: {{- toYaml . | nindent 12 }} {{- end }} + volumeMounts: + - name: config + subPath: config.yaml + mountPath: /opt/server/config.yaml initContainers: - name: mysql-isalive image: {{ include "mysql.image" .Subcharts.mysql }} @@ -116,6 +120,10 @@ spec: password_aux=$(cat "${MYSQL_PASSWORD_FILE}") fi mysqladmin ping -h ${DB_HOST} -P ${DB_PORT} -u"${MYSQL_USER}" -p"${password_aux}" | grep "mysqld is alive" + volumes: + - name: config + configMap: + name: {{ include "cosmic.fullname" . }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/charts/cosmic/values.yaml b/charts/cosmic/values.yaml index 74c19531893..d1d87438fbc 100644 --- a/charts/cosmic/values.yaml +++ b/charts/cosmic/values.yaml @@ -81,6 +81,8 @@ affinity: {} tolerations: [] cosmic: + # Contents for custom config file. + configuration: "" containerPorts: login: 8484 # Channels are exclusive of "end" values. From f12fac982e001332cca4e26ba431afb3f0986da2 Mon Sep 17 00:00:00 2001 From: HarkuLi Date: Sat, 9 May 2026 18:46:14 +0800 Subject: [PATCH 9/9] Add documentation for Helm chart --- README.md | 4 ++++ charts/cosmic/README.md | 48 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 charts/cosmic/README.md diff --git a/README.md b/README.md index ca2be7de9d6..bf1c7c27e19 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,10 @@ You must rebuild images after any code changes: docker compose build ``` +## Deploying to Kubernetes + +See [charts/cosmic](./charts/cosmic). + ## Local setup You can also run Cosmic on your actual machine. diff --git a/charts/cosmic/README.md b/charts/cosmic/README.md new file mode 100644 index 00000000000..e064a950c8a --- /dev/null +++ b/charts/cosmic/README.md @@ -0,0 +1,48 @@ +# Helm Chart for Cosmic + +## Quick Start + +1. Build the image for Cosmic and push it to your own image repository. + + ```bash + docker build -t : ../../ + docker image push : + ``` + +2. Fetch dependencies. + + ```bash + helm dependency build + ``` + +3. Create `values-override.yaml`. + + ```bash + cat < values-override.yaml + image: + repository: + tag: + EOF + ``` + +4. Install the Helm chart into your cluster. + + ``` + helm install cosmic-release . -f values-override.yaml + ``` + +## Custom Config + +1. Create a copy of `config.yaml` and edit it. + + ```bash + cp ../../config.yaml ./config.yaml + vim config.yaml + ``` + +2. Install the Helm chart with your custom config file. + + ```bash + helm install cosmic-release ../cosmic -f values-override.yaml \ + --set-file cosmic.configuration=./config.yaml + ```