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