diff --git a/.github/workflows/chart.yaml b/.github/workflows/chart.yaml new file mode 100644 index 0000000..a231dc5 --- /dev/null +++ b/.github/workflows/chart.yaml @@ -0,0 +1,31 @@ +name: Release Charts + +on: + push: + branches: + - main + +jobs: + release: + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - + name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + - + name: Install Helm + uses: azure/setup-helm@v3 + - + name: Run chart-releaser + uses: helm/chart-releaser-action@v1.6.0 + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 0000000..6254a59 --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,46 @@ +name: ci + +on: + push: + branches: + - 'master' + tags: + - 'v*' + pull_request: + branches: + - 'master' + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + - + name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: | + gdurandvadas/air-helm + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + - + name: Login to DockerHub + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - + name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6ce80b4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Secrets +.env.** diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..067a605 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM golang:1.22-alpine + +WORKDIR /app + +RUN go install github.com/cosmtrek/air@latest + +CMD ["air", "-c", "/etc/air/air.toml"] diff --git a/README.md b/README.md index 084f461..5376890 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ -# air-helm -Local development environment built for Go and Kubernetes projects. +# Air Helm + +Welcome to Air Helm, a tool crafted to support the development of Go applications within Kubernetes environments. + +Modern API servers often require a complex assembly of infrastructure components, and Air Helm is designed to navigate this complexity from your local setup. By integrating Air for live reloading with a Helm chart for local Kubernetes deployment, Air Helm accelerates the development experience. This synergy enables developers to work on their Go applications and accurately define and interact with the necessary architecture components. diff --git a/charts/air-helm/.helmignore b/charts/air-helm/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/charts/air-helm/.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/air-helm/Chart.yaml b/charts/air-helm/Chart.yaml new file mode 100644 index 0000000..d0402b4 --- /dev/null +++ b/charts/air-helm/Chart.yaml @@ -0,0 +1,15 @@ +apiVersion: v2 +name: air-helm +description: Air Helm enhances Go development in Kubernetes by leveraging Air for live reloading and Helm for deployment. +type: application +version: 0.1.0 +appVersion: "1.16.0" +keywords: + - go + - golang + - development + - kubernetes + - helm + - live-reloading +sources: + - https://github.com/gdurandvadas/air-helm diff --git a/charts/air-helm/templates/NOTES.txt b/charts/air-helm/templates/NOTES.txt new file mode 100644 index 0000000..1b755ad --- /dev/null +++ b/charts/air-helm/templates/NOTES.txt @@ -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 "air-helm.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 "air-helm.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "air-helm.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 "air-helm.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/air-helm/templates/_helpers.tpl b/charts/air-helm/templates/_helpers.tpl new file mode 100644 index 0000000..428f1b0 --- /dev/null +++ b/charts/air-helm/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "air-helm.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 "air-helm.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 "air-helm.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "air-helm.labels" -}} +helm.sh/chart: {{ include "air-helm.chart" . }} +{{ include "air-helm.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "air-helm.selectorLabels" -}} +app.kubernetes.io/name: {{ include "air-helm.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "air-helm.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "air-helm.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/charts/air-helm/templates/configmap.yaml b/charts/air-helm/templates/configmap.yaml new file mode 100644 index 0000000..03b3381 --- /dev/null +++ b/charts/air-helm/templates/configmap.yaml @@ -0,0 +1,56 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "air-helm.fullname" . }}-config + labels: + {{- include "air-helm.labels" . | nindent 4 }} +data: + air.toml: |- + {{- if .Values.airServer.config }} + {{ .Values.airServer.config | indent 4 }} + {{- else }} + root = "/app" + testdata_dir = "testdata" + tmp_dir = "tmp" + + [build] + args_bin = [] + bin = "/app/tmp/{{ .Release.Name }}" + cmd = "go -C /app/source/{{ .Values.airServer.code.modulePath }} build -o /app/tmp/{{ .Release.Name }} {{ .Values.airServer.code.buildSource }}" + delay = 0 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + + [color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + + [log] + main_only = false + time = false + + [misc] + clean_on_exit = false + + [screen] + clear_on_rebuild = false + keep_scroll = true + {{- end }} diff --git a/charts/air-helm/templates/deployment.yaml b/charts/air-helm/templates/deployment.yaml new file mode 100644 index 0000000..2411854 --- /dev/null +++ b/charts/air-helm/templates/deployment.yaml @@ -0,0 +1,85 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "air-helm.fullname" . }} + labels: + {{- include "air-helm.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "air-helm.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "air-helm.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "air-helm.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 }} + {{- if or .Values.service.http.enabled .Values.service.grpc.enabled }} + ports: + {{- if .Values.service.http.enabled }} + - name: http + containerPort: {{ .Values.service.http.port }} + protocol: TCP + {{- end }} + {{- if .Values.service.grpc.enabled }} + - name: grpc + containerPort: {{ .Values.service.grpc.port }} + protocol: TCP + {{- end }} + {{- end }} + {{- if .Values.probes.enabled }} + livenessProbe: {{ toYaml .Values.probes.liveness | nindent 12 }} + readinessProbe: {{ toYaml .Values.probes.readiness | nindent 12 }} + {{- end }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: source-code + mountPath: /app/source + - name: air-config + mountPath: /etc/air + readOnly: true + - name: sandbox-tmp + mountPath: /app/tmp + volumes: + - name: source-code + hostPath: + path: {{ .Values.airServer.code.directory }} + type: Directory + - name: sandbox-tmp + emptyDir: {} + - name: air-config + configMap: + name: {{ include "air-helm.fullname" . }}-config + {{- 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/air-helm/templates/hpa.yaml b/charts/air-helm/templates/hpa.yaml new file mode 100644 index 0000000..d7d28a7 --- /dev/null +++ b/charts/air-helm/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "air-helm.fullname" . }} + labels: + {{- include "air-helm.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "air-helm.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/charts/air-helm/templates/ingress.yaml b/charts/air-helm/templates/ingress.yaml new file mode 100644 index 0000000..8f49e45 --- /dev/null +++ b/charts/air-helm/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "air-helm.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 "air-helm.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 }} diff --git a/charts/air-helm/templates/service.yaml b/charts/air-helm/templates/service.yaml new file mode 100644 index 0000000..127cd56 --- /dev/null +++ b/charts/air-helm/templates/service.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "air-helm.fullname" . }} + labels: + {{- include "air-helm.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + {{- if .Values.service.http.enabled }} + - name: http + port: {{ .Values.service.http.port }} + targetPort: http + protocol: TCP + {{- end }} + {{- if .Values.service.grpc.enabled }} + - name: grpc + port: {{ .Values.service.grpc.port }} + targetPort: grpc + protocol: TCP + {{- end }} + selector: + {{- include "air-helm.selectorLabels" . | nindent 4 }} diff --git a/charts/air-helm/templates/serviceaccount.yaml b/charts/air-helm/templates/serviceaccount.yaml new file mode 100644 index 0000000..1d8a005 --- /dev/null +++ b/charts/air-helm/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "air-helm.serviceAccountName" . }} + labels: + {{- include "air-helm.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/air-helm/values.schema.json b/charts/air-helm/values.schema.json new file mode 100644 index 0000000..32782ab --- /dev/null +++ b/charts/air-helm/values.schema.json @@ -0,0 +1,199 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "affinity": { + "properties": {}, + "type": "object" + }, + "airServer": { + "properties": { + "code": { + "properties": { + "buildSource": { + "type": "string" + }, + "directory": { + "type": "string" + }, + "modulePath": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "autoscaling": { + "properties": { + "enabled": { + "type": "boolean" + }, + "maxReplicas": { + "type": "integer" + }, + "minReplicas": { + "type": "integer" + }, + "targetCPUUtilizationPercentage": { + "type": "integer" + } + }, + "type": "object" + }, + "fullnameOverride": { + "type": "string" + }, + "image": { + "properties": { + "pullPolicy": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + } + }, + "type": "object" + }, + "imagePullSecrets": { + "type": "array" + }, + "ingress": { + "properties": { + "annotations": { + "properties": {}, + "type": "object" + }, + "className": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "hosts": { + "items": { + "properties": { + "host": { + "type": "string" + }, + "paths": { + "items": { + "properties": { + "path": { + "type": "string" + }, + "pathType": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + } + }, + "type": "object" + }, + "type": "array" + }, + "tls": { + "type": "array" + } + }, + "type": "object" + }, + "nameOverride": { + "type": "string" + }, + "nodeSelector": { + "properties": {}, + "type": "object" + }, + "podAnnotations": { + "properties": {}, + "type": "object" + }, + "podSecurityContext": { + "properties": {}, + "type": "object" + }, + "probes": { + "properties": { + "enabled": { + "type": "boolean" + }, + "liveness": { + "properties": {}, + "type": "object" + }, + "readiness": { + "properties": {}, + "type": "object" + } + }, + "type": "object" + }, + "replicaCount": { + "type": "integer" + }, + "resources": { + "properties": {}, + "type": "object" + }, + "securityContext": { + "properties": {}, + "type": "object" + }, + "service": { + "properties": { + "grpc": { + "properties": { + "enabled": { + "type": "boolean" + }, + "port": { + "type": "integer" + } + }, + "type": "object" + }, + "http": { + "properties": { + "enabled": { + "type": "boolean" + }, + "port": { + "type": "integer" + } + }, + "type": "object" + }, + "type": { + "type": "string" + } + }, + "type": "object" + }, + "serviceAccount": { + "properties": { + "annotations": { + "properties": {}, + "type": "object" + }, + "create": { + "type": "boolean" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "tolerations": { + "type": "array" + } + }, + "type": "object" +} \ No newline at end of file diff --git a/charts/air-helm/values.yaml b/charts/air-helm/values.yaml new file mode 100644 index 0000000..8992652 --- /dev/null +++ b/charts/air-helm/values.yaml @@ -0,0 +1,103 @@ +airServer: + code: + directory: "/Users/username/code/project" # Root directory of the project on your local filesystem. + modulePath: "app" # Path of the module relative to the project's root directory. + buildSource: "./cmd/server" # Path to the main package or entry point, relative to the modulePath. + + +# Default values for air-server. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +probes: + enabled: false + liveness: {} + readiness: {} + +image: + repository: gdurandvadas/air-helm + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: latest + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # 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 + http: + enabled: true + port: 8080 + grpc: + enabled: false + port: 9090 + +ingress: + enabled: false + className: "" + annotations: + {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + 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 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/example/api/cmd/server/main.go b/example/api/cmd/server/main.go new file mode 100644 index 0000000..bcbe802 --- /dev/null +++ b/example/api/cmd/server/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "net/http" + + "github.com/gdurandvadas/air-helm/example/api/internal/api/handlers" + "github.com/gdurandvadas/air-helm/example/api/internal/api/middlewares" + "github.com/gorilla/mux" +) + +func main() { + r := mux.NewRouter() + + // Setup subpath /api/v1 + api := r.PathPrefix("/api/v1").Subrouter() + + // Message handlers + api.HandleFunc("/message", handlers.CreateMessage).Methods("POST") + api.HandleFunc("/message/{id}", handlers.GetMessage).Methods("GET") + + // Middlewares + r.Use(middlewares.AccessLog) + + // Start the server + http.ListenAndServe(":8080", r) +} diff --git a/example/api/go.mod b/example/api/go.mod new file mode 100644 index 0000000..37e3f16 --- /dev/null +++ b/example/api/go.mod @@ -0,0 +1,5 @@ +module github.com/gdurandvadas/air-helm/example/api + +go 1.20 + +require github.com/gorilla/mux v1.8.1 diff --git a/example/api/go.sum b/example/api/go.sum new file mode 100644 index 0000000..7128337 --- /dev/null +++ b/example/api/go.sum @@ -0,0 +1,2 @@ +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= diff --git a/example/api/internal/api/handlers/message.go b/example/api/internal/api/handlers/message.go new file mode 100644 index 0000000..5486bc4 --- /dev/null +++ b/example/api/internal/api/handlers/message.go @@ -0,0 +1,45 @@ +package handlers + +import ( + "encoding/json" + "net/http" + + "github.com/gorilla/mux" +) + +// Assuming you want a simple in-memory store +var messages = make(map[string]string) + +// Message structure +type Message struct { + ID string `json:"id"` + Content string `json:"content"` +} + +// CreateMessage - POST /message +func CreateMessage(w http.ResponseWriter, r *http.Request) { + var msg Message + if err := json.NewDecoder(r.Body).Decode(&msg); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + // Store the message + messages[msg.ID] = msg.Content + json.NewEncoder(w).Encode(msg) +} + +// GetMessage - GET /message/{id} +func GetMessage(w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id, exists := params["id"] + if !exists { + http.Error(w, "ID is required", http.StatusBadRequest) + return + } + content, ok := messages[id] + if !ok { + http.NotFound(w, r) + return + } + json.NewEncoder(w).Encode(Message{ID: id, Content: content}) +} diff --git a/example/api/internal/api/middlewares/access_logs.go b/example/api/internal/api/middlewares/access_logs.go new file mode 100644 index 0000000..5e2056f --- /dev/null +++ b/example/api/internal/api/middlewares/access_logs.go @@ -0,0 +1,23 @@ +package middlewares + +import ( + "log" + "net/http" + "time" +) + +func AccessLog(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + startTime := time.Now() + + next.ServeHTTP(w, r) + + log.Printf( + "%s %s %s %s", + r.Method, + r.RequestURI, + r.RemoteAddr, + time.Since(startTime), + ) + }) +} diff --git a/example/api/kubernetes/helmfile.yaml b/example/api/kubernetes/helmfile.yaml new file mode 100644 index 0000000..b7e4b5c --- /dev/null +++ b/example/api/kubernetes/helmfile.yaml @@ -0,0 +1,17 @@ +releases: +- name: api + namespace: backend + chart: ../../../charts/air-helm/ + values: + - airServer: + code: + directory: "/Users/{username}/project" # (MacOS Example) Root directory of the project on your local filesystem. + modulePath: "example/api" + buildSource: "./cmd/server" + - service: + type: LoadBalancer # Expose the service to the host in Docker Desktop + http: + enabled: true + port: 8080 + - image: + tag: commit