mirror of
https://github.com/mainnika/nikita-tokarch-uk.git
synced 2026-06-19 18:44:57 +00:00
Merge branch 'main' into develop
This commit is contained in:
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
*
|
*
|
||||||
|
!cmd
|
||||||
!nginx
|
!nginx
|
||||||
!frontend
|
!pkg
|
||||||
!web
|
!web
|
||||||
!go.mod
|
!go.mod
|
||||||
!go.sum
|
!go.sum
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ name: Docker
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
branches: [ main ]
|
branches:
|
||||||
|
- 'main'
|
||||||
|
- 'develop'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
@@ -22,6 +24,11 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: '3.8'
|
||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
uses: sigstore/cosign-installer@main
|
uses: sigstore/cosign-installer@main
|
||||||
@@ -37,17 +44,38 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Get date as semver
|
||||||
|
id: calver
|
||||||
|
env:
|
||||||
|
REF_NAME: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
python -c "
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from os import environ
|
||||||
|
ref = environ['REF_NAME']
|
||||||
|
now = datetime.now()
|
||||||
|
date = now.strftime('%y.%m.%d')
|
||||||
|
delta = timedelta(hours=now.hour,minutes=now.minute,seconds=now.second)
|
||||||
|
print(f'::set-output name=current::{date}-{ref}.{delta.seconds}')
|
||||||
|
"
|
||||||
|
|
||||||
- name: Extract Docker metadata
|
- name: Extract Docker metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v3
|
uses: docker/metadata-action@v3
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=${{ steps.calver.outputs.current }}
|
||||||
|
type=raw,value=${{ github.ref_name }}
|
||||||
|
type=sha
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
id: build-and-push
|
id: build-and-push
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
build-args: |
|
||||||
|
APP_VERSION=${{ steps.calver.outputs.current }}
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|||||||
+3
-2
@@ -16,13 +16,14 @@ RUN --mount=type=cache,id=gopath,target=${GOPATH} \
|
|||||||
|
|
||||||
ARG APP_VERSION=containerized
|
ARG APP_VERSION=containerized
|
||||||
|
|
||||||
COPY frontend frontend
|
COPY cmd cmd
|
||||||
|
COPY pkg pkg
|
||||||
|
|
||||||
RUN --mount=type=cache,id=gopath,target=${GOPATH} \
|
RUN --mount=type=cache,id=gopath,target=${GOPATH} \
|
||||||
go build \
|
go build \
|
||||||
-o nikita-tokarch-uk-frontend \
|
-o nikita-tokarch-uk-frontend \
|
||||||
-ldflags "-X main.Version=${APP_VERSION}" \
|
-ldflags "-X main.Version=${APP_VERSION}" \
|
||||||
code.tokarch.uk/mainnika/nikita-tokarch-uk/frontend
|
code.tokarch.uk/mainnika/nikita-tokarch-uk/cmd/renderer
|
||||||
|
|
||||||
FROM registry.access.redhat.com/ubi8/ubi as js-builder
|
FROM registry.access.redhat.com/ubi8/ubi as js-builder
|
||||||
|
|
||||||
|
|||||||
@@ -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/
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: nikita-tokarch-uk
|
||||||
|
description: A Helm chart for Kubernetes
|
||||||
|
type: application
|
||||||
|
version: 0.1.0
|
||||||
|
appVersion: main
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
1. Get the application URL by running these commands:
|
||||||
|
{{- if .Values.ingress.enabled }}
|
||||||
|
{{- range $host := .Values.ingress.hosts }}
|
||||||
|
{{- range .paths }}
|
||||||
|
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- else if contains "NodePort" .Values.service.type }}
|
||||||
|
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "nikita-tokarch-uk.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 the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "nikita-tokarch-uk.fullname" . }}'
|
||||||
|
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "nikita-tokarch-uk.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}")
|
||||||
|
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||||
|
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||||
|
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "nikita-tokarch-uk.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 }}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{{/*
|
||||||
|
Expand the name of the chart.
|
||||||
|
*/}}
|
||||||
|
{{- define "nikita-tokarch-uk.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 "nikita-tokarch-uk.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 "nikita-tokarch-uk.chart" -}}
|
||||||
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Common labels
|
||||||
|
*/}}
|
||||||
|
{{- define "nikita-tokarch-uk.labels" -}}
|
||||||
|
helm.sh/chart: {{ include "nikita-tokarch-uk.chart" . }}
|
||||||
|
{{ include "nikita-tokarch-uk.selectorLabels" . }}
|
||||||
|
{{- if .Chart.AppVersion }}
|
||||||
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
|
{{- end }}
|
||||||
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Selector labels
|
||||||
|
*/}}
|
||||||
|
{{- define "nikita-tokarch-uk.selectorLabels" -}}
|
||||||
|
app.kubernetes.io/name: {{ include "nikita-tokarch-uk.name" . }}
|
||||||
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create the name of the service account to use
|
||||||
|
*/}}
|
||||||
|
{{- define "nikita-tokarch-uk.serviceAccountName" -}}
|
||||||
|
{{- if .Values.serviceAccount.create }}
|
||||||
|
{{- default (include "nikita-tokarch-uk.fullname" .) .Values.serviceAccount.name }}
|
||||||
|
{{- else }}
|
||||||
|
{{- default "default" .Values.serviceAccount.name }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{{- if .Values.config -}}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: "{{ include "nikita-tokarch-uk.fullname" . }}-config"
|
||||||
|
labels:
|
||||||
|
{{- include "nikita-tokarch-uk.labels" . | nindent 4 }}
|
||||||
|
data:
|
||||||
|
{{- toYaml .Values.config | nindent 2 }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "nikita-tokarch-uk.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "nikita-tokarch-uk.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
{{- include "nikita-tokarch-uk.selectorLabels" . | nindent 6 }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
checksum/config: {{ include ( print $.Template.BasePath "/config.yaml" ) . | sha256sum }}
|
||||||
|
{{- with .Values.podAnnotations }}
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
labels:
|
||||||
|
{{- include "nikita-tokarch-uk.selectorLabels" . | nindent 8 }}
|
||||||
|
spec:
|
||||||
|
{{- with .Values.imagePullSecrets }}
|
||||||
|
imagePullSecrets:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
serviceAccountName: {{ include "nikita-tokarch-uk.serviceAccountName" . }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.podSecurityContext | nindent 8 }}
|
||||||
|
containers:
|
||||||
|
- name: {{ .Chart.Name }}
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
command:
|
||||||
|
- /usr/sbin/nginx
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 80
|
||||||
|
protocol: TCP
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /version.html
|
||||||
|
port: http
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /version.html
|
||||||
|
port: http
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
|
- name: {{ .Chart.Name }}-renderer
|
||||||
|
securityContext:
|
||||||
|
{{- toYaml .Values.securityContext | nindent 12 }}
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
workingDir: /etc/nikita-tokarch-uk
|
||||||
|
command:
|
||||||
|
- /usr/local/bin/nikita-tokarch-uk-frontend
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
|
volumeMounts:
|
||||||
|
- name: "{{ include "nikita-tokarch-uk.fullname" . }}-config"
|
||||||
|
mountPath: /etc/nikita-tokarch-uk
|
||||||
|
readOnly: true
|
||||||
|
volumes:
|
||||||
|
- name: "{{ template "nikita-tokarch-uk.fullname" . }}-config"
|
||||||
|
{{- if .Values.config }}
|
||||||
|
configMap:
|
||||||
|
name: "{{ include "nikita-tokarch-uk.fullname" . }}-config"
|
||||||
|
{{- else }}
|
||||||
|
emptyDir: {}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.nodeSelector }}
|
||||||
|
nodeSelector:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.affinity }}
|
||||||
|
affinity:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- with .Values.tolerations }}
|
||||||
|
tolerations:
|
||||||
|
{{- toYaml . | nindent 8 }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
{{- if .Values.ingress.enabled -}}
|
||||||
|
{{- $fullName := include "nikita-tokarch-uk.fullname" . -}}
|
||||||
|
{{- $svcPort := .Values.service.port -}}
|
||||||
|
{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }}
|
||||||
|
{{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }}
|
||||||
|
{{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
|
||||||
|
apiVersion: networking.k8s.io/v1beta1
|
||||||
|
{{- else -}}
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
{{- end }}
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ $fullName }}
|
||||||
|
labels:
|
||||||
|
{{- include "nikita-tokarch-uk.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.ingress.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }}
|
||||||
|
ingressClassName: {{ .Values.ingress.className }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.ingress.tls }}
|
||||||
|
tls:
|
||||||
|
{{- range .Values.ingress.tls }}
|
||||||
|
- hosts:
|
||||||
|
{{- range .hosts }}
|
||||||
|
- {{ . | quote }}
|
||||||
|
{{- end }}
|
||||||
|
secretName: {{ .secretName }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
{{- range .Values.ingress.hosts }}
|
||||||
|
- host: {{ .host | quote }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
{{- range .paths }}
|
||||||
|
- path: {{ .path }}
|
||||||
|
{{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }}
|
||||||
|
pathType: {{ .pathType }}
|
||||||
|
{{- end }}
|
||||||
|
backend:
|
||||||
|
{{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }}
|
||||||
|
service:
|
||||||
|
name: {{ $fullName }}
|
||||||
|
port:
|
||||||
|
number: {{ $svcPort }}
|
||||||
|
{{- else }}
|
||||||
|
serviceName: {{ $fullName }}
|
||||||
|
servicePort: {{ $svcPort }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "nikita-tokarch-uk.fullname" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "nikita-tokarch-uk.labels" . | nindent 4 }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.service.port }}
|
||||||
|
targetPort: http
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
{{- include "nikita-tokarch-uk.selectorLabels" . | nindent 4 }}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{{- if .Values.serviceAccount.create -}}
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: {{ include "nikita-tokarch-uk.serviceAccountName" . }}
|
||||||
|
labels:
|
||||||
|
{{- include "nikita-tokarch-uk.labels" . | nindent 4 }}
|
||||||
|
{{- with .Values.serviceAccount.annotations }}
|
||||||
|
annotations:
|
||||||
|
{{- toYaml . | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: "{{ include "nikita-tokarch-uk.fullname" . }}-test-connection"
|
||||||
|
labels:
|
||||||
|
{{- include "nikita-tokarch-uk.labels" . | nindent 4 }}
|
||||||
|
annotations:
|
||||||
|
"helm.sh/hook": test
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: wget
|
||||||
|
image: busybox
|
||||||
|
command: ['wget']
|
||||||
|
args: ['{{ include "nikita-tokarch-uk.fullname" . }}:{{ .Values.service.port }}']
|
||||||
|
restartPolicy: Never
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
# Default values for nikita-tokarch-uk.
|
||||||
|
# This is a YAML-formatted file.
|
||||||
|
# Declare variables to be passed into your templates.
|
||||||
|
|
||||||
|
# config:
|
||||||
|
# frontend.yaml: |-
|
||||||
|
|
||||||
|
replicaCount: 1
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: ghcr.io/mainnika/nikita-tokarch-uk
|
||||||
|
pullPolicy: Always
|
||||||
|
# tag: "main"
|
||||||
|
|
||||||
|
imagePullSecrets: []
|
||||||
|
nameOverride: ""
|
||||||
|
fullnameOverride: ""
|
||||||
|
|
||||||
|
serviceAccount:
|
||||||
|
# Specifies whether a service account should be created
|
||||||
|
create: 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: ""
|
||||||
|
|
||||||
|
podAnnotations: {}
|
||||||
|
|
||||||
|
podSecurityContext: {}
|
||||||
|
# fsGroup: 2000
|
||||||
|
|
||||||
|
securityContext: {}
|
||||||
|
# capabilities:
|
||||||
|
# drop:
|
||||||
|
# - ALL
|
||||||
|
# readOnlyRootFilesystem: true
|
||||||
|
# runAsNonRoot: true
|
||||||
|
# runAsUser: 1000
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 80
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
className: "nginx"
|
||||||
|
annotations: {}
|
||||||
|
# kubernetes.io/ingress.class: nginx
|
||||||
|
# kubernetes.io/tls-acme: "true"
|
||||||
|
hosts:
|
||||||
|
- host: nikita.tokarch.uk
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
tls: []
|
||||||
|
# - secretName: chart-example-tls
|
||||||
|
# hosts:
|
||||||
|
# - chart-example.local
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
nodeSelector: {}
|
||||||
|
|
||||||
|
tolerations: []
|
||||||
|
|
||||||
|
affinity: {}
|
||||||
@@ -9,11 +9,10 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
|
|
||||||
"code.tokarch.uk/mainnika/nikita-tokarch-uk/frontend/config"
|
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/config"
|
||||||
"code.tokarch.uk/mainnika/nikita-tokarch-uk/frontend/ghost"
|
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost/v4api/httpclient"
|
||||||
"code.tokarch.uk/mainnika/nikita-tokarch-uk/frontend/renderer"
|
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/routes"
|
||||||
|
_ "code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/templates"
|
||||||
_ "code.tokarch.uk/mainnika/nikita-tokarch-uk/frontend/templates"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "nightly"
|
var Version = "nightly"
|
||||||
@@ -53,14 +52,15 @@ func main() {
|
|||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ghostClient := &ghost.HTTPClient{
|
ghostClient := &httpclient.HTTPClient{
|
||||||
Addr: config.Content.Backend,
|
Addr: config.Content.Backend.Addr,
|
||||||
|
Secured: config.Content.Backend.Secured,
|
||||||
|
Headers: config.Content.Backend.Headers,
|
||||||
ContentKey: config.Content.Key,
|
ContentKey: config.Content.Key,
|
||||||
Secured: true,
|
|
||||||
QueryTimeout: time.Second,
|
QueryTimeout: time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
rendererHandler := &renderer.Renderer{
|
apiRoutes := &routes.Routes{
|
||||||
GhostClient: ghostClient,
|
GhostClient: ghostClient,
|
||||||
ContentConfig: config.Content,
|
ContentConfig: config.Content,
|
||||||
Base: config.Base,
|
Base: config.Base,
|
||||||
@@ -68,7 +68,7 @@ func main() {
|
|||||||
|
|
||||||
httpServer := fasthttp.Server{
|
httpServer := fasthttp.Server{
|
||||||
Logger: logrus.StandardLogger(),
|
Logger: logrus.StandardLogger(),
|
||||||
Handler: rendererHandler.Handler,
|
Handler: apiRoutes.Handler,
|
||||||
Name: frontendServerIdentity,
|
Name: frontendServerIdentity,
|
||||||
GetOnly: true,
|
GetOnly: true,
|
||||||
}
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package ghost
|
|
||||||
|
|
||||||
//go:generate $GOPATH/bin/easyjson -pkg -no_std_marshalers
|
|
||||||
|
|
||||||
// Client is the ghost backend client
|
|
||||||
type Client interface {
|
|
||||||
GetPosts(params ...QueryParam) (posts *Posts, err error)
|
|
||||||
GetPostBySlug(slug string, params ...QueryParam) (posts *Posts, err error)
|
|
||||||
GetPageBySlug(slug string) (pages *Pages, err error)
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package ghost
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
|
||||||
)
|
|
||||||
|
|
||||||
// QueryParam is the generic query param applier
|
|
||||||
type QueryParam interface {
|
|
||||||
Apply(headers *fasthttp.RequestHeader, args *fasthttp.Args)
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryLimit returns limit param query
|
|
||||||
func QueryLimit(limit int) queryLimit {
|
|
||||||
return queryLimit{limit: limit}
|
|
||||||
}
|
|
||||||
|
|
||||||
// QueryPage returns page param query
|
|
||||||
func QueryPage(page int) queryPage {
|
|
||||||
return queryPage{page: page}
|
|
||||||
}
|
|
||||||
|
|
||||||
// queryLimit implements the limit param query applier
|
|
||||||
type queryLimit struct{ limit int }
|
|
||||||
|
|
||||||
// Apply applies the limit argument to the query
|
|
||||||
func (ql queryLimit) Apply(headers *fasthttp.RequestHeader, args *fasthttp.Args) {
|
|
||||||
|
|
||||||
if ql.limit == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
args.Add("limit", strconv.Itoa(ql.limit))
|
|
||||||
}
|
|
||||||
|
|
||||||
// queryPage implements the page param query applier
|
|
||||||
type queryPage struct{ page int }
|
|
||||||
|
|
||||||
// Apply applies the page argument to the query
|
|
||||||
func (qp queryPage) Apply(headers *fasthttp.RequestHeader, args *fasthttp.Args) {
|
|
||||||
|
|
||||||
if qp.page < 2 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
args.Add("page", strconv.Itoa(qp.page))
|
|
||||||
}
|
|
||||||
@@ -38,6 +38,10 @@ http {
|
|||||||
try_files $uri $uri/ @frontend;
|
try_files $uri $uri/ @frontend;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location = / {
|
||||||
|
proxy_pass http://frontend;
|
||||||
|
}
|
||||||
|
|
||||||
location @frontend {
|
location @frontend {
|
||||||
proxy_pass http://frontend;
|
proxy_pass http://frontend;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,19 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Backend contains backend connection-specific configuration
|
||||||
|
type Backend struct {
|
||||||
|
Addr string `mapstructure:"addr"`
|
||||||
|
Secured bool `mapstructure:"secured"`
|
||||||
|
Headers map[string]string `mapstructure:"headers"`
|
||||||
|
}
|
||||||
|
|
||||||
// Content contains content-specific configuration
|
// Content contains content-specific configuration
|
||||||
type Content struct {
|
type Content struct {
|
||||||
Backend string `mapstructure:"backend"`
|
Backend Backend `mapstructure:"backend"`
|
||||||
Key string `mapstructure:"key"`
|
Key string `mapstructure:"key"`
|
||||||
Pinned string `mapstructure:"pinned"`
|
Pinned string `mapstructure:"pinned"`
|
||||||
PostsPerPage int `mapstructure:"postsPerPage"`
|
PostsPerPage int `mapstructure:"postsPerPage"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config contains application configuration
|
// Config contains application configuration
|
||||||
@@ -36,7 +43,9 @@ func init() {
|
|||||||
pflag.String("unix", "", "unix socket path to listen")
|
pflag.String("unix", "", "unix socket path to listen")
|
||||||
pflag.String("base", "", "http URI prefix")
|
pflag.String("base", "", "http URI prefix")
|
||||||
|
|
||||||
pflag.String("content.backend", "demo.ghost.io:443", "ghost backend addr")
|
pflag.StringToString("content.backend.headers", nil, "map of additional headers to send")
|
||||||
|
pflag.String("content.backend.addr", "demo.ghost.io:443", "ghost backend addr")
|
||||||
|
pflag.Bool("content.backend.secured", true, "is ghost backend secured")
|
||||||
pflag.String("content.key", "22444f78447824223cefc48062", "ghost content api key")
|
pflag.String("content.key", "22444f78447824223cefc48062", "ghost content api key")
|
||||||
pflag.String("content.pinned", "contact", "pinned page slug")
|
pflag.String("content.pinned", "contact", "pinned page slug")
|
||||||
pflag.Int("content.postsPerPage", 5, "amount of posts per page")
|
pflag.Int("content.postsPerPage", 5, "amount of posts per page")
|
||||||
@@ -3,15 +3,15 @@ package content
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"code.tokarch.uk/mainnika/nikita-tokarch-uk/frontend/ghost"
|
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost/data"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Blog content data
|
// Blog content data
|
||||||
type Blog struct {
|
type Blog struct {
|
||||||
_ interface{} `template:"blog.go.tmpl"`
|
_ interface{} `template:"blog.go.tmpl"`
|
||||||
ghost.Meta
|
data.Meta
|
||||||
Pinned []ghost.Post
|
Pinned []data.Post
|
||||||
Posts []ghost.Post
|
Posts []data.Post
|
||||||
}
|
}
|
||||||
|
|
||||||
// Title returns blog content title
|
// Title returns blog content title
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
package content
|
package content
|
||||||
|
|
||||||
import (
|
import "code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost/data"
|
||||||
"code.tokarch.uk/mainnika/nikita-tokarch-uk/frontend/ghost"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Index content data
|
// Index content data
|
||||||
type Index struct {
|
type Index struct {
|
||||||
_ interface{} `template:"index.go.tmpl"`
|
_ interface{} `template:"index.go.tmpl"`
|
||||||
ghost.Meta
|
data.Meta
|
||||||
Pinned []ghost.Post
|
Pinned []data.Post
|
||||||
Posts []ghost.Post
|
Posts []data.Post
|
||||||
}
|
}
|
||||||
|
|
||||||
// Title returns index title
|
// Title returns index title
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
package ghost
|
package data
|
||||||
|
|
||||||
|
//go:generate $GOPATH/bin/easyjson -pkg -no_std_marshalers
|
||||||
|
|
||||||
import "html/template"
|
import "html/template"
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.
|
// Code generated by easyjson for marshaling/unmarshaling. DO NOT EDIT.
|
||||||
|
|
||||||
package ghost
|
package data
|
||||||
|
|
||||||
import (
|
import (
|
||||||
json "encoding/json"
|
json "encoding/json"
|
||||||
@@ -18,7 +18,7 @@ var (
|
|||||||
_ easyjson.Marshaler
|
_ easyjson.Marshaler
|
||||||
)
|
)
|
||||||
|
|
||||||
func easyjson72852e1bDecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost(in *jlexer.Lexer, out *Posts) {
|
func easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData(in *jlexer.Lexer, out *Posts) {
|
||||||
isTopLevel := in.IsStart()
|
isTopLevel := in.IsStart()
|
||||||
if in.IsNull() {
|
if in.IsNull() {
|
||||||
if isTopLevel {
|
if isTopLevel {
|
||||||
@@ -72,7 +72,7 @@ func easyjson72852e1bDecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost(in
|
|||||||
in.Consumed()
|
in.Consumed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func easyjson72852e1bEncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost(out *jwriter.Writer, in Posts) {
|
func easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData(out *jwriter.Writer, in Posts) {
|
||||||
out.RawByte('{')
|
out.RawByte('{')
|
||||||
first := true
|
first := true
|
||||||
_ = first
|
_ = first
|
||||||
@@ -102,14 +102,14 @@ func easyjson72852e1bEncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost(out
|
|||||||
|
|
||||||
// MarshalEasyJSON supports easyjson.Marshaler interface
|
// MarshalEasyJSON supports easyjson.Marshaler interface
|
||||||
func (v Posts) MarshalEasyJSON(w *jwriter.Writer) {
|
func (v Posts) MarshalEasyJSON(w *jwriter.Writer) {
|
||||||
easyjson72852e1bEncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost(w, v)
|
easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData(w, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
||||||
func (v *Posts) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
func (v *Posts) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||||
easyjson72852e1bDecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost(l, v)
|
easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData(l, v)
|
||||||
}
|
}
|
||||||
func easyjson72852e1bDecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost1(in *jlexer.Lexer, out *Post) {
|
func easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData1(in *jlexer.Lexer, out *Post) {
|
||||||
isTopLevel := in.IsStart()
|
isTopLevel := in.IsStart()
|
||||||
if in.IsNull() {
|
if in.IsNull() {
|
||||||
if isTopLevel {
|
if isTopLevel {
|
||||||
@@ -148,7 +148,7 @@ func easyjson72852e1bDecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost1(in
|
|||||||
in.Consumed()
|
in.Consumed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func easyjson72852e1bEncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost1(out *jwriter.Writer, in Post) {
|
func easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData1(out *jwriter.Writer, in Post) {
|
||||||
out.RawByte('{')
|
out.RawByte('{')
|
||||||
first := true
|
first := true
|
||||||
_ = first
|
_ = first
|
||||||
@@ -182,14 +182,14 @@ func easyjson72852e1bEncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost1(ou
|
|||||||
|
|
||||||
// MarshalEasyJSON supports easyjson.Marshaler interface
|
// MarshalEasyJSON supports easyjson.Marshaler interface
|
||||||
func (v Post) MarshalEasyJSON(w *jwriter.Writer) {
|
func (v Post) MarshalEasyJSON(w *jwriter.Writer) {
|
||||||
easyjson72852e1bEncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost1(w, v)
|
easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData1(w, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
||||||
func (v *Post) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
func (v *Post) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||||
easyjson72852e1bDecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost1(l, v)
|
easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData1(l, v)
|
||||||
}
|
}
|
||||||
func easyjson72852e1bDecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost2(in *jlexer.Lexer, out *Pagination) {
|
func easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData2(in *jlexer.Lexer, out *Pagination) {
|
||||||
isTopLevel := in.IsStart()
|
isTopLevel := in.IsStart()
|
||||||
if in.IsNull() {
|
if in.IsNull() {
|
||||||
if isTopLevel {
|
if isTopLevel {
|
||||||
@@ -226,7 +226,7 @@ func easyjson72852e1bDecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost2(in
|
|||||||
in.Consumed()
|
in.Consumed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func easyjson72852e1bEncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost2(out *jwriter.Writer, in Pagination) {
|
func easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData2(out *jwriter.Writer, in Pagination) {
|
||||||
out.RawByte('{')
|
out.RawByte('{')
|
||||||
first := true
|
first := true
|
||||||
_ = first
|
_ = first
|
||||||
@@ -255,14 +255,14 @@ func easyjson72852e1bEncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost2(ou
|
|||||||
|
|
||||||
// MarshalEasyJSON supports easyjson.Marshaler interface
|
// MarshalEasyJSON supports easyjson.Marshaler interface
|
||||||
func (v Pagination) MarshalEasyJSON(w *jwriter.Writer) {
|
func (v Pagination) MarshalEasyJSON(w *jwriter.Writer) {
|
||||||
easyjson72852e1bEncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost2(w, v)
|
easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData2(w, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
||||||
func (v *Pagination) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
func (v *Pagination) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||||
easyjson72852e1bDecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost2(l, v)
|
easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData2(l, v)
|
||||||
}
|
}
|
||||||
func easyjson72852e1bDecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost3(in *jlexer.Lexer, out *Pages) {
|
func easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData3(in *jlexer.Lexer, out *Pages) {
|
||||||
isTopLevel := in.IsStart()
|
isTopLevel := in.IsStart()
|
||||||
if in.IsNull() {
|
if in.IsNull() {
|
||||||
if isTopLevel {
|
if isTopLevel {
|
||||||
@@ -316,7 +316,7 @@ func easyjson72852e1bDecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost3(in
|
|||||||
in.Consumed()
|
in.Consumed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func easyjson72852e1bEncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost3(out *jwriter.Writer, in Pages) {
|
func easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData3(out *jwriter.Writer, in Pages) {
|
||||||
out.RawByte('{')
|
out.RawByte('{')
|
||||||
first := true
|
first := true
|
||||||
_ = first
|
_ = first
|
||||||
@@ -346,14 +346,14 @@ func easyjson72852e1bEncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost3(ou
|
|||||||
|
|
||||||
// MarshalEasyJSON supports easyjson.Marshaler interface
|
// MarshalEasyJSON supports easyjson.Marshaler interface
|
||||||
func (v Pages) MarshalEasyJSON(w *jwriter.Writer) {
|
func (v Pages) MarshalEasyJSON(w *jwriter.Writer) {
|
||||||
easyjson72852e1bEncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost3(w, v)
|
easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData3(w, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
||||||
func (v *Pages) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
func (v *Pages) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||||
easyjson72852e1bDecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost3(l, v)
|
easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData3(l, v)
|
||||||
}
|
}
|
||||||
func easyjson72852e1bDecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost4(in *jlexer.Lexer, out *Meta) {
|
func easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData4(in *jlexer.Lexer, out *Meta) {
|
||||||
isTopLevel := in.IsStart()
|
isTopLevel := in.IsStart()
|
||||||
if in.IsNull() {
|
if in.IsNull() {
|
||||||
if isTopLevel {
|
if isTopLevel {
|
||||||
@@ -384,7 +384,7 @@ func easyjson72852e1bDecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost4(in
|
|||||||
in.Consumed()
|
in.Consumed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func easyjson72852e1bEncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost4(out *jwriter.Writer, in Meta) {
|
func easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData4(out *jwriter.Writer, in Meta) {
|
||||||
out.RawByte('{')
|
out.RawByte('{')
|
||||||
first := true
|
first := true
|
||||||
_ = first
|
_ = first
|
||||||
@@ -398,10 +398,10 @@ func easyjson72852e1bEncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost4(ou
|
|||||||
|
|
||||||
// MarshalEasyJSON supports easyjson.Marshaler interface
|
// MarshalEasyJSON supports easyjson.Marshaler interface
|
||||||
func (v Meta) MarshalEasyJSON(w *jwriter.Writer) {
|
func (v Meta) MarshalEasyJSON(w *jwriter.Writer) {
|
||||||
easyjson72852e1bEncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost4(w, v)
|
easyjson794297d0EncodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData4(w, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
// UnmarshalEasyJSON supports easyjson.Unmarshaler interface
|
||||||
func (v *Meta) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
func (v *Meta) UnmarshalEasyJSON(l *jlexer.Lexer) {
|
||||||
easyjson72852e1bDecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhost4(l, v)
|
easyjson794297d0DecodeCodeTokarchUkMainnikaNikitaTokarchUkFrontendGhostData4(l, v)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package ghost
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost/data"
|
||||||
|
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost/params"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client is the ghost backend client
|
||||||
|
type Client interface {
|
||||||
|
// GetPosts returns blog posts according to query params
|
||||||
|
GetPosts(queryParams ...params.Modifier) (posts *data.Posts, err error)
|
||||||
|
// GetPostBySlug returns a single post by its slug title and query params
|
||||||
|
GetPostBySlug(slug string, queryParams ...params.Modifier) (posts *data.Posts, err error)
|
||||||
|
// GetPageBySlug returns a single page by its slug title and query params
|
||||||
|
GetPageBySlug(slug string, queryParams ...params.Modifier) (pages *data.Pages, err error)
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package params
|
||||||
|
|
||||||
|
// Params are generics query argument
|
||||||
|
type Params struct {
|
||||||
|
Limit int
|
||||||
|
Page int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modifier function takes params and makes some changes
|
||||||
|
type Modifier func(params Params) Params
|
||||||
|
|
||||||
|
// Modifiers is a list of modifier
|
||||||
|
type Modifiers []Modifier
|
||||||
|
|
||||||
|
// Apply function modifies params
|
||||||
|
func (ms Modifiers) Apply(params Params) Params {
|
||||||
|
|
||||||
|
for _, m := range ms {
|
||||||
|
params = m(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLimit modifier setups the limit
|
||||||
|
func WithLimit(limit int) Modifier {
|
||||||
|
return func(params Params) Params {
|
||||||
|
params.Limit = limit
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPage modifier setups the page
|
||||||
|
func WithPage(page int) Modifier {
|
||||||
|
return func(params Params) Params {
|
||||||
|
params.Page = page
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,20 @@
|
|||||||
package ghost
|
package httpclient
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mailru/easyjson"
|
"github.com/mailru/easyjson"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
|
|
||||||
|
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost"
|
||||||
|
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost/data"
|
||||||
|
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost/params"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Client = (*HTTPClient)(nil)
|
var _ ghost.Client = (*HTTPClient)(nil)
|
||||||
|
|
||||||
// Ghost content data URIs:
|
// Ghost content data URIs:
|
||||||
const (
|
const (
|
||||||
@@ -25,6 +30,7 @@ type HTTPClient struct {
|
|||||||
ContentKey string
|
ContentKey string
|
||||||
Addr string
|
Addr string
|
||||||
Secured bool
|
Secured bool
|
||||||
|
Headers map[string]string
|
||||||
|
|
||||||
client *fasthttp.HostClient
|
client *fasthttp.HostClient
|
||||||
|
|
||||||
@@ -44,7 +50,7 @@ func (g *HTTPClient) setupClient() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// doQuery does the method and unmarshals the result into the easyjson Unmarshaler
|
// doQuery does the method and unmarshals the result into the easyjson Unmarshaler
|
||||||
func (g *HTTPClient) doQuery(method string, v easyjson.Unmarshaler, params ...QueryParam) (err error) {
|
func (g *HTTPClient) doQuery(path string, v easyjson.Unmarshaler, params params.Params) (err error) {
|
||||||
|
|
||||||
g.setupClientOnce.Do(g.setupClient)
|
g.setupClientOnce.Do(g.setupClient)
|
||||||
|
|
||||||
@@ -55,17 +61,8 @@ func (g *HTTPClient) doQuery(method string, v easyjson.Unmarshaler, params ...Qu
|
|||||||
fasthttp.ReleaseRequest(req)
|
fasthttp.ReleaseRequest(req)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
uri := req.URI()
|
g.setupRequest(path, req)
|
||||||
uri.SetHost(g.Addr)
|
g.applyParams(params, req)
|
||||||
uri.SetPath(method)
|
|
||||||
uri.QueryArgs().Add("key", g.ContentKey)
|
|
||||||
if g.client.IsTLS {
|
|
||||||
uri.SetScheme("https")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, param := range params {
|
|
||||||
param.Apply(&req.Header, uri.QueryArgs())
|
|
||||||
}
|
|
||||||
|
|
||||||
err = g.client.DoTimeout(req, res, g.QueryTimeout)
|
err = g.client.DoTimeout(req, res, g.QueryTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -78,7 +75,6 @@ func (g *HTTPClient) doQuery(method string, v easyjson.Unmarshaler, params ...Qu
|
|||||||
resBytes := res.Body()
|
resBytes := res.Body()
|
||||||
if resBytes == nil && v == nil {
|
if resBytes == nil && v == nil {
|
||||||
return fmt.Errorf("nothing to unmarshal")
|
return fmt.Errorf("nothing to unmarshal")
|
||||||
|
|
||||||
}
|
}
|
||||||
if resBytes == nil {
|
if resBytes == nil {
|
||||||
return
|
return
|
||||||
@@ -89,13 +85,53 @@ func (g *HTTPClient) doQuery(method string, v easyjson.Unmarshaler, params ...Qu
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPageBySlug returns the only one page using slug filter
|
// setupRequest does the necessary initial configuration to the http request
|
||||||
func (g *HTTPClient) GetPageBySlug(slug string) (pages *Pages, err error) {
|
func (g *HTTPClient) setupRequest(path string, req *fasthttp.Request) {
|
||||||
|
|
||||||
pages = &Pages{}
|
uri := req.URI()
|
||||||
|
|
||||||
|
scheme := "http"
|
||||||
|
if g.Secured {
|
||||||
|
scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
uri.SetHost(g.Addr)
|
||||||
|
uri.SetPath(path)
|
||||||
|
uri.SetScheme(scheme)
|
||||||
|
|
||||||
|
uri.QueryArgs().Add("key", g.ContentKey)
|
||||||
|
|
||||||
|
for hKey, hValue := range g.Headers {
|
||||||
|
req.Header.Add(hKey, hValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyParams function additionally configure the http request using params
|
||||||
|
func (g *HTTPClient) applyParams(p params.Params, req *fasthttp.Request) (err error) {
|
||||||
|
|
||||||
|
uri := req.URI()
|
||||||
|
|
||||||
|
limit := p.Limit
|
||||||
|
if limit > 0 {
|
||||||
|
uri.QueryArgs().Add("limit", strconv.Itoa(limit))
|
||||||
|
}
|
||||||
|
|
||||||
|
page := p.Page
|
||||||
|
if page > 1 {
|
||||||
|
uri.QueryArgs().Add("page", strconv.Itoa(page))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPageBySlug returns the only one page using slug filter
|
||||||
|
func (g *HTTPClient) GetPageBySlug(slug string, queryModifiers ...params.Modifier) (pages *data.Pages, err error) {
|
||||||
|
|
||||||
|
pages = &data.Pages{}
|
||||||
|
defaultParams := params.Params{}
|
||||||
method := fmt.Sprintf(ghostAPIGetPageBySlug, slug)
|
method := fmt.Sprintf(ghostAPIGetPageBySlug, slug)
|
||||||
|
|
||||||
err = g.doQuery(method, pages)
|
err = g.doQuery(method, pages, defaultParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pages = nil
|
pages = nil
|
||||||
}
|
}
|
||||||
@@ -104,10 +140,13 @@ func (g *HTTPClient) GetPageBySlug(slug string) (pages *Pages, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetPosts returns posts
|
// GetPosts returns posts
|
||||||
func (g *HTTPClient) GetPosts(params ...QueryParam) (posts *Posts, err error) {
|
func (g *HTTPClient) GetPosts(queryModifiers ...params.Modifier) (posts *data.Posts, err error) {
|
||||||
|
|
||||||
posts = &Posts{}
|
posts = &data.Posts{}
|
||||||
err = g.doQuery(ghostAPIGetPosts, posts, params...)
|
defaultParams := params.Params{}
|
||||||
|
combinedParams := params.Modifiers(queryModifiers).Apply(defaultParams)
|
||||||
|
|
||||||
|
err = g.doQuery(ghostAPIGetPosts, posts, combinedParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
posts = nil
|
posts = nil
|
||||||
}
|
}
|
||||||
@@ -116,12 +155,14 @@ func (g *HTTPClient) GetPosts(params ...QueryParam) (posts *Posts, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetPostBySlug returns the only one post using slug filter
|
// GetPostBySlug returns the only one post using slug filter
|
||||||
func (g *HTTPClient) GetPostBySlug(slug string, params ...QueryParam) (posts *Posts, err error) {
|
func (g *HTTPClient) GetPostBySlug(slug string, queryModifiers ...params.Modifier) (posts *data.Posts, err error) {
|
||||||
|
|
||||||
posts = &Posts{}
|
posts = &data.Posts{}
|
||||||
|
defaultParams := params.Params{}
|
||||||
|
combinedParams := params.Modifiers(queryModifiers).Apply(defaultParams)
|
||||||
method := fmt.Sprintf(ghostAPIGetPostBySlug, slug)
|
method := fmt.Sprintf(ghostAPIGetPostBySlug, slug)
|
||||||
|
|
||||||
err = g.doQuery(method, posts, params...)
|
err = g.doQuery(method, posts, combinedParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
posts = nil
|
posts = nil
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
package renderer
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.tokarch.uk/mainnika/nikita-tokarch-uk/frontend/content"
|
|
||||||
"code.tokarch.uk/mainnika/nikita-tokarch-uk/frontend/ghost"
|
|
||||||
routing "github.com/jackwhelpton/fasthttp-routing/v2"
|
routing "github.com/jackwhelpton/fasthttp-routing/v2"
|
||||||
|
|
||||||
|
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/content"
|
||||||
|
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost/params"
|
||||||
)
|
)
|
||||||
|
|
||||||
// blog handler renders blog data
|
// blog handler renders blog data
|
||||||
func (r *Renderer) blog(c *routing.Context) (err error) {
|
func (r *Routes) blog(c *routing.Context) (err error) {
|
||||||
|
|
||||||
postsPerPage := r.ContentConfig.PostsPerPage
|
postsPerPage := r.ContentConfig.PostsPerPage
|
||||||
currentPage := c.QueryArgs().GetUintOrZero("page")
|
currentPage := c.QueryArgs().GetUintOrZero("page")
|
||||||
|
|
||||||
latestPosts, err := r.GhostClient.GetPosts(ghost.QueryLimit(postsPerPage), ghost.QueryPage(currentPage))
|
latestPosts, err := r.GhostClient.GetPosts(
|
||||||
|
params.WithLimit(postsPerPage),
|
||||||
|
params.WithPage(currentPage),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
package renderer
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"code.tokarch.uk/mainnika/nikita-tokarch-uk/frontend/content"
|
|
||||||
routing "github.com/jackwhelpton/fasthttp-routing/v2"
|
routing "github.com/jackwhelpton/fasthttp-routing/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/content"
|
||||||
)
|
)
|
||||||
|
|
||||||
// errorNotFound renders http error-404 template
|
// errorNotFound renders http error-404 template
|
||||||
func (r *Renderer) errorNotFound(c *routing.Context) (err error) {
|
func (r *Routes) errorNotFound(c *routing.Context) (err error) {
|
||||||
|
|
||||||
errorContent := content.Error{Message: "not found"}
|
errorContent := content.Error{Message: "not found"}
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ func (r *Renderer) errorNotFound(c *routing.Context) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// useErrorHandler is the middleware that catch handlers errors and render error template
|
// useErrorHandler is the middleware that catch handlers errors and render error template
|
||||||
func (r *Renderer) useErrorHandler(c *routing.Context) (err error) {
|
func (r *Routes) useErrorHandler(c *routing.Context) (err error) {
|
||||||
|
|
||||||
worker := func() (err error) {
|
worker := func() (err error) {
|
||||||
|
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
package renderer
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"code.tokarch.uk/mainnika/nikita-tokarch-uk/frontend/content"
|
|
||||||
"code.tokarch.uk/mainnika/nikita-tokarch-uk/frontend/ghost"
|
|
||||||
"code.tokarch.uk/mainnika/nikita-tokarch-uk/frontend/templates"
|
|
||||||
routing "github.com/jackwhelpton/fasthttp-routing/v2"
|
routing "github.com/jackwhelpton/fasthttp-routing/v2"
|
||||||
|
|
||||||
|
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/content"
|
||||||
|
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost/params"
|
||||||
|
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
// rootRedirect redirects the root url to the index using http redirect
|
// rootRedirect redirects the root url to the index using http redirect
|
||||||
func (r *Renderer) rootRedirect(c *routing.Context) (err error) {
|
func (r *Routes) rootRedirect(c *routing.Context) (err error) {
|
||||||
|
|
||||||
c.Redirect(templates.URLIndex, http.StatusFound)
|
c.Redirect(templates.URLIndex, http.StatusFound)
|
||||||
|
|
||||||
@@ -18,7 +19,7 @@ func (r *Renderer) rootRedirect(c *routing.Context) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// index handler renders index data
|
// index handler renders index data
|
||||||
func (r *Renderer) index(c *routing.Context) (err error) {
|
func (r *Routes) index(c *routing.Context) (err error) {
|
||||||
|
|
||||||
pinnedPageSlug := r.ContentConfig.Pinned
|
pinnedPageSlug := r.ContentConfig.Pinned
|
||||||
postsPerPage := r.ContentConfig.PostsPerPage
|
postsPerPage := r.ContentConfig.PostsPerPage
|
||||||
@@ -28,7 +29,7 @@ func (r *Renderer) index(c *routing.Context) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
latestPosts, err := r.GhostClient.GetPosts(ghost.QueryLimit(postsPerPage))
|
latestPosts, err := r.GhostClient.GetPosts(params.WithLimit(postsPerPage))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
package renderer
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"code.tokarch.uk/mainnika/nikita-tokarch-uk/frontend/templates"
|
|
||||||
routing "github.com/jackwhelpton/fasthttp-routing/v2"
|
routing "github.com/jackwhelpton/fasthttp-routing/v2"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
|
|
||||||
|
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ routing.DataWriter = (*TemplateWriter)(nil)
|
var _ routing.DataWriter = (*TemplateWriter)(nil)
|
||||||
@@ -30,7 +31,7 @@ func (tw *TemplateWriter) Write(w io.Writer, content interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// useTemplateWriter is the routing middleware to set the default data writer
|
// useTemplateWriter is the routing middleware to set the default data writer
|
||||||
func (r *Renderer) useTemplateWriter(c *routing.Context) (err error) {
|
func (r *Routes) useTemplateWriter(c *routing.Context) (err error) {
|
||||||
|
|
||||||
c.SetDataWriter(staticWriter)
|
c.SetDataWriter(staticWriter)
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package renderer
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
@@ -6,13 +6,13 @@ import (
|
|||||||
routing "github.com/jackwhelpton/fasthttp-routing/v2"
|
routing "github.com/jackwhelpton/fasthttp-routing/v2"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
|
|
||||||
"code.tokarch.uk/mainnika/nikita-tokarch-uk/frontend/config"
|
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/config"
|
||||||
"code.tokarch.uk/mainnika/nikita-tokarch-uk/frontend/ghost"
|
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/ghost"
|
||||||
"code.tokarch.uk/mainnika/nikita-tokarch-uk/frontend/templates"
|
"code.tokarch.uk/mainnika/nikita-tokarch-uk/pkg/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Renderer is the main handler that contains all routes handlers
|
// Routes is the main handler that contains all routes handlers
|
||||||
type Renderer struct {
|
type Routes struct {
|
||||||
GhostClient ghost.Client
|
GhostClient ghost.Client
|
||||||
ContentConfig config.Content
|
ContentConfig config.Content
|
||||||
|
|
||||||
@@ -25,13 +25,13 @@ type Renderer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handler invokes the lazy once-initializer and then does the request
|
// Handler invokes the lazy once-initializer and then does the request
|
||||||
func (r *Renderer) Handler(ctx *fasthttp.RequestCtx) {
|
func (r *Routes) Handler(ctx *fasthttp.RequestCtx) {
|
||||||
r.initOnce.Do(r.init)
|
r.initOnce.Do(r.init)
|
||||||
r.handler(ctx)
|
r.handler(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// init has the renderer initialization
|
// init has the renderer initialization
|
||||||
func (r *Renderer) init() {
|
func (r *Routes) init() {
|
||||||
|
|
||||||
router := routing.New()
|
router := routing.New()
|
||||||
|
|
||||||
@@ -2,6 +2,7 @@ package templates
|
|||||||
|
|
||||||
import "html/template"
|
import "html/template"
|
||||||
|
|
||||||
|
// UseFuncs returns a func map with template helpers functions
|
||||||
func UseFuncs() template.FuncMap {
|
func UseFuncs() template.FuncMap {
|
||||||
return template.FuncMap{
|
return template.FuncMap{
|
||||||
"add": func(i int) int {
|
"add": func(i int) int {
|
||||||
Reference in New Issue
Block a user