Init the ddns service, create app, aws and ip modules
All checks were successful
Build and deploy / deploy (push) Successful in 41s
All checks were successful
Build and deploy / deploy (push) Successful in 41s
This commit is contained in:
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.log
|
||||||
|
*.md
|
||||||
|
.git
|
||||||
25
.eslintrc.js
Normal file
25
.eslintrc.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
};
|
||||||
36
.gitea/workflows/deploy.yaml
Normal file
36
.gitea/workflows/deploy.yaml
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: Build and deploy
|
||||||
|
run-name: Build and deploy
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
github-server-url: 'https://gitea.home.joemonk.co.uk'
|
||||||
|
-
|
||||||
|
name: Set up docker
|
||||||
|
run: 'curl -fsSL https://get.docker.com | sh'
|
||||||
|
-
|
||||||
|
name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
-
|
||||||
|
name: Login to private registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: 'gitea.home.joemonk.co.uk/${{ github.repository }}'
|
||||||
|
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
-
|
||||||
|
name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: 'gitea.home.joemonk.co.uk/${{ gitea.repository }}:latest'
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.env
|
||||||
4
.prettierrc
Normal file
4
.prettierrc
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"editor.tabSize": 2,
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
}
|
||||||
23
Dockerfile
Normal file
23
Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Use the official Node.js image as the base image
|
||||||
|
FROM node:22-alpine
|
||||||
|
|
||||||
|
# Set the working directory inside the container
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Copy package.json and package-lock.json to the working directory
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install the application dependencies
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
# Copy the rest of the application files
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the NestJS application
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Expose the application port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Command to run the application
|
||||||
|
CMD ["node", "dist/main"]
|
||||||
72
README.md
Normal file
72
README.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
## Description
|
||||||
|
|
||||||
|
A nestjs service that grabs your ip from https://ifconfig.co and updates a record in route 53 to point to that.
|
||||||
|
Effectively aws dynamic dns as a docker.
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Env vars
|
||||||
|
|
||||||
|
You can use a `.env` file for local dev
|
||||||
|
|
||||||
|
```
|
||||||
|
# The keys from the IAM user below
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
PORT=3000
|
||||||
|
CRON=0 0 */4 * * *
|
||||||
|
# The record name is your hostname, i.e. subdomain.domain.co.uk
|
||||||
|
RECORD_NAME=
|
||||||
|
# From hosted zone details
|
||||||
|
HOSTED_ZONE_ID=
|
||||||
|
```
|
||||||
|
|
||||||
|
### AWS
|
||||||
|
|
||||||
|
Create a new IAM user and attach a policy to change the records in your zone:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": ["route53:ChangeResourceRecordSets"],
|
||||||
|
"Resource": "arn:aws:route53:::hostedzone/HOSTED_ZONE_ID"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a new access key and use the credentials in the env vars to allow access.
|
||||||
|
|
||||||
|
## Compile and run the project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# development
|
||||||
|
$ npm run start
|
||||||
|
|
||||||
|
# watch mode
|
||||||
|
$ npm run start:dev
|
||||||
|
|
||||||
|
# production mode
|
||||||
|
$ npm run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
### ip module
|
||||||
|
|
||||||
|
This module is just to send provide your current ip, whether this is by asking your router, or using a service like
|
||||||
|
|
||||||
|
### aws module
|
||||||
|
|
||||||
|
This connects to aws and updates the record name to the ip passed in
|
||||||
|
|
||||||
|
## app module
|
||||||
|
|
||||||
|
Connects everything together
|
||||||
9
nest-cli.json
Normal file
9
nest-cli.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true,
|
||||||
|
"typeCheck": true
|
||||||
|
}
|
||||||
|
}
|
||||||
10584
package-lock.json
generated
Normal file
10584
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
74
package.json
Normal file
74
package.json
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"name": "ddns",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@aws-sdk/client-route-53": "^3.687.0",
|
||||||
|
"@nestjs/axios": "^3.1.2",
|
||||||
|
"@nestjs/common": "^10.0.0",
|
||||||
|
"@nestjs/config": "^3.3.0",
|
||||||
|
"@nestjs/core": "^10.0.0",
|
||||||
|
"@nestjs/platform-express": "^10.0.0",
|
||||||
|
"@nestjs/schedule": "^4.1.1",
|
||||||
|
"joi": "^17.13.3",
|
||||||
|
"reflect-metadata": "^0.2.0",
|
||||||
|
"rxjs": "^7.8.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^10.0.0",
|
||||||
|
"@nestjs/schematics": "^10.0.0",
|
||||||
|
"@nestjs/testing": "^10.0.0",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^29.5.2",
|
||||||
|
"@types/node": "^20.3.1",
|
||||||
|
"@types/supertest": "^6.0.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||||
|
"@typescript-eslint/parser": "^8.0.0",
|
||||||
|
"eslint": "^8.0.0",
|
||||||
|
"eslint-config-prettier": "^9.0.0",
|
||||||
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
|
"jest": "^29.5.0",
|
||||||
|
"prettier": "^3.0.0",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.1.0",
|
||||||
|
"ts-loader": "^9.4.3",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.1.3"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/app/app.controller.ts
Normal file
12
src/app/app.controller.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class AppController {
|
||||||
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
@Get('/update')
|
||||||
|
async update(): Promise<void> {
|
||||||
|
return this.appService.updateR53();
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/app/app.module.ts
Normal file
24
src/app/app.module.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
|
import { configuration, validationSchema } from '../config/configuration';
|
||||||
|
import { IpModule } from '../ip/ip.module';
|
||||||
|
import { AwsModule } from 'src/aws/aws.module';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
validationSchema,
|
||||||
|
isGlobal: true,
|
||||||
|
load: [configuration],
|
||||||
|
}),
|
||||||
|
ScheduleModule.forRoot(),
|
||||||
|
IpModule,
|
||||||
|
AwsModule,
|
||||||
|
],
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
37
src/app/app.service.ts
Normal file
37
src/app/app.service.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||||
|
import { CronJob } from 'cron';
|
||||||
|
import { AwsService } from 'src/aws/aws.service';
|
||||||
|
import { IpService } from 'src/ip/ip.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly schedulerRegistry: SchedulerRegistry,
|
||||||
|
private readonly awsService: AwsService,
|
||||||
|
private readonly ipService: IpService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
onModuleInit() {
|
||||||
|
Logger.log(
|
||||||
|
`Creating R53 update cronjob with: ${this.configService.get('cron')}`,
|
||||||
|
);
|
||||||
|
const job = new CronJob(this.configService.get('cron'), () => {
|
||||||
|
Logger.log('Running R53 update from cron');
|
||||||
|
this.updateR53();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.schedulerRegistry.addCronJob('UpdateR53', job);
|
||||||
|
job.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateR53() {
|
||||||
|
Logger.log('Getting IP');
|
||||||
|
const ip = await this.ipService.getIp();
|
||||||
|
Logger.log('Updating R53');
|
||||||
|
this.awsService.updateR53(ip);
|
||||||
|
Logger.log('IP updated in R53');
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/aws/aws.module.ts
Normal file
9
src/aws/aws.module.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AwsService } from './aws.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [],
|
||||||
|
providers: [AwsService],
|
||||||
|
exports: [AwsService],
|
||||||
|
})
|
||||||
|
export class AwsModule {}
|
||||||
48
src/aws/aws.service.ts
Normal file
48
src/aws/aws.service.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import {
|
||||||
|
ChangeAction,
|
||||||
|
ChangeResourceRecordSetsCommand,
|
||||||
|
Route53Client,
|
||||||
|
RRType,
|
||||||
|
} from '@aws-sdk/client-route-53';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AwsService {
|
||||||
|
constructor(private readonly configService: ConfigService) {}
|
||||||
|
private readonly route53Client = new Route53Client({
|
||||||
|
region: this.configService.get('aws.region'),
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: this.configService.get('aws.accessKeyId'),
|
||||||
|
secretAccessKey: this.configService.get('aws.secretAccessKey'),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
async updateR53(ip: string): Promise<void> {
|
||||||
|
const record = this.configService.get('recordName');
|
||||||
|
const hostedZone = this.configService.get('hostedZoneId');
|
||||||
|
Logger.log(`Updating ${record} with ${ip}`);
|
||||||
|
const input = {
|
||||||
|
ChangeBatch: {
|
||||||
|
Changes: [
|
||||||
|
{
|
||||||
|
Action: ChangeAction.UPSERT,
|
||||||
|
ResourceRecordSet: {
|
||||||
|
Name: record,
|
||||||
|
ResourceRecords: [
|
||||||
|
{
|
||||||
|
Value: ip,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
TTL: 300,
|
||||||
|
Type: RRType.A,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
HostedZoneId: hostedZone,
|
||||||
|
};
|
||||||
|
const command = new ChangeResourceRecordSetsCommand(input);
|
||||||
|
this.route53Client.send(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/config/configuration.ts
Normal file
25
src/config/configuration.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import * as Joi from 'joi';
|
||||||
|
|
||||||
|
export function configuration() {
|
||||||
|
return {
|
||||||
|
aws: {
|
||||||
|
region: process.env.AWS_REGION,
|
||||||
|
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
|
||||||
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
||||||
|
},
|
||||||
|
port: process.env.PORT,
|
||||||
|
cron: process.env.CRON,
|
||||||
|
recordName: process.env.RECORD_NAME,
|
||||||
|
hostedZoneId: process.env.HOSTED_ZONE_ID,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validationSchema = Joi.object({
|
||||||
|
AWS_REGION: Joi.string().default('eu-west-2'),
|
||||||
|
AWS_ACCESS_KEY_ID: Joi.string().required(),
|
||||||
|
AWS_SECRET_ACCESS_KEY: Joi.string().required(),
|
||||||
|
PORT: Joi.number().port().default(3000),
|
||||||
|
CRON: Joi.string().required(),
|
||||||
|
RECORD_NAME: Joi.string().required(),
|
||||||
|
HOSTED_ZONE_ID: Joi.string().required(),
|
||||||
|
});
|
||||||
10
src/ip/ip.module.ts
Normal file
10
src/ip/ip.module.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { IpService } from './ip.service';
|
||||||
|
import { HttpModule } from '@nestjs/axios';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [HttpModule],
|
||||||
|
providers: [IpService],
|
||||||
|
exports: [IpService],
|
||||||
|
})
|
||||||
|
export class IpModule {}
|
||||||
22
src/ip/ip.service.ts
Normal file
22
src/ip/ip.service.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class IpService {
|
||||||
|
constructor(private readonly httpService: HttpService) {}
|
||||||
|
|
||||||
|
async getIp(): Promise<string> {
|
||||||
|
Logger.log('Getting IP from https://ifconfig.co');
|
||||||
|
const { data } = await firstValueFrom(
|
||||||
|
this.httpService.get<string>(`https://ifconfig.co/`, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'text/plain',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const ip = data.trim();
|
||||||
|
Logger.log(`IP: ${ip}`);
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/main.ts
Normal file
10
src/main.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { AppModule } from './app/app.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
const configService = app.get(ConfigService);
|
||||||
|
await app.listen(configService.get('port'));
|
||||||
|
}
|
||||||
|
bootstrap();
|
||||||
4
tsconfig.build.json
Normal file
4
tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
21
tsconfig.json
Normal file
21
tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2021",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"forceConsistentCasingInFileNames": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user