Compare commits

..

No commits in common. "master" and "part1" have entirely different histories.

825 changed files with 27 additions and 307839 deletions

View File

@ -1,42 +0,0 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.162.0/containers/javascript-node
{
"name": "kubernetes-tutorial-basic",
"build": {
"dockerfile": "dockerfile",
"args": { "KIND_VERSION": "0.14.0" }
},
"mounts": ["type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock"],
// Set *default* container specific settings.json values on container create.
"settings": {
"terminal.integrated.shell.linux": "/bin/sh"
},
// Add the IDs of extensions you want installed when the container is created.
// "extensions": [
// "dbaeumer.vscode-eslint"
// ],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "yarn install",
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "root"
}
//Notes:
// Set site url for wordpress
// mysql -u exampleuser -p exampledb
// UPDATE wp_options
// SET option_value = 'https://docker-development-youtube-series-4rjv9rg7hqrg9-80.githubpreview.dev'
// WHERE option_name = 'home';
// UPDATE wp_options
// SET option_value = 'https://docker-development-youtube-series-4rjv9rg7hqrg9-80.githubpreview.dev'
// WHERE option_name = 'siteurl';

View File

@ -1,12 +0,0 @@
FROM alpine:latest
ARG KIND_VERSION=0.14.0
RUN apk add --no-cache curl docker-cli
RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl
RUN chmod +x ./kubectl
RUN mv ./kubectl /usr/local/bin/kubectl
RUN curl -L https://github.com/kubernetes-sigs/kind/releases/download/v${KIND_VERSION}/kind-linux-amd64 -o /usr/local/bin/kind && \
chmod +x /usr/local/bin/kind

View File

@ -1,37 +0,0 @@
###########################################################
# Rename the file extension to ".yaml" (remove "_") to enable
###########################################################
name: Docker Series Builds
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: docker login
env:
DOCKER_USER: ${{ secrets.DOCKER_USER }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
- name: docker build csharp
run: |
docker build ./c# -t aimvector/csharp:1.0.0
- name: docker build nodejs
run: |
docker build ./nodejs -t aimvector/nodejs:1.0.0
- name: docker build python
run: |
docker build ./python -t aimvector/python:1.0.0
- name: docker build golang
run: |
docker build ./golang -t aimvector/golang:1.0.0
- name: docker push
run: |
docker push aimvector/csharp:1.0.0
docker push aimvector/nodejs:1.0.0
docker push aimvector/golang:1.0.0
docker push aimvector/python:1.0.0

View File

@ -1,20 +0,0 @@
###########################################################
# IMPORTANT -> Rename the file extension to ".yaml" (remove "_") to enable this
###########################################################
name: Self-Hosted Runner Test
on:
push:
branches:
- <branch-name>
jobs:
build:
runs-on: self-hosted
steps:
- uses: actions/checkout@v2
- name: docker build python
run: |
docker build ./python/introduction/ -t python:1.0.0

21
.gitignore vendored
View File

@ -1,21 +0,0 @@
c#/src/bin/
c#/src/obj/
node_modules/
__pycache__/
*.pem
*.csr
.terraform
*.tfstate
*.tfstate.*
security/letsencrypt/introduction/certs/**
kubernetes/shipa/installs/shipa-helm-chart-1.1.1/
messaging/kafka/data/*
kubernetes/portainer/volume*
kubernetes/rancher/volume/*
kubernetes/portainer/business/volume*
#ignore postgres data for sample and database tutorials
pgdata
#ignore sample data mount points
.data

58
.vscode/launch.json vendored
View File

@ -1,58 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Docker Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickRemoteProcess}",
"pipeTransport": {
"pipeProgram": "docker",
"pipeArgs": [ "exec", "-i", "csharp" ],
"debuggerPath": "/root/vsdbg/vsdbg",
"pipeCwd": "${workspaceRoot}",
"quoteArgs": false
},
"sourceFileMap": {
"/work": "${workspaceRoot}/c#/src/"
}
},
{
"name": "Remote Docker",
"type": "go",
"request": "launch",
"mode": "remote",
"remotePath":"/go/src/work/",
"port": 2345,
"host": "127.0.0.1",
"program": "${workspaceFolder}/golang/src/",
"args": [],
"trace" : "verbose",
"env" : {}
},
{
"name": "Python Attach",
"type": "python",
"request": "attach",
"pathMappings": [
{
"localRoot": "${workspaceFolder}/python/src/",
"remoteRoot": "/work"
}
],
"port": 5678,
"host": "127.0.0.1"
},
{
"name": "Docker: Attach to Node",
"type": "node",
"request": "attach",
"remoteRoot": "/work/src/",
"port": 9229,
"address": "localhost",
"localRoot": "${workspaceFolder}/nodejs/src/",
"protocol": "inspector",
"restart": true
}
]
}

View File

@ -1,37 +1,9 @@
# The Ultimate Engineer Toolbox <img src="https://www.shareicon.net/data/128x128/2017/04/11/883708_media_512x512.png" alt="YouTube" width="5%" height="5%"> :hammer::wrench: # docker-development-youtube-series
A Collection of tools, hands-on walkthroughs with source code. <br/> Hi!
The Ultimate Swiss Army knife for DevOps, Developers and Platform Engineers
<br/> This is the source code for the YouTube series covering docker-based development workflows.
Part #1 https://youtu.be/wyjNpxLRmLg
| Steps | Playlist :tv: | Source :octocat: | More details coming soon!
|---|---|---|
| [Learn Kubernetes](./kubernetes/README.md) :snowflake: | <a href="https://www.youtube.com/playlist?list=PLHq1uqvAteVvUEdqaBeMK2awVThNujwMd" title="Kubernetes"><img src="https://i.ytimg.com/vi/8h4FoWK7tIA/hqdefault.jpg" width="50%" alt="Kubernetes Guide" /></a> | [source](./kubernetes/readme.md) |
| Learn about CI/CD tools :whale: | <a href="https://www.youtube.com/playlist?list=PLHq1uqvAteVsSsrnZimHEf7NJ1MlRhQUj" title="CI/CD"><img src="https://i.ytimg.com/vi/myCcJJ_Fk10/hqdefault.jpg" width="50%" alt="CI/CD Guide" /></a> | | | |
| Deploy Kubernetes to the cloud :partly_sunny: | <a href="https://www.youtube.com/playlist?list=PLHq1uqvAteVsUhzNBkn-rPzXtPNpJu1-k" title="Cloud K8s"><img src="https://i.ytimg.com/vi/3jA9EfkSAUU/hqdefault.jpg" width="50%" alt="Cloud Guide" /></a> | [source](./kubernetes/cloud/readme.md) |
| Monitoring Kubernetes :mag: | <a href="https://www.youtube.com/playlist?list=PLHq1uqvAteVuEXCrRkPFWLXRKWNLOVUHn" title="Cloud K8s"><img src="https://i.ytimg.com/vi/5o37CGlNLr8/hqdefault.jpg" width="50%" alt="Cloud Guide" /></a> | [source](./monitoring/prometheus/kubernetes/readme.md) |
| Guide to Logging :page_with_curl: | <a href="https://www.youtube.com/playlist?list=PLHq1uqvAteVvfDxFW50Mdezk0xum-tyHT" title="Cloud K8s"><img src="https://i.ytimg.com/vi/MMVdkzeQ848/hqdefault.jpg" width="50%" alt="Cloud Guide" /></a> | [source](./monitoring/logging/readme.md) |
| Guide to ServiceMesh :globe_with_meridians: | <a href="https://www.youtube.com/playlist?list=PLHq1uqvAteVsmxHpGsMjTOROn3i99lzTA" title="Cloud K8s"><img src="https://i.ytimg.com/vi/rVNPnHeGYBE/hqdefault.jpg" width="50%" alt="Cloud Guide" /></a> | [source](./kubernetes/servicemesh/readme.md) |
## Docker Development Basics
| Step :heavy_check_mark: | Video :movie_camera: | Source Code :octocat: |
|---|---|---|
| Working with `Dockerfiles` <br/>(.NET, Golang, Python, NodeJS) | <a href="https://youtu.be/wyjNpxLRmLg" title="Docker 1"><img src="https://i.ytimg.com/vi/wyjNpxLRmLg/hqdefault.jpg" width="50%" alt="Docker 1" /></a> | [source](https://github.com/marcel-dempers/docker-development-youtube-series/tree/part1) |
| Working with code <br/>(.NET, Golang, Python, NodeJS) | <a href="https://youtu.be/EdmKENqnQUw" title="Docker 1"><img src="https://i.ytimg.com/vi/EdmKENqnQUw/hqdefault.jpg" width="50%" alt="Docker 1" /></a> | [source](https://github.com/marcel-dempers/docker-development-youtube-series/tree/part2) |
| Docker Multistage explained | <a href="https://youtu.be/2lQ7WrwpZfI" title="Docker 1"><img src="https://i.ytimg.com/vi/2lQ7WrwpZfI/hqdefault.jpg" width="50%" alt="Docker 1" /></a> | [source](https://github.com/marcel-dempers/docker-development-youtube-series/tree/part3) |
| Debugging Go in Docker | <a href="https://youtu.be/kToyI16IFxs" title="Docker 1"><img src="https://i.ytimg.com/vi/kToyI16IFxs/hqdefault.jpg" width="50%" alt="Docker 1" /></a> | [source](https://github.com/marcel-dempers/docker-development-youtube-series/tree/master/golang) |
| Debugging .NET in Docker | <a href="https://youtu.be/ds2bud0ZYTY" title="Docker 1"><img src="https://i.ytimg.com/vi/ds2bud0ZYTY/hqdefault.jpg" width="50%" alt="Docker 1" /></a> | [source](https://github.com/marcel-dempers/docker-development-youtube-series/tree/part5) |
| Debugging Python in Docker | <a href="https://youtu.be/b78Tg-YmJZI" title="Docker 1"><img src="https://i.ytimg.com/vi/b78Tg-YmJZI/hqdefault.jpg" width="50%" alt="Docker 1" /></a> | [source](https://github.com/marcel-dempers/docker-development-youtube-series/tree/debugging-python) |
| Debugging NodeJS in Docker | <a href="https://youtu.be/ktvgr9VZ4dc" title="Docker 1"><img src="https://i.ytimg.com/vi/ktvgr9VZ4dc/hqdefault.jpg" width="50%" alt="Docker 1" /></a> | [source](https://github.com/marcel-dempers/docker-development-youtube-series/tree/master/nodejs) |
## Engineering Toolbox :hammer::wrench:
Checkout the toolbox [website](https://marceldempers.dev/toolbox)
<a href="https://marceldempers.dev/toolbox" title="toolbox 1"><img src="./toolbox.png" alt="toolbox 1" /></a>

View File

@ -1,151 +0,0 @@
# Introduction to Open AI
## Overview
What is [Open AI](https://openai.com/) ?
* Research company on AI development
* Builds and provides models
* Builds and provides a standard protocol for using AI
What is a model ?
I see a model as a language super database. </br>
Instead of writing a query, that is slow to query a traditional database like SQL, you can throw a question at a model and it gives you an answer really fast </br>
Model examples:
* GPT 3.5
* GPT 4
## Getting started
The best way to get started and to understand OpenAI, is to learn hands on
* Create an OpenAI account [here](https://openai.com/)
## Chat GPT
Here you can find the link to [ChatGPT](https://chat.openai.com/)
## Open AI Playground
Here you can find the link to the [OpenAI Playground](https://platform.openai.com/playground)
## Build an AI powered app
We can start with a `main.py` that reads a message
```
import sys
message = sys.argv[0]
```
Then we will need the code from the Open AI playground and add it to our `main.py`. </br>
Move the `import` statements to the top </br>
Once you have tidied up everything, you can get the response message from the AI:
```
responseMessage = response.choices[0].message.content
```
Let's build our app
```
cd ai\openai\introduction
docker build . -t ai-app
```
Set my OpenAI API key
```
$ENV:OPENAI_API_KEY=""
```
Run our AI App:
```
docker run -it -e OPENAI_API_KEY=$ENV:OPENAI_API_KEY ai-app
```
When we run the app, notice it has no concept of memory. </br>
The playground works because it keeps track of all the user and AI messages and keeps appending new messages to it </br>
So it can track the conversation.
Let's keep track of messages, by writing it to a local file </br>
We will also take the system message out and keep it as a constant in our code </br>
Full example:
```
import sys
import os
import json
import openai
openai.api_key = os.getenv("OPENAI_API_KEY")
#read the incoming message
message = sys.argv[1]
user_message = {
"role" : "user",
"content" : message
}
systemMessage = {
"role": "system",
"content": "You are a kubernetes exper that can assist developers with troubleshooting deployments\n\nTo help the developer you will need to know the namespaces as well as the pod name. Ask for missing information\n\nGenerate a command to help the developer surface logs or information\n"
}
# read the cached user messages if there are any
userMessages = []
if os.path.isfile("messages.json"):
with open('messages.json', newline='') as messagesFile:
data = messagesFile.read()
userMessages = json.loads(data)
# add the new message to it and update the cached messages
userMessages.append(user_message)
with open('messages.json', 'w', newline='') as messagesFile:
msgJSON = json.dumps(userMessages)
messagesFile.write(msgJSON)
print(msgJSON)
messages = []
messages.append(systemMessage)
messages.extend(userMessages)
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=messages,
temperature=1,
max_tokens=256,
top_p=1,
frequency_penalty=0,
presence_penalty=0
)
responseMessage = response.choices[0].message.content
print(responseMessage)
```
Now we can mount our volume so we persist the cache of messages
```
docker run -it -e OPENAI_API_KEY=$ENV:OPENAI_API_KEY -v ${PWD}:/app ai-app "can you help me with my deployment?"
Of course! I'd be happy to help with your deployment. Could you please provide me with the namespace and the name of the pod you're encountering issues with?
docker run -it -e OPENAI_API_KEY=$ENV:OPENAI_API_KEY -v ${PWD}:/app ai-app "my pod is pod-123"
Sure, I can help you with your deployment. Can you please provide me with the namespace in which the pod is running?
docker run -it -e OPENAI_API_KEY=$ENV:OPENAI_API_KEY -v ${PWD}:/app ai-app "its in the products namespace"
Great! To surface the logs for the pod "pod-123" in the "products" namespace, you can use the following command:
```shell
kubectl logs -n products pod-123
```
This command will retrieve the logs for the specified pod in the given namespace. Make sure you have the necessary permissions to access the namespace.
```

View File

@ -1,11 +0,0 @@
FROM python:3.11-alpine
RUN mkdir /app
WORKDIR /app
COPY requirements.txt /app/requirements.txt
RUN pip install -r requirements.txt
COPY main.py /app/
ENTRYPOINT ["python3", "main.py"]

View File

@ -1,49 +0,0 @@
import sys
import os
import json
import openai
openai.api_key = os.getenv("OPENAI_API_KEY")
#read the incoming message
message = sys.argv[1]
user_message = {
"role" : "user",
"content" : message
}
systemMessage = {
"role": "system",
"content": "You are a kubernetes exper that can assist developers with troubleshooting deployments\n\nTo help the developer you will need to know the namespaces as well as the pod name. Ask for missing information\n\nGenerate a command to help the developer surface logs or information\n"
}
# read the cached user messages if there are any
userMessages = []
if os.path.isfile("messages.json"):
with open('messages.json', newline='') as messagesFile:
data = messagesFile.read()
userMessages = json.loads(data)
# add the new message to it and update the cached messages
userMessages.append(user_message)
with open('messages.json', 'w', newline='') as messagesFile:
msgJSON = json.dumps(userMessages)
messagesFile.write(msgJSON)
print(msgJSON)
messages = []
messages.append(systemMessage)
messages.extend(userMessages)
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=messages,
temperature=1,
max_tokens=256,
top_p=1,
frequency_penalty=0,
presence_penalty=0
)
responseMessage = response.choices[0].message.content
print(responseMessage)

View File

@ -1 +0,0 @@
openai==0.28.0

View File

@ -1,4 +0,0 @@
# Introduction to Kafka
This guide is under the messaging section alongside other message brokers like `RabbitMQ` etc. </br>
Checkout the guide under the [messaging/kafka](../../messaging/kafka/README.md) folder

View File

@ -1,3 +0,0 @@
# Introduction to Argo CD
<a href="https://youtu.be/2WSJF7d8dUg" title="argo"><img src="https://i.ytimg.com/vi/2WSJF7d8dUg/hqdefault.jpg" width="20%" alt="introduction to argo cd" /></a>

View File

@ -1,20 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: example-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/marcel-dempers/docker-development-youtube-series.git
targetRevision: HEAD
path: argo/example-app
directory:
recurse: true
destination:
server: https://kubernetes.default.svc
namespace: example-app
syncPolicy:
automated:
prune: false
selfHeal: false

View File

@ -1,28 +0,0 @@
CLI
```
argocd app create --name test \
--repo https://github.com/marcel-dempers/docker-development-youtube-series \
--dest-server https://kubernetes.default.svc \
--dest-namespace marcel --path kubernetes
```
YAML
```
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: test
namespace: marcel
spec:
project: default
source:
repoURL: https://github.com/marcel-dempers/docker-development-youtube-series.git
targetRevision: HEAD
path: argo/example-app
destination:
server: https://kubernetes.default.svc
namespace: marcel
```

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: example-config
data:
config.json: |
{
"environment" : "dev"
}
# kubectl create configmap example-config --from-file ./golang/configs/config.json

View File

@ -1,47 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: example-deploy
labels:
app: example-app
annotations:
spec:
selector:
matchLabels:
app: example-app
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: example-app
spec:
containers:
- name: example-app
image: aimvector/python:1.0.0
imagePullPolicy: Always
ports:
- containerPort: 5000
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "500m"
volumeMounts:
- name: secret-volume
mountPath: /secrets/
- name: config-volume
mountPath: /configs/
volumes:
- name: secret-volume
secret:
secretName: mysecret
- name: config-volume
configMap:
name: example-config #name of our configmap object

View File

@ -1,4 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: example-app

View File

@ -1,12 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
stringData:
secret.json: |-
{
"api_key" : "somesecretgoeshere"
}
#kubectl create secret generic mysecret --from-file .\golang\secrets\secret.json

View File

@ -1,11 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: example-service
spec:
selector:
app: example-app
ports:
- protocol: TCP
port: 80
targetPort: 5000

View File

@ -1,27 +1 @@
FROM mcr.microsoft.com/dotnet/sdk:8.0 as build FROM mcr.microsoft.com/dotnet/core/sdk:2.2
#install debugger for NET Core
RUN apt-get update
RUN apt-get install -y unzip
RUN curl -sSL https://aka.ms/getvsdbgsh | /bin/sh /dev/stdin -v latest -l ~/vsdbg
RUN mkdir /src/
WORKDIR /src/
COPY ./src/helloworld.csproj /src/helloworld.csproj
RUN dotnet restore
COPY ./src/ /src/
RUN mkdir /out/
RUN dotnet build helloworld.csproj --configuration Debug --no-restore
RUN dotnet publish helloworld.csproj --output /out --configuration Debug --no-restore
ENTRYPOINT ["dotnet", "run"]
FROM mcr.microsoft.com/dotnet/aspnet:8.0 as runtime
WORKDIR /app
COPY --from=build /out/ /app
ENTRYPOINT ["dotnet", "helloworld.dll"]

View File

@ -1,26 +0,0 @@
using Microsoft.AspNetCore.Hosting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.Configure<HostOptions>(builder.Configuration.GetSection("HostOptions"));
builder.WebHost.ConfigureKestrel(serverOptions =>
{
serverOptions.ListenAnyIP(5000);
});
var app = builder.Build();
Console.WriteLine(Environment.ProcessorCount.ToString());
app.UseHttpsRedirection();
app.MapGet("/", async () =>
{
await Task.Delay(40000); // Sleep for 40 seconds
return "Hello World!";
});
app.Run();

View File

@ -1,31 +0,0 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:59079",
"sslPort": 44372
}
},
"profiles": {
"helloworld": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7158;http://localhost:5122",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -1,13 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
</ItemGroup>
</Project>

View File

@ -1,3 +0,0 @@
# Introduction to Deno with Docker
<a href="https://youtu.be/4EfnECkCx8E" title="Kubernetes"><img src="https://i.ytimg.com/vi/4EfnECkCx8E/hqdefault.jpg" width="20%" alt="introduction to deno" /></a>

View File

@ -1,13 +0,0 @@
FROM aimvector/deno:1.0.0-buster-slim as build
COPY ./src/ $DENO_DIR
RUN mkdir /out/
RUN deno bundle /deno-dir/server.js /out/server.js
FROM aimvector/deno:1.0.0-buster-slim as final
COPY --from=build /out/server.js /deno-dir/server.js
ENTRYPOINT ["deno"]
CMD ["run", "--allow-net", "/deno-dir/server.js"]

View File

@ -1,25 +0,0 @@
FROM debian:buster-slim
ENV DENO_VERSION=1.0.0
RUN apt-get -qq update \
&& apt-get -qq install -y --no-install-recommends curl ca-certificates unzip \
&& curl -fsSL https://github.com/denoland/deno/releases/download/v${DENO_VERSION}/deno-x86_64-unknown-linux-gnu.zip \
--output deno.zip \
&& unzip deno.zip \
&& rm deno.zip \
&& chmod 777 deno \
&& mv deno /usr/bin/deno \
&& apt-get -qq remove --purge -y curl ca-certificates unzip \
&& apt-get -y -qq autoremove \
&& apt-get -qq clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN useradd --uid 1993 --user-group deno \
&& mkdir /deno-dir/ \
&& chown deno:deno /deno-dir/
ENV DENO_DIR /deno-dir/
ENTRYPOINT ["deno"]
CMD ["run", "https://deno.land/std/examples/welcome.ts"]

View File

@ -1,35 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: example-deploy
labels:
app: example-app
annotations:
spec:
selector:
matchLabels:
app: example-app
replicas: 2
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: example-app
spec:
containers:
- name: example-app
image: aimvector/deno-app:v1
imagePullPolicy: Always
ports:
- containerPort: 5000
resources:
requests:
memory: "64Mi"
cpu: "10m"
limits:
memory: "256Mi"
cpu: "500m"

View File

@ -1,15 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: example-service
labels:
app: example-app
spec:
type: LoadBalancer
selector:
app: example-app
ports:
- protocol: TCP
name: http
port: 80
targetPort: 5000

View File

@ -1,6 +0,0 @@
FROM aimvector/deno:1.0.0-buster-slim as build
COPY ./src/ $DENO_DIR
ENTRYPOINT ["deno"]
CMD ["run", "--allow-net", "/deno-dir/server.js"]

View File

@ -1,5 +0,0 @@
import { serve } from "https://deno.land/std@0.50.0/http/server.ts";
for await (const req of serve({ port: 5000 })) {
req.respond({ body: "Hello World! - from Deno 1.0.0\n" });
}

View File

@ -1,71 +1,18 @@
version: "3.4" version: "3"
services: services:
csharp: #docker run --rm -it -v ${PWD}:/work -w /work -p 5000:5000 aimvector/csharp:1.0.0 /bin/sh csharp:
container_name: csharp container_name: csharp
image: aimvector/csharp:1.0.0 image: aimvector/csharp:1.0.0
build: build: ./c#
context: ./c# golang:
target: debug
volumes:
- ./c#/src/:/work/
ports:
- 5000:5000
golang: #docker run --rm -it -v ${PWD}:/go/src/work -v ${PWD}/golang/configs/:/configs -v ${PWD}/golang/secrets/:/secrets -p 5001:5000 -p 2345:2345 --security-opt "seccomp:unconfined" aimvector/golang:1.0.0
container_name: golang container_name: golang
image: aimvector/golang:1.0.0 image: aimvector/golang:1.0.0
build: build: ./golang
context: ./golang nodejs:
target: prod
volumes:
- ./golang/configs:/configs/
- ./golang/secrets:/secrets/
- ./golang/src/:/go/src/work/
ports:
- 5001:5000
- 2345:2345
security_opt:
- "seccomp:unconfined"
python: #docker run --rm -it -v ${PWD}:/work -w /work -p 5003:5000 aimvector/python:1.0.0 /bin/sh
container_name: python
image: aimvector/python:1.0.0
build:
context: ./python
target: debug
#working_dir: /work #comment out for build.target:prod
#entrypoint: /bin/sh #comment out for build.target:prod
#stdin_open: true #comment out for build.target:prod
#tty: true #comment out for build.target:prod
volumes:
- ./python/src/:/work
ports:
- 5003:5000
- 5678:5678
nodejs: #docker run --rm -it -v ${PWD}:/work -w /work -p 5002:5000 aimvector/nodejs:1.0.0 /bin/sh
container_name: nodejs container_name: nodejs
image: aimvector/nodejs:1.0.0 image: aimvector/nodejs:1.0.0
build: build: ./nodejs
context: ./nodejs python:
target: debug container_name: python
#working_dir: /work #comment out for build.target:prod image: aimvector/python:1.0.0
#entrypoint: /bin/sh #comment out for build.target:prod build: ./python
#stdin_open: true #comment out for build.target:prod
#tty: true #comment out for build.target:prod
volumes:
- ./nodejs/src/:/work/src/
ports:
- 5002:5000
- 9229:9229 #debug port
spring-java: #docker run --rm -it -v ${PWD}:/work -w /work -p 5002:5000 aimvector/nodejs:1.0.0 /bin/sh
container_name: spring-java
image: aimvector/spring-java:1.0.0
build:
context: ./springboot/java/
target: debug
working_dir: /work/src/ #comment out for build.target:prod
entrypoint: /bin/sh #comment out for build.target:prod
stdin_open: true #comment out for build.target:prod
tty: true #comment out for build.target:prod
volumes:
- ./springboot/java/:/work/src/
ports:
- 9999:8080

View File

@ -1,3 +0,0 @@
# Introduction to Drone CI
<a href="https://youtu.be/myCcJJ_Fk10" title="drone ci"><img src="https://i.ytimg.com/vi/myCcJJ_Fk10/hqdefault.jpg" width="20%" alt="introduction to drone ci" /></a>

View File

@ -1,32 +0,0 @@
---
kind: pipeline
type: kubernetes
name: default
steps:
- name: build-push
image: docker:dind
volumes:
- name: dockersock
path: /var/run
environment:
DOCKER_USER:
from_secret: DOCKER_USER
DOCKER_PASSWORD:
from_secret: DOCKER_PASSWORD
commands:
- sleep 5 ## give docker enough time to start
- docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
- docker build ./golang -t aimvector/golang:1.0.0
- docker push aimvector/golang:1.0.0
services:
- name: docker
image: docker:dind
privileged: true
volumes:
- name: dockersock
path: /var/run
volumes:
- name: dockersock
temp: {}

View File

@ -1,50 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: postgres-config
labels:
app: postgres
data:
POSTGRES_DB: postgresdb
POSTGRES_USER: postgresadmin
POSTGRES_PASSWORD: admin123
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: "postgres"
selector:
matchLabels:
app: postgres
replicas: 1
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:10.4
imagePullPolicy: "IfNotPresent"
ports:
- containerPort: 5432
envFrom:
- configMapRef:
name: postgres-config
---
apiVersion: v1
kind: Service
metadata:
name: postgres
labels:
app: postgres
spec:
selector:
app: postgres
ports:
- protocol: TCP
name: http
port: 5432
targetPort: 5432

View File

@ -1,40 +0,0 @@
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
namespace: drone
name: drone-runner
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- create
- delete
- apiGroups:
- ""
resources:
- pods
- pods/log
verbs:
- get
- create
- delete
- list
- watch
- update
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: drone-runner
namespace: drone
subjects:
- kind: ServiceAccount
name: drone-runner
namespace: drone
roleRef:
kind: Role
name: drone-runner
apiGroup: rbac.authorization.k8s.io

View File

@ -1,43 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: drone-runner
labels:
app.kubernetes.io/name: drone-runner
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: drone
template:
metadata:
labels:
app.kubernetes.io/name: drone
spec:
serviceAccountName: drone-runner
containers:
- name: runner
image: drone/drone-runner-kube:latest
ports:
- containerPort: 3000
env:
- name: DRONE_NAMESPACE_DEFAULT
value: drone
- name: DRONE_SERVICE_ACCOUNT_DEFAULT
value: drone-runner
- name: DRONE_RPC_HOST
value: droneserver.drone
- name: DRONE_RPC_PROTO
value: http
- name: DRONE_RPC_SECRET
valueFrom:
secretKeyRef:
name: drone-server-secret
key: DRONE_RPC_SECRET
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: drone-runner
labels:
app.kubernetes.io/name: drone-runner

View File

@ -1,64 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: drone-server
labels:
app: drone-server
annotations:
spec:
selector:
matchLabels:
app: drone-server
replicas: 1
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: drone-server
spec:
containers:
- name: drone-server
image: drone/drone:1.6.5
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
- containerPort: 443
env:
- name: DRONE_USER_CREATE
valueFrom:
secretKeyRef:
name: drone-server-secret
key: DRONE_USER_CREATE
- name: DRONE_DATABASE_DRIVER
value: postgres
- name: DRONE_DATABASE_DATASOURCE
valueFrom:
secretKeyRef:
name: drone-server-secret
key: DRONE_DATABASE_DATASOURCE
- name: DRONE_SERVER_PROTO
value: https
- name: DRONE_SERVER_HOST
valueFrom:
secretKeyRef:
name: drone-server-secret
key: DRONE_SERVER_HOST
- name: DRONE_GITHUB_CLIENT_ID
valueFrom:
secretKeyRef:
name: drone-server-secret
key: DRONE_GITHUB_CLIENT_ID
- name: DRONE_GITHUB_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: drone-server-secret
key: DRONE_GITHUB_CLIENT_SECRET
- name: DRONE_RPC_SECRET
valueFrom:
secretKeyRef:
name: drone-server-secret
key: DRONE_RPC_SECRET

View File

@ -1,18 +0,0 @@
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: drone-server
annotations:
kubernetes.io/ingress.class: "traefik"
traefik.ingress.kubernetes.io/frontend-entry-points: http,https
traefik.ingress.kubernetes.io/redirect-entry-point: https
traefik.ingress.kubernetes.io/redirect-permanent: "true"
spec:
rules:
- host: drone.marceldempers.dev
http:
paths:
- backend:
serviceName: droneserver
servicePort: 80
path: /

View File

@ -1,12 +0,0 @@
apiVersion: v1
kind: Secret
metadata:
name: drone-server-secret
type: Opaque
data:
DRONE_GITHUB_CLIENT_ID: xxxxxxx #Get this from GitHub OAUTH
DRONE_GITHUB_CLIENT_SECRET: xxxxxxx #Get this from GitHub OAUTH
DRONE_RPC_SECRET: xxxxxxx #openssl rand -hex 16
DRONE_DATABASE_DATASOURCE: xxxxxxx #postgres://postgresadmin:admin123@postgres:5432/postgresdb?sslmode=disable
DRONE_USER_CREATE: xxxxxxx #username:marcel-dempers,admin:true
DRONE_SERVER_HOST: xxxxxxx #drone.marceldempers.dev

View File

@ -1,19 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: droneserver
labels:
app: drone-server
spec:
type: ClusterIP
selector:
app: drone-server
ports:
- protocol: TCP
name: http
port: 80
targetPort: 80
- protocol: TCP
name: https
port: 443
targetPort: 443

View File

@ -1,84 +0,0 @@
<a href="https://youtu.be/RcHGqCBofvw" title="githubactions"><img src="https://i.ytimg.com/vi/RcHGqCBofvw/hqdefault.jpg" width="20%" alt="introduction to github actions runners" /></a>
# Introduction to GitHub Actions: Self hosted runners
## Create a kubernetes cluster
In this guide we we''ll need a Kubernetes cluster for testing. Let's create one using [kind](https://kind.sigs.k8s.io/) </br>
```
kind create cluster --name githubactions --image kindest/node:v1.28.0@sha256:b7a4cad12c197af3ba43202d3efe03246b3f0793f162afb40a33c923952d5b31
```
Let's test our cluster:
```
kubectl get nodes
NAME STATUS ROLES AGE VERSION
githubactions-control-plane Ready control-plane 2m53s v1.28.0
```
## Running the Runner in Docker
We can simply install this directly on to virtual machines , but for this demo, I'd like to run it in Kubernetes inside a container. </br>
### Security notes
* Running in Docker needs high priviledges.
* Would not recommend to use these on public repositories.
* Would recommend to always run your CI systems in seperate Kubernetes clusters.
### Creating a Dockerfile
* Installing Docker CLI
For this to work we need a `dockerfile` and follow instructions to [Install Docker](https://docs.docker.com/engine/install/debian/).
I would grab the content and create statements for my `dockerfile` </br>
Now notice that we only install the `docker` CLI. </br>
This is because we want our running to be able to run docker commands , but the actual docker server runs elsewhere </br>
This gives you flexibility to tighten security by running docker on the host itself and potentially run the container runtime in a non-root environment </br>
* Installing Github Actions Runner
Next up we will need to install the [GitHub actions runner](https://github.com/actions/runner) in our `dockerfile`
Now to give you a "behind the scenes" of how I usually build my `dockerfile`s, I run a container to test my installs:
```
docker build . -t github-runner:latest
docker run -it github-runner bash
```
Next steps:
* Now we can see `docker` is installed
* To see how a runner is installed, lets go to our repo | runner and click "New self-hosted runner"
* Try these steps in the container
* We will needfew dependencies
* We download the runner
* TODO
Finally lets test the runner in `docker`
```
docker run -it -e GITHUB_PERSONAL_TOKEN="" -e GITHUB_OWNER=marcel-dempers -e GITHUB_REPOSITORY=docker-development-youtube-series github-runner
```
## Deploy to Kubernetes
Load our github runner image so we dont need to push it to a registry:
```
kind load docker-image github-runner:latest --name githubactions
```
Create a kubernetes secret with our github details
```
kubectl create ns github
kubectl -n github create secret generic github-secret `
--from-literal GITHUB_OWNER=marcel-dempers `
--from-literal GITHUB_REPOSITORY=docker-development-youtube-series `
--from-literal GITHUB_PERSONAL_TOKEN=""
kubectl -n github apply -f kubernetes.yaml
```

View File

@ -1,46 +0,0 @@
FROM debian:bookworm-slim
ARG RUNNER_VERSION="2.302.1"
ENV GITHUB_PERSONAL_TOKEN ""
ENV GITHUB_OWNER ""
ENV GITHUB_REPOSITORY ""
# Install Docker -> https://docs.docker.com/engine/install/debian/
# Add Docker's official GPG key:
RUN apt-get update && \
apt-get install -y ca-certificates curl gnupg
RUN install -m 0755 -d /etc/apt/keyrings
RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
RUN chmod a+r /etc/apt/keyrings/docker.gpg
# Add the repository to Apt sources:
RUN echo \
"deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
"$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null
RUN apt-get update
# I only install the CLI, we will run docker in another container!
RUN apt-get install -y docker-ce-cli
# Install the GitHub Actions Runner
RUN apt-get update && apt-get install -y sudo jq
RUN useradd -m github && \
usermod -aG sudo github && \
echo "%sudo ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
USER github
WORKDIR /actions-runner
RUN curl -Ls https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz | tar xz \
&& sudo ./bin/installdependencies.sh
COPY --chown=github:github entrypoint.sh /actions-runner/entrypoint.sh
RUN sudo chmod u+x /actions-runner/entrypoint.sh
#working folder for the runner
RUN sudo mkdir /work
ENTRYPOINT ["/actions-runner/entrypoint.sh"]

View File

@ -1,26 +0,0 @@
#!/bin/sh
registration_url="https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPOSITORY}/actions/runners/registration-token"
echo "Requesting registration URL at '${registration_url}'"
payload=$(curl -sX POST -H "Authorization: token ${GITHUB_PERSONAL_TOKEN}" ${registration_url})
export RUNNER_TOKEN=$(echo $payload | jq .token --raw-output)
./config.sh \
--name $(hostname) \
--token ${RUNNER_TOKEN} \
-- labels my-runner \
--url https://github.com/${GITHUB_OWNER}/${GITHUB_REPOSITORY} \
--work "/work" \
--unattended \
--replace
remove() {
./config.sh remove --unattended --token "${RUNNER_TOKEN}"
}
trap 'remove; exit 130' INT
trap 'remove; exit 143' TERM
./run.sh "$*" &
wait $!

View File

@ -1,64 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: github-runner
labels:
app: github-runner
spec:
replicas: 1
selector:
matchLabels:
app: github-runner
template:
metadata:
labels:
app: github-runner
spec:
containers:
- name: github-runner
imagePullPolicy: Never #use local kind image
image: github-runner:latest
env:
- name: GITHUB_OWNER
valueFrom:
secretKeyRef:
name: github-secret
key: GITHUB_OWNER
- name: GITHUB_REPOSITORY
valueFrom:
secretKeyRef:
name: github-secret
key: GITHUB_REPOSITORY
- name: GITHUB_PERSONAL_TOKEN
valueFrom:
secretKeyRef:
name: github-secret
key: GITHUB_PERSONAL_TOKEN
- name: DOCKER_HOST
value: tcp://localhost:2375
volumeMounts:
- name: data
mountPath: /work/
- name: dind
image: docker:24.0.6-dind
env:
- name: DOCKER_TLS_CERTDIR
value: ""
resources:
requests:
cpu: 20m
memory: 512Mi
securityContext:
privileged: true
volumeMounts:
- name: docker-graph-storage
mountPath: /var/lib/docker
- name: data
mountPath: /work/
volumes:
- name: docker-graph-storage
emptyDir: {}
- name: data
emptyDir: {}

View File

@ -1,3 +0,0 @@
{
"environment" : "dev"
}

View File

@ -1,3 +0,0 @@
#!/bin/sh
cd /go/src/work
dlv debug --headless --log -l 0.0.0.0:2345 --api-version=2

View File

@ -1,32 +1 @@
FROM golang:1.12.5-alpine3.9 as debug FROM golang:1.12.5-alpine3.9 as builder
# installing git
RUN apk update && apk upgrade && \
apk add --no-cache git \
dpkg \
gcc \
git \
musl-dev
ENV GOPATH /go
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
RUN go get github.com/sirupsen/logrus
RUN go get github.com/buaazp/fasthttprouter
RUN go get github.com/valyala/fasthttp
RUN go get github.com/go-delve/delve/cmd/dlv
WORKDIR /go/src/work
COPY ./src /go/src/work/
RUN go build -o app
### Run the Delve debugger ###
COPY ./dlv.sh /
RUN chmod +x /dlv.sh
ENTRYPOINT [ "/dlv.sh"]
###########START NEW IMAGE###################
FROM alpine:3.9 as prod
COPY --from=debug /go/src/work/app /
CMD ./app

View File

@ -1,36 +0,0 @@
package main
import (
"fmt"
)
func main() {
customers := GetCustomers()
for _, customer := range customers {
//we can access the "customer" variable in this approach
fmt.Println(customer)
}
}
func getData() (customers []string) {
customers = []string{ "Marcel Dempers", "Bob Smith", "John Smith"}
customers = append(customers, "Ben Spain")
customers = append(customers, "Aleem Janmohamed")
customers = append(customers, "Jamie le Notre")
customers = append(customers, "Victor Savkov")
customers = append(customers, "P The Admin")
customers = append(customers, "Adrian Oprea")
customers = append(customers, "Jonathan D")
for _, customer := range customers {
fmt.Println(customer)
}
return customers
}

View File

@ -1,28 +0,0 @@
package main
type (
Customer struct {
FirstName string
LastName string
FullName string
}
)
func GetCustomers()(customers []Customer) {
marcel := Customer{ FirstName: "Marcel", LastName: "Dempers"}
customers = append(customers, marcel,
Customer{ FirstName: "Ben", LastName: "Spain" },
Customer{ FirstName: "Aleem", LastName: "Janmohamed" },
Customer{ FirstName: "Jamie", LastName: "le Notre" },
Customer{ FirstName: "Victor", LastName: "Savkov" },
Customer{ FirstName: "P", LastName: "The Admin" },
Customer{ FirstName: "Adrian", LastName: "Oprea" },
Customer{ FirstName: "Jonathan", LastName: "D" },
)
return customers
}

View File

@ -1,3 +0,0 @@
module github.com/docker-development-youtube-series/golang/introdouction/app
go 1.15

View File

@ -1,13 +0,0 @@
FROM golang:1.15-alpine as dev
WORKDIR /work
FROM golang:1.15-alpine as build
WORKDIR /app
COPY ./app/* /app/
RUN go build -o app
FROM alpine as runtime
COPY --from=build /app/app /
CMD ./app

View File

@ -1,3 +0,0 @@
FROM golang:1.15-alpine as dev
WORKDIR /work

View File

@ -1,326 +0,0 @@
# Introduction to Go: JSON
<a href="https://youtu.be/_ok29xwZ11k" title="golang-part-2"><img src="https://i.ytimg.com/vi/_ok29xwZ11k/hqdefault.jpg" width="20%" alt="introduction to Go part 2" /></a>
In programming languages, you will very often deal with data structures internally. <br/>
Sometimes, you need to pass data outside of your application or read data from another application, or even a file. <br/>
API, for example often expose data in `json`, `xml`, `grpc` and all sorts of formats.
Before we can really take a look at building APIs and even storing data in databases,
we need some fundamental knowledge about how to convert these data structures, into structures
that our application can understand. <br/>
In part 1, we dealt with [Variables]("https://tour.golang.org/basics/8") and more importantly, <br/> we dealt with `struct` data type, which made us build a `customer` object.
If we wanted to pass the `customer` to a database, or an external system, it's often required that we convert this to `json` format.
## Dev Environment
The same as Part 1, we start with a [dockerfile](./dockerfile) where we declare our version of `go`.
```
cd golang\introduction\part-2.json
docker build --target dev . -t go
docker run -it -v ${PWD}:/work go sh
go version
```
## Create our App
Create a new directory that holds defines our `repository` and holds our `module`
```
mkdir videos
```
* Define a module path
```
# change directory to your application source code
cd videos
# create a go module file
go mod init videos
```
## Create our base code
We start out all our applications with a `main.go` defining our `package`, declaring our `import` dependencies <br/>
and our entrypoint `main()` function
```
package main
import "fmt"
func main() {
fmt.Println("Hello, world.")
}
```
## Create our Videos app
Firstly, we create a seperate code file `videos.go` that deals with our YouTube videos <br/>
Similar to Part 1, we define the file, we define what a video looks like using a `struct` <br/>
and a function for returning a slice of videos.
```
package main
import ()
type video struct {
Id string
Title string
Description string
Imageurl string
Url string
}
func getVideos()(videos []video){
//Get our videos here,
//and return them
return videos
}
```
## Populating our Video Struct
Similar to our customers app, we created a `struct` and populated it with data:
Let's add the following to our `getVideos()` function:
```
video1 := video{
Id : "eyvLwK5C2dw",
Title : "Kubernetes on Azure",
Imageurl : "https://i.ytimg.com/vi/eyvLwK5C2dw/mqdefault.jpg?sqp=CISC_PoF&rs=AOn4CLDo7kizrJozB0pxBhxL9JbyiW_EPw",
Url : "https://youtu.be/eyvLwK5C2dw",
Description : "",
}
video2 := video{
Id : "QThadS3Soig",
Title : "Kubernetes on Amazon",
Imageurl : "https://i.ytimg.com/vi/QThadS3Soig/sddefault.jpg",
Url : "https://youtu.be/QThadS3Soig",
Description : "",
}
return []video{ video1, video2}
```
We need to also invoke our function that will return the videos
Don't worry about where the videos will come from. <br/>
We always start with the building blocks and move on from there <br/>
In our `main()` function: <br/>
```
func main() {
videos := getVideos()
fmt.Println(videos)
}
```
## Files
Now, Ideally, we do not want to "hard code" our videos like this. <br/>
Currently, our videos are embedded into the code and we can only return 2 videos. <br/>
If we want to introduce a new video, we have to rebuild the application. <br/>
In the real world, videos are our data. <br/>
Data lives outside of the application. Like in a database.
Technically, we can use a database, but that is too big of a step for now. <br/>
So let's start with a file instead.
### Introducing ioutil
It's important to learn how to navigate and read go documentation.
Let's go to https://golang.org/pkg/io/ioutil/
We can import this library since its part of the Go standard library set.
```
import (
"io/ioutil"
)
```
Let's move our videos into a `json` file called `videos.json` :
```
[
{
"id" : "QThadS3Soig",
"title" : "Kubernetes on Amazon",
"imageurl" : "https://i.ytimg.com/vi/QThadS3Soig/sddefault.jpg",
"url" : "https://youtu.be/QThadS3Soig",
"description" : ""
},
{
"id" : "eyvLwK5C2dw",
"title" : "Kubernetes on Azure",
"imageurl" : "https://i.ytimg.com/vi/eyvLwK5C2dw/mqdefault.jpg?sqp=CISC_PoF&rs=AOn4CLDo7kizrJozB0pxBhxL9JbyiW_EPw",
"url" : "https://youtu.be/eyvLwK5C2dw",
"description" : ""
}
]
```
## Reading a file
Let's take another look at https://golang.org/pkg/io/ioutil/ <br/>
Notice the `ReadFile` function
We can read a file from disk:
```
fileBytes, err := ioutil.ReadFile("./videos.json")
if err != nil {
panic(err)
}
fileContent := string(fileBytes)
fmt.Println(fileContent)
```
## Panic
Also notice the function `panic`. <br/>
"A panic typically means something went unexpectedly wrong. Mostly we use it to fail fast on errors that shouldnt occur during normal operation, or that we arent prepared to handle gracefully."
- https://gobyexample.com/panic
For now, we will panic on every potential error. <br/>
In a future video, we'll cover Error handling in more depth
## JSON
Working with JSON is pretty straightforward in go. <br/>
`struct` objects are pretty well synergized with `json`
Let's take a look at the `json` package that is part of go.
https://golang.org/pkg/encoding/json/
```
"encoding/json"
```
This package allows us to marshal and unmarshal JSON. <br/>
This allows conversion between a `struct` or a go type, and `json`
## Unmarshal
From JSON(bytes) ==> Struct\Go type
```
err = json.Unmarshal(fileBytes, &videos)
if err != nil {
panic(err)
}
return videos
```
## Using our Data
Now in the real world, you would use your data to do something. <br/>
Let's say we'd like to update some common terms and conditions to the video description <br/>
In our `main` function, let's loop the videos & update the description.
Feel free to checkout Part 1 of the series on `loops`!
```
for _, video := range videos {
}
```
Now we dont want to override the description, we want to inject the terms and conditions on a new line
```
video.Description = video.Description + "\n Remember to Like & Subscribe!"
```
Note that when we run this, it does not print our new description field </br>
This is because the loop `range` is giving us a copy of the video object. </br>
This means we are updating a copy, but printing out the original. </br>
We need to use the loop indexer and update the original object in the slice. </br>
```
for i, _ := range videos {
videos[i].Description = videos[i].Description + "\n Remember to Like & Subscribe!"
}
```
Run our app again and now it updates the original video!
## Marshal
https://golang.org/pkg/encoding/json/#Marshal
From Struct\Go type ==> JSON(bytes)
Let's create a new function for saving our video back to file
```
func saveVideos(videos []video)(){
videoBytes, err := json.Marshal(videos)
if err != nil {
panic(err)
}
}
```
## Writing to File
https://golang.org/pkg/io/ioutil/
```
err = ioutil.WriteFile("./videos-updated.json", videoBytes, 0644)
if err != nil {
panic(err)
}
```
Then in our `main` function we can save our videos after the update:
```
saveVideos(videos)
```
## Mapping fields
In our example our `json` and our struct fields match exactly.
Sometimes this is not possible to maintain.
Sometimes we need to tell go, to map certain `json` fields to certain fields in our `struct`
We can do it like so:
```
type video struct {
Id string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Imageurl string `json:"imageurl"`
Url string `json:"url"`
}
```

View File

@ -1,3 +0,0 @@
module videos
go 1.15

View File

@ -1,17 +0,0 @@
package main
import "fmt"
func main() {
videos := getVideos()
for i, _ := range videos {
videos[i].Description = videos[i].Description + "\n Remember to Like & Subscribe!"
}
fmt.Println(videos)
saveVideos(videos)
}

View File

@ -1,16 +0,0 @@
[
{
"id": "QThadS3Soig",
"title": "Kubernetes on Amazon",
"description": "\n Remember to Like \u0026 Subscribe!",
"imageurl": "https://i.ytimg.com/vi/QThadS3Soig/sddefault.jpg",
"url": "https://youtu.be/QThadS3Soig"
},
{
"id": "eyvLwK5C2dw",
"title": "Kubernetes on Azure",
"description": "\n Remember to Like \u0026 Subscribe!",
"imageurl": "https://i.ytimg.com/vi/eyvLwK5C2dw/mqdefault.jpg?sqp=CISC_PoF\u0026rs=AOn4CLDo7kizrJozB0pxBhxL9JbyiW_EPw",
"url": "https://youtu.be/eyvLwK5C2dw"
}
]

View File

@ -1,44 +0,0 @@
package main
import (
"io/ioutil"
"encoding/json"
)
type video struct {
Id string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Imageurl string `json:"imageurl"`
Url string `json:"url"`
}
func getVideos()(videos []video){
fileBytes, err := ioutil.ReadFile("./videos.json")
if err != nil {
panic(err)
}
err = json.Unmarshal(fileBytes, &videos)
if err != nil {
panic(err)
}
return videos
}
func saveVideos(videos []video){
videoBytes, err := json.Marshal(videos)
if err != nil {
panic(err)
}
err = ioutil.WriteFile("./videos-updated.json", videoBytes, 0644)
if err != nil {
panic(err)
}
}

View File

@ -1,16 +0,0 @@
[
{
"id" : "QThadS3Soig",
"title" : "Kubernetes on Amazon",
"imageurl" : "https://i.ytimg.com/vi/QThadS3Soig/sddefault.jpg",
"url" : "https://youtu.be/QThadS3Soig",
"description" : ""
},
{
"id" : "eyvLwK5C2dw",
"title" : "Kubernetes on Azure",
"imageurl" : "https://i.ytimg.com/vi/eyvLwK5C2dw/mqdefault.jpg?sqp=CISC_PoF&rs=AOn4CLDo7kizrJozB0pxBhxL9JbyiW_EPw",
"url" : "https://youtu.be/eyvLwK5C2dw",
"description" : ""
}
]

View File

@ -1,14 +0,0 @@
FROM golang:1.15-alpine as dev
WORKDIR /work
FROM golang:1.15-alpine as build
WORKDIR /videos
COPY ./videos/* /videos/
RUN go build -o videos
FROM alpine as runtime
COPY --from=build /videos/videos /
COPY ./videos/videos.json /
CMD ./videos

View File

@ -1,435 +0,0 @@
# Introduction to Go: HTTP
<a href="https://youtu.be/MKkokYpGyTU" title="golang-part-3"><img src="https://i.ytimg.com/vi/MKkokYpGyTU/hqdefault.jpg" width="20%" alt="introduction to Go part 3" /></a>
HTTP is a fundamental part of Microservices and Web distributed systems <br/>
Go has a built in HTTP web server package. The package can be found [here](https://golang.org/pkg/net/http/) <br/>
We simply have to import the `http` package:
```
import (
"net/http"
)
```
[In part 1](../readme.md), we covered the fundamentals of writing basic Go <br/>
[In part 2](../part-2.json/readme.md), we've learn how to use basic data structures like `json` so we can send\receive data. <br/>
We'll be combining both these techniques so we can serve our `videos` data over a web endpoint.
As always, let's start with our `dockerfile` , `main.go` and `videos.go` we created in Part 2
## Dev Environment
The same as Part 1+2, we start with a [dockerfile](./dockerfile) where we declare our version of `go`.
```
cd golang\introduction\part-3.http
docker build --target dev . -t go
docker run -it -v ${PWD}:/work go sh
go version
```
## Create our App
Create a new directory that holds defines our `repository` and holds our `module`
```
mkdir videos
```
* Define a module path
```
# change directory to your application source code
cd videos
# create a go module file
go mod init videos
```
## Create our base code
We start out all our applications with a `main.go` defining our `package`, declaring our `import` dependencies <br/>
and our entrypoint `main()` function
```
package main
import "fmt"
func main() {
fmt.Println("Hello, world.")
}
```
## Create our Videos app
Firstly, we create a seperate code file `videos.go` that deals with our YouTube videos <br/>
The `videos.go` file defines what a video `struct` looks like, a `getVideos()` function to retrieve <br/>
videos list as a slice and a `saveVideos()` function to save videos to a file locally. <br/>
Let's copy the following content from Part 2 and create `videos.go` :
We want `videos.go` to be part of package main:
```
package main
```
We import 2 packages, 1 for reading and writing files, and another for dealing with `json`
```
import (
"io/ioutil"
"encoding/json"
)
```
Then we define what a video `struct` looks like:
```
type video struct {
Id string
Title string
Description string
Imageurl string
Url string
}
```
We have a function for retrieving `video` objects as a list of type `slice` :
```
func getVideos()(videos []video){
fileBytes, err := ioutil.ReadFile("./videos.json")
if err != nil {
panic(err)
}
err = json.Unmarshal(fileBytes, &videos)
if err != nil {
panic(err)
}
return videos
}
```
We also need to copy our `videos.json` file which contains our video data. <br/>
And finally, we have a function that accepts a list of type `slice` and stores the videos to a local file
```
func saveVideos(videos []video)(){
videoBytes, err := json.Marshal(videos)
if err != nil {
panic(err)
}
err = ioutil.WriteFile("./videos-updated.json", videoBytes, 0644)
if err != nil {
panic(err)
}
}
```
## HTTP Package
https://golang.org/pkg/net/http/
The HTTP package allows us to implement an HTTP client and a server.
A client is a component that makes HTTP calls.
A server is a component that receives or serves HTTP.
The HTTP package is capable of sending HTTP requests as well as defining a server
for receiving HTTP requests.
We can use this to run an HTTP server to serve files, or serve data, like an API.
Let's define a server in `main.go` :
```
# just one line :)
http.ListenAndServe(":8080", nil)
# ListenAndServe starts an HTTP server with a given address and handler.
# The handler is usually nil, which means to use DefaultServeMux.
# Handle and HandleFunc add handlers to DefaultServeMux
```
Now before we run this, since we're running in Docker, we want to exit the container <br/>
and rerun it, but this time open port `8080`
```
docker run -it -p 8080:8080 -v ${PWD}:/work go sh
cd videos
go run main.go
# you will notice the application pausing
```
We should see our server with a 404 on http://localhost:8080/
## Handle HTTP requests
In order to handle requests, we can tell the HTTP service that we want it to run a function </br>
for the request coming in.
We can see the `http` package has a `HandleFunc` function: https://golang.org/pkg/net/http/
To see this in action, lets create a `Hello()` function:
```
func Hello(){
}
```
And tell our `http` service to run it:
```
http.HandleFunc("/", Hello)
```
We cannot run this yet. As per `http` documentation, our `Hello` function needs to take in some inputs.
`func HandleFunc(pattern string, handler func(ResponseWriter, *Request))`
Therefore we need to add inputs to our function:
```
func Hello(w http.ResponseWriter, r *http.Request){
}
```
This allows us to get the request, its `body`, `headers` and a write where we can send a response.
Run this in the browser and you will notice the 404 goes away, but we now get an empty response.
## HTTP Response
Let's write a reponse to the incoming request. </br>
The response write has a `Write()` function that takes a bunch of bytes. <br/>
We can convert string to bytes by casting a `string` to a `[]byte` <br/> like:
`[]byte("Hello!")`. Let's convert it and write "Hello" to the response:
```
w.Write([]byte("Hello!"))
```
IF we run this code, we can see "Hello!" in the response body
## HTTP Headers
Headers play an important role in HTTP communication. </br>
Lets access all the headers of the incoming request!
If we look at the Header definition [here](https://golang.org/pkg/net/http), we can see how to access it.
Let's use the `for` loop we learnt in [part 1](../readme.md)
```
for i, value := range r.Header {
}
```
We learn't from our loop, we have in indexer and a value.
For `i`, we can rename it to header since it represents the header key in the dictionary.
And the `value` is the value of type `[]string`, containing the value of the header:
```
for header, value := range r.Header {
fmt.Printf("Key: %v \t Value: %v \n", header, value)
}
```
We can use `fmt` to print out the values and look at the headers.
We can also set headers on our response. <br/>
If we take a look at the `http` docs, we can see header is also a dictionary or strings.
```
w.Header().Add("TestHeader", "TestValue")
```
You can now see the headers in the response value if you use `curl` or your browser development tools
## HTTP Methods | GET
Web servers can serve data in a number of ways and support multiple type of HTTP methods.
`GET` is used to request data from a specified resource.
So far, our HTTP route for our Hello function is using the `GET` method.
Let's make our `GET` method more useful by serving our video data </br>
Let's rename our `Hello()` function to `HandleGetVideos()`. </br>
Our `/` route will return all videos:
```
videos := getVideos()
```
In [part 2](../part-2.json/readme.md) we covered `JSON`.
We need to convert our video `slice` of `struct`'s to `JSON` in order to return it to the client.
For this we learnt about the Marshall function:
Import the `JSON` package:
```
"encoding/json"
```
Convert our videos to `JSON` :
```
videoBytes, err := json.Marshal(videos)
if err != nil {
panic(err)
}
w.Write(videoBytes)
```
If we run this code and hit our `/` endpoint, we can now see `JSON` data being returned. <br/>
This is a core part of building an API in Go. <br/>
## HTTP Methods | POST
A `POST` method is used to send data to a server to create/update a resource. <br/>
Since we built a `saveVideos` function, lets use that so a client can update videos! <br/>
We need to define a new route, we can all it `/update` :
```
http.HandleFunc("/update", HandleUpdateVideos)
```
And we need to define an `HandleUpdateVideos()` function:
```
func HandleUpdateVideos(w http.ResponseWriter, r *http.Request){
}
```
Let's validate the request method to ensure its `POST`
We need to also ensure we send a status code to inform the user of method not allowed.
https://golang.org/pkg/net/http/#ResponseWriter
```
if r.Method == "POST" {
//update our videos here!
} else {
w.WriteHeader(405)
fmt.Fprintf(w, "Method not Supported!")
}
```
Now we need to accept `JSON` from the `POST` request body
https://golang.org/pkg/net/http/#Request
In the docs above, we can see the request Body is of type `Body io.ReadCloser`
To read that, we can use the `ioutil` package
https://golang.org/pkg/io/ioutil/
```
import "io/ioutil"
```
Then we can read the body into a `slice` of `bytes`:
```
body, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
```
Now that we have the body in a `[]byte`, we need to use our knowledge from [part 2](../part-2.json/readme.md) where we <br/>
convert `[]byte` to a `slice` of `video` items.
```
var videos []video
err = json.Unmarshal(body, &videos)
if err != nil {
panic(err)
}
```
Creating our video objects allows us to do some validation if we wanted to. <br/>
We can ensure the request body adheres to our API contract for this videos API. <br/>
So instead of calling `panic`, lets return a `400` Bad request status code if we cannot <br/>
Unmarshal the `JSON` data. This might help with some basic validation.
```
w.WriteHeader(400)
fmt.Fprintf(w, "Bad request")
```
And Finally, let's update our videos file! :
```
saveVideos(videos)
```
# Build our Docker container
Let's uncomment all the build lines in the `dockerfile`
Full `dockerfile` :
```
FROM golang:1.15-alpine as dev
WORKDIR /work
FROM golang:1.15-alpine as build
WORKDIR /videos
COPY ./videos/* /videos/
RUN go build -o videos
FROM alpine as runtime
COPY --from=build /videos/videos /
COPY ./videos/videos.json /
CMD ./videos
```
Build :
```
cd golang\introduction\part-3.http
docker build . -t videos
```
Run :
```
docker run -it -p 8080:8080 videos
```
## Things to know
* SSL for secure web connection
* Authentication
* Good API validation
* Support a backwards compatible contract (Inputs remain consistent)

View File

@ -1,3 +0,0 @@
module videos
go 1.15

View File

@ -1,54 +0,0 @@
package main
import (
"net/http"
"encoding/json"
"io/ioutil"
"fmt"
)
func main() {
http.HandleFunc("/", HandleGetVideos)
http.HandleFunc("/update", HandleUpdateVideos)
http.ListenAndServe(":8080", nil)
}
func HandleGetVideos(w http.ResponseWriter, r *http.Request){
videos := getVideos()
videoBytes, err := json.Marshal(videos)
if err != nil {
panic(err)
}
w.Write(videoBytes)
}
func HandleUpdateVideos(w http.ResponseWriter, r *http.Request){
if r.Method == "POST" {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
var videos []video
err = json.Unmarshal(body, &videos)
if err != nil {
w.WriteHeader(400)
fmt.Fprintf(w, "Bad request")
}
saveVideos(videos)
} else {
w.WriteHeader(405)
fmt.Fprintf(w, "Method not Supported!")
}
}

View File

@ -1,16 +0,0 @@
[
{
"Id": "QThadS3Soig",
"Title": "Kubernetes on Amazon",
"Description": "TEST",
"Imageurl": "https://i.ytimg.com/vi/QThadS3Soig/sddefault.jpg",
"Url": "https://youtu.be/QThadS3Soig"
},
{
"Id": "eyvLwK5C2dw",
"Title": "Kubernetes on Azure",
"Description": "TEST",
"Imageurl": "https://i.ytimg.com/vi/eyvLwK5C2dw/mqdefault.jpg?sqp=CISC_PoF\u0026rs=AOn4CLDo7kizrJozB0pxBhxL9JbyiW_EPw",
"Url": "https://youtu.be/eyvLwK5C2dw"
}
]

View File

@ -1,46 +0,0 @@
package main
import (
"io/ioutil"
"encoding/json"
)
type video struct {
Id string
Title string
Description string
Imageurl string
Url string
}
func getVideos()(videos []video){
fileBytes, err := ioutil.ReadFile("./videos.json")
if err != nil {
panic(err)
}
err = json.Unmarshal(fileBytes, &videos)
if err != nil {
panic(err)
}
return videos
}
func saveVideos(videos []video)(){
videoBytes, err := json.Marshal(videos)
if err != nil {
panic(err)
}
err = ioutil.WriteFile("./videos-updated.json", videoBytes, 0644)
if err != nil {
panic(err)
}
}

View File

@ -1,16 +0,0 @@
[
{
"id" : "QThadS3Soig",
"title" : "Kubernetes on Amazon",
"imageurl" : "https://i.ytimg.com/vi/QThadS3Soig/sddefault.jpg",
"url" : "https://youtu.be/QThadS3Soig",
"description" : ""
},
{
"id" : "eyvLwK5C2dw",
"title" : "Kubernetes on Azure",
"imageurl" : "https://i.ytimg.com/vi/eyvLwK5C2dw/mqdefault.jpg?sqp=CISC_PoF&rs=AOn4CLDo7kizrJozB0pxBhxL9JbyiW_EPw",
"url" : "https://youtu.be/eyvLwK5C2dw",
"description" : ""
}
]

View File

@ -1,17 +0,0 @@
FROM golang:1.15-alpine as dev
WORKDIR /work
# FROM golang:1.15-alpine as build
# WORKDIR /videos
# COPY ./videos/* /videos/
# RUN go build -o videos
# FROM alpine as runtime
# COPY --from=build /videos/videos /usr/local/bin/videos
# COPY ./videos/videos.json /
# COPY run.sh /
# RUN chmod +x /run.sh
# ENTRYPOINT [ "./run.sh" ]

View File

@ -1,418 +0,0 @@
# Introduction to Go: Command Line
<a href="https://youtu.be/CODqM_rzwtk" title="golang-part-4"><img src="https://i.ytimg.com/vi/CODqM_rzwtk/hqdefault.jpg" width="20%" alt="introduction to Go part 4" /></a>
Command line apps are a fundamental part of software development <br/>
Go has a built in Commandline parser package. The package can be found [here](https://golang.org/pkg/flag/) <br/>
We simply have to import the `flag` package:
```
import (
"flag"
)
```
[In part 1](../readme.md), we covered the fundamentals of writing basic Go <br/>
[In part 2](../part-2.json/readme.md), we've learn how to use basic data structures like `json` so we can send\receive data. <br/>
[Part 3](../part-3.http/readme.md) was about exposing data via a Web server.
We'll be combining these techniques so we can serve our `videos` data over a commandline application.
As always, let's start with our `dockerfile` , `main.go` and `videos.go` we created in Part 2
## Dev Environment
The same as Part 1+2+3, we start with a [dockerfile](./dockerfile) where we declare our version of `go`.
```
cd golang\introduction\part-4.commandline
docker build --target dev . -t go
docker run -it -v ${PWD}:/work go sh
go version
```
## Create our App
Create a new directory that holds defines our `repository` and holds our `module`
```
mkdir videos
cd videos
```
* Define a module path
```
# create a go module file
go mod init videos
```
## Create our base code
We start out all our applications with a `main.go` defining our `package`, declaring our `import` dependencies <br/>
and our entrypoint `main()` function
```
package main
import "fmt"
func main() {
fmt.Println("Hello, world.")
}
```
## Create our Videos app
Firstly, we create a seperate code file `videos.go` that deals with our YouTube videos <br/>
The `videos.go` file defines what a video `struct` looks like, a `getVideos()` function to retrieve <br/>
videos list as a slice and a `saveVideos()` function to save videos to a file locally. <br/>
Let's copy the following content from Part 2 and create `videos.go` :
We want `videos.go` to be part of package main:
```
package main
```
We import 2 packages, 1 for reading and writing files, and another for dealing with `json`
```
import (
"io/ioutil"
"encoding/json"
)
```
Then we define what a video `struct` looks like:
```
type video struct {
Id string
Title string
Description string
Imageurl string
Url string
}
```
We have a function for retrieving `video` objects as a list of type `slice` :
```
func getVideos()(videos []video){
fileBytes, err := ioutil.ReadFile("./videos.json")
if err != nil {
panic(err)
}
err = json.Unmarshal(fileBytes, &videos)
if err != nil {
panic(err)
}
return videos
}
```
We also need to copy our `videos.json` file which contains our video data. <br/>
And finally, we have a function that accepts a list of type `slice` and stores the videos to a local file
```
func saveVideos(videos []video)(){
videoBytes, err := json.Marshal(videos)
if err != nil {
panic(err)
}
err = ioutil.WriteFile("./videos.json", videoBytes, 0644)
if err != nil {
panic(err)
}
}
```
## Flag Package
https://golang.org/pkg/flag/
Package flag implements command-line flag parsing.
So we can run our videos app and pass it inputs like any other CLI.
Let's build a CLI tool that users our `getVideos()` and `saveVideos` functions.
To get all videos, perhaps we'd like a command
```
# get all videos
videos get --all
# get video by ID
videos get -id <video-id>
# add a video to our list
videos add -id -title -url -imageurl -desc
```
To start, we import package `flag`
```
import (
"flag"
"fmt"
)
```
Let's define our subcommands in `main.go` :
```
//'videos get' subcommand
getCmd := flag.NewFlagSet("get", flag.ExitOnError)
```
`videos get` command will need two inputs, `--all` if the user wants to return all videos, or `--id` if the user only wants a specific video
```
// inputs for `videos get` command
getAll := getCmd.Bool("all", false, "Get all videos")
getID := getCmd.String("id", "", "YouTube video ID")
```
`videos add` command will take a bit more inputs to be able to add a video to our storage.
```
addCmd := flag.NewFlagSet("add", flag.ExitOnError)
addID := addCmd.String("id", "", "YouTube video ID")
addTitle := addCmd.String("title", "", "YouTube video Title")
addUrl := addCmd.String("url", "", "YouTube video URL")
addImageUrl := addCmd.String("imageurl", "", "YouTube video Image URL")
addDesc := addCmd.String("desc", "", "YouTube video description")
```
When a user runs our videos CLI tool, we may need to validate that
our application receives the right subcommands. So lets ensure a simple validation to check if the user has passed a subcommand
To check the arguments passed to our CLI, we use the ["os"](https://golang.org/pkg/os/) package. Check the Args variable, it holds usefull information passed to our application including its name.
`var Args []string`
We can do a simple check by ensuring the length of `os.Args` is atleast 2.
We firstly need to add `os` to our import section </br>
Followed by our check:
```
if len(os.Args) < 2 {
fmt.Println("expected 'get' or 'add' subcommands")
os.Exit(1)
}
```
## Handling our subcommands
So to handle each sub command like `get` and `add`, we add a simple
`switch` statement that can branch into different pathways of execution,
based on a variables content.
Let's take a look at this simple `switch` statement:
```
//look at the 2nd argument's value
switch os.Args[1] {
case "get": // if its the 'get' command
//hande get here
case "add": // if its the 'add' command
//hande add here
default: // if we don't understand the input
}
```
Let's create seperate handler functions for each sub command to keep our code tidy:
```
func HandleGet(getCmd *flag.FlagSet, all *bool, id *string){
}
func HandleAdd(addCmd *flag.FlagSet,id *string, title *string, url *string, imageUrl *string, description *string ){
}
```
Now that we have seperate functions for each subcommand, we can take appropriate actions in each. Let's firstly parse the command flags for each subcommand:
This allows us to parse everything after the `videos <subcommand>` arguments:
```
getCmd.Parse(os.Args[2:])
```
## Input Validation
For our `HandleGet` function, let's validate input to ensure its correct.
```
if *all == false && *id == "" {
fmt.Print("id is required or specify --all for all videos")
getCmd.PrintDefaults()
os.Exit(1)
}
```
Let's handle the scenario if user passing `--all` flag:
```
if *all {
//return all videos
videos := getVideos()
fmt.Printf("ID \t Title \t URL \t ImageURL \t Description \n")
for _, video := range videos {
fmt.Printf("%v \t %v \t %v \t %v \t %v \n",video.Id, video.Title, video.Url, video.Imageurl,video.Description)
}
return
}
```
Let's handle when user is searching for a video by ID
```
if *id != "" {
videos := getVideos()
id := *id
for _, video := range videos {
if id == video.Id {
fmt.Printf("ID \t Title \t URL \t ImageURL \t Description \n")
fmt.Printf("%v \t %v \t %v \t %v \t %v \n",video.Id, video.Title, video.Url, video.Imageurl,video.Description)
}
}
}
```
## Parsing multiple fields
For our `HandleAdd` function, we need to validate multiple inputs, create a `video` struct, append it to the existing video list and save it back to file
Let's create a `ValidateVideo()` function with similar inputs to our `HandleAdd()`:
```
func ValidateVideo(addCmd *flag.FlagSet,id *string, title *string, url *string, imageUrl *string, description *string ){
}
```
Let's simply validate all fields since they are all required:
```
if *id == "" || *title == "" || *url == "" || *imageUrl == "" || *description == "" {
fmt.Print("all fields are required for adding a video")
addCmd.PrintDefaults()
os.Exit(1)
}
```
And we can now call this function in our add function:
```
ValidateVideo(addCmd, id,title,url, imageUrl, description)
```
## Adding our video
Now that we have some basic validation, not perfect, but good enough to get started, let's add our video to the existing file.
Define a video struct with the CLI input:
```
video := video{
Id: *id,
Title: *title,
Description: *description,
Imageurl: *imageUrl,
Url: *url,
}
```
Get the existing videos:
```
videos := getVideos()
```
Append our video to the list:
```
videos = append(videos,video)
```
Save the new updated video list:
```
saveVideos(videos)
```
# Build our Docker container
Let's uncomment all the build lines in the `dockerfile`
Full `dockerfile` :
```
FROM golang:1.15-alpine as dev
WORKDIR /work
FROM golang:1.15-alpine as build
WORKDIR /videos
COPY ./videos/* /videos/
RUN go build -o videos
FROM alpine as runtime
COPY --from=build /videos/videos /usr/local/bin/videos
COPY ./videos/videos.json /
COPY run.sh /
RUN chmod +x /run.sh
ENTRYPOINT [ "./run.sh" ]
```
For our entrypoint, we need to create a shell script to accept all the arguments:
Let's create a script called `run.sh`
```
#!/bin/sh
videos $@
```
Build :
```
cd golang\introduction\part-4.commandline
docker build . -t videos
```
Run :
```
docker run -it videos get --help
```

View File

@ -1,3 +0,0 @@
#!/bin/sh
videos $@

View File

@ -1,3 +0,0 @@
module videos
go 1.15

View File

@ -1,111 +0,0 @@
package main
import (
"flag"
"fmt"
"os"
)
func main() {
//'videos get' subcommand
getCmd := flag.NewFlagSet("get", flag.ExitOnError)
// inputs for `videos get` command
getAll := getCmd.Bool("all", false, "Get all videos")
getID := getCmd.String("id", "", "YouTube video ID")
//'videos add' subcommand
addCmd := flag.NewFlagSet("add", flag.ExitOnError)
// inputs for `videos add` command
addID := addCmd.String("id", "", "YouTube video ID")
addTitle := addCmd.String("title", "", "YouTube video Title")
addUrl := addCmd.String("url", "", "YouTube video URL")
addImageUrl := addCmd.String("imageurl", "", "YouTube video Image URL")
addDesc := addCmd.String("desc", "", "YouTube video description")
if len(os.Args) < 2 {
fmt.Println("expected 'get' or 'add' subcommands")
os.Exit(1)
}
//look at the 2nd argument's value
switch os.Args[1] {
case "get": // if its the 'get' command
HandleGet(getCmd, getAll, getID)
case "add": // if its the 'add' command
HandleAdd(addCmd, addID,addTitle,addUrl, addImageUrl, addDesc)
default: // if we don't understand the input
}
}
func HandleGet(getCmd *flag.FlagSet, all *bool, id *string){
getCmd.Parse(os.Args[2:])
if *all == false && *id == "" {
fmt.Print("id is required or specify --all for all videos")
getCmd.PrintDefaults()
os.Exit(1)
}
if *all {
//return all videos
videos := getVideos()
fmt.Printf("ID \t Title \t URL \t ImageURL \t Description \n")
for _, video := range videos {
fmt.Printf("%v \t %v \t %v \t %v \t %v \n",video.Id, video.Title, video.Url, video.Imageurl,video.Description)
}
return
}
if *id != "" {
videos := getVideos()
id := *id
for _, video := range videos {
if id == video.Id {
fmt.Printf("ID \t Title \t URL \t ImageURL \t Description \n")
fmt.Printf("%v \t %v \t %v \t %v \t %v \n",video.Id, video.Title, video.Url, video.Imageurl,video.Description)
}
}
}
}
func ValidateVideo(addCmd *flag.FlagSet,id *string, title *string, url *string, imageUrl *string, description *string ){
addCmd.Parse(os.Args[2:])
if *id == "" || *title == "" || *url == "" || *imageUrl == "" || *description == "" {
fmt.Print("all fields are required for adding a video")
addCmd.PrintDefaults()
os.Exit(1)
}
}
func HandleAdd(addCmd *flag.FlagSet,id *string, title *string, url *string, imageUrl *string, description *string ){
ValidateVideo(addCmd, id,title,url, imageUrl, description)
video := video{
Id: *id,
Title: *title,
Description: *description,
Imageurl: *imageUrl,
Url: *url,
}
videos := getVideos()
videos = append(videos,video)
saveVideos(videos)
}

View File

@ -1,45 +0,0 @@
package main
import (
"io/ioutil"
"encoding/json"
)
type video struct {
Id string
Title string
Description string
Imageurl string
Url string
}
func getVideos()(videos []video){
fileBytes, err := ioutil.ReadFile("./videos.json")
if err != nil {
panic(err)
}
err = json.Unmarshal(fileBytes, &videos)
if err != nil {
panic(err)
}
return videos
}
func saveVideos(videos []video)(){
videoBytes, err := json.Marshal(videos)
if err != nil {
panic(err)
}
err = ioutil.WriteFile("./videos.json", videoBytes, 0644)
if err != nil {
panic(err)
}
}

View File

@ -1,16 +0,0 @@
[
{
"Id": "QThadS3Soig",
"Title": "Kubernetes on Amazon",
"Description": "",
"Imageurl": "https://i.ytimg.com/vi/QThadS3Soig/sddefault.jpg",
"Url": "https://youtu.be/QThadS3Soig"
},
{
"Id": "eyvLwK5C2dw",
"Title": "Kubernetes on Azure",
"Description": "",
"Imageurl": "https://i.ytimg.com/vi/eyvLwK5C2dw/mqdefault.jpg?sqp=CISC_PoF\u0026rs=AOn4CLDo7kizrJozB0pxBhxL9JbyiW_EPw",
"Url": "https://youtu.be/eyvLwK5C2dw"
}
]

View File

@ -1,13 +0,0 @@
FROM golang:1.15-alpine as dev
WORKDIR /work
FROM golang:1.15-alpine as build
WORKDIR /videos
COPY ./videos/* /videos/
RUN go build -o videos
FROM alpine as runtime
COPY --from=build /videos/videos /
CMD ./videos

View File

@ -1,559 +0,0 @@
# Introduction to Go: Storing data in Redis Database
<a href="https://youtu.be/6lJCyKwoQaQ" title="golang-part-5"><img src="https://i.ytimg.com/vi/6lJCyKwoQaQ/hqdefault.jpg" width="20%" alt="introduction to Go part 5" /></a>
Up until now, we've learned the fundamentals of Go and built a small web microservice that handles our video data.
Our service has a `/` `GET` endpoint for returning all videos, as well as a simple `/update` endpoint for updating our list of videos.
We've learnt how to read and write from files and learn how to work with `json` data. </br>
This is important for learning Go, however there are a few challenges for using a file as storage. <br/>
* It can be problematic if we have more than one instance of our service writing to the same file
* It's important to keep state out of our application, so if our application crashes, we don't lose our data
[In part 1](../readme.md), we covered the fundamentals of writing basic Go <br/>
[In part 2](../part-2.json/readme.md), we've learnt how to use basic data structures like `json` so we can send\receive data. <br/>
[In part 3](../part-3.http/readme.md), we've learnt how to write a HTTP service to expose our videos data <br/>
## Start up a Redis Cluster
Follow my Redis clustering Tutorial </br>
<a href="https://youtube.com/playlist?list=PLHq1uqvAteVtlgFkmOlIqWro3XP26y_oW" title="Redis"><img src="https://i.ytimg.com/vi/L3zp347cWNw/hqdefault.jpg" width="30%" alt="Redis Guide" /></a>
Code is over [here](../../../storage/redis/clustering/readme.md)
## Go Dev Environment
The same as Part 1+2+3+4, we start with a [dockerfile](./dockerfile) where we declare our version of `go`.
The `dockerfile`:
```
FROM golang:1.15-alpine as dev
WORKDIR /work
```
Let's build and start our container:
```
cd golang\introduction\part-5.database.redis
docker build --target dev . -t go
docker run -it -p 80:80 -v ${PWD}:/work go sh
go version
```
## Our application
Create a new directory that holds defines our `repository` and holds our `module`
```
mkdir videos
```
* Define a module path
```
# change directory to your application source code
cd videos
# create a go module file
go mod init videos
```
## Create our base code
We start out all our applications with a `main.go` defining our `package`, declaring our `import` dependencies <br/>
and our entrypoint `main()` function
```
package main
import (
"net/http"
"encoding/json"
"io/ioutil"
"fmt"
)
func main() {
http.HandleFunc("/", HandleGetVideos)
http.HandleFunc("/update", HandleUpdateVideos)
http.ListenAndServe(":80", nil)
}
```
Now before we write these handler functions, we need our videos application
## Create our Videos app
Firstly, we create a separate code file `videos.go` that deals with our YouTube videos <br/>
The `videos.go` file defines what a video `struct` looks like, a `getVideos()` function to retrieve <br/>
videos list as a slice and a `saveVideos()` function to save videos to a file locally. <br/>
We start with our dependencies. <br>
We import 2 packages, 1 for reading and writing files, and another for dealing with `json`
```
package main
import (
"io/ioutil"
"encoding/json"
)
```
Then we define what a video `struct` looks like:
```
type video struct {
Id string
Title string
Description string
Imageurl string
Url string
}
```
We have a function for retrieving `video` objects as a list of type `slice` :
```
func getVideos()(videos []video){
fileBytes, err := ioutil.ReadFile("./videos.json")
if err != nil {
panic(err)
}
err = json.Unmarshal(fileBytes, &videos)
if err != nil {
panic(err)
}
return videos
}
```
We also need to copy our `videos.json` file which contains our video data. <br/>
And finally, we have a function that accepts a list of type `slice` and stores the videos to a local file
```
func saveVideos(videos []video)(){
videoBytes, err := json.Marshal(videos)
if err != nil {
panic(err)
}
err = ioutil.WriteFile("./videos.json", videoBytes, 0644)
if err != nil {
panic(err)
}
}
```
## HTTP Handlers
Now we have to define our handler functions `HandleGetVideos` and `HandleUpdateVideos`,
```
func HandleGetVideos(w http.ResponseWriter, r *http.Request){
videos := getVideos()
videoBytes, err := json.Marshal(videos)
if err != nil {
panic(err)
}
w.Write(videoBytes)
}
func HandleUpdateVideos(w http.ResponseWriter, r *http.Request){
if r.Method == "POST" {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
var videos []video
err = json.Unmarshal(body, &videos)
if err != nil {
w.WriteHeader(400)
fmt.Fprintf(w, "Bad request")
}
saveVideos(videos)
} else {
w.WriteHeader(405)
fmt.Fprintf(w, "Method not Supported!")
}
}
```
Now so far, we have a web HTTP application that can list and update our youtube videos. <br/>
Let's build and run it
```
go build
./videos
```
If we head over to `http://localhost` in the browser we can see our application returns our 2 videos. </br>.
We can use tools like PostMan to generate a `POST` request to update our videos too.
## Redis Go Package
Now instead of reading and writing our records to a `json` file, we are going to read and write records to Redis.
https://github.com/go-redis/redis/
```
go get github.com/go-redis/redis/v8
```
And to use the library, we have to import it
```
import (
"github.com/go-redis/redis/v8"
)
```
Now to connect to any database, you're going to need a bit of information:
* Database Address
* Database Port
* Database Username\Password
We can define these as environment variables and read those in our code
```
import (
"os"
)
//main.go (global variables)
var redis_sentinels = os.Getenv("REDIS_SENTINELS")
var redis_master = os.Getenv("REDIS_MASTER_NAME")
var redis_password = os.Getenv("REDIS_PASSWORD")
```
We define an empty context and Redis client
Context can be used to control timeouts and deadlines for our application. We can set up an empty context for now.
```
import (
"context"
)
var ctx = context.Background()
var redisClient *redis.Client
```
## Redis Sentinel Client
https://redis.uptrace.dev/guide/sentinel.html#redis-server-client
```
import (
"strings"
)
sentinelAddrs := strings.Split(redis_sentinels, ",")
rdb := redis.NewFailoverClient(&redis.FailoverOptions{
MasterName: redis_master,
SentinelAddrs: sentinelAddrs,
Password: redis_password,
DB: 0,
})
redisClient = rdb
rdb.Ping(ctx)
```
We can also add the `Ping()` to our handler functions to ensure it can connect using the global redis client variable
## Networking
Now we need to remember our go container may not be able to talk to the redis containers because they are running on different networks.
If you took note, we started our Redis containers on a `redis` network by passing `--net redis` as a flag to our `docker run` commands. </br>
Let's restart our Go container on the same network
We also need to set our `ENV` variables to point our container to the redis sentinels. </br>
If we look at our sentinel configuration, our master alias is set to `mymaster`
```
docker run -it -p 80:80 `
--net redis `
-e REDIS_SENTINELS="sentinel-0:5000,sentinel-1:5000,sentinel-2:5000" `
-e REDIS_MASTER_NAME="mymaster" `
-e REDIS_PASSWORD="a-very-complex-password-here" `
-v ${PWD}:/work go sh
```
We can now observe our container is connected to Redis. </br>
Our application: [http://localhost](http://localhost)
# Store our Data
So now we can store our video data in Redis instead of a local `json` file. </br>
We'll write the `json` document of a video (struct) to Redis. </br>
Let's create a `saveVideo()` function that stores a single record.
```
func saveVideo(video video)(){
videoBytes, err := json.Marshal(video)
if err != nil {
panic(err)
}
err = redisClient.Set(ctx, video.Id, videoBytes, 0).Err()
if err != nil {
panic(err)
}
}
```
Now since we have one endpoint that saves all videos, we need to update it to save each video it has.
```
func saveVideos(videos []video)(){
for _, video := range videos {
saveVideo(video)
}
}
```
To get the videos, let's create a function to retrieve a single record:
```
func getVideo(id string)(video video) {
value, err := redisClient.Get(ctx, id).Result()
if err != nil {
panic(err)
}
if err != redis.Nil {
err = json.Unmarshal([]byte(value), &video)
}
return video
}
```
And finally we need to update our `GetVideos()` function to loop all the video keys and return all videos
```
func getVideos()(videos []video){
keys, err := redisClient.Keys(ctx,"*").Result()
if err != nil {
panic(err)
}
for _, key := range keys {
video := getVideo(key)
videos = append(videos, video)
}
return videos
}
```
And we need all our imports
```
import (
"encoding/json"
"github.com/go-redis/redis/v8"
)
```
Now if we rebuild our all and access it, we get `null` as there are no videos in Redis. Let's add two!
Let's `POST` the following using PostMan to url `http://localhost/update`
```
[
{
"id" : "QThadS3Soig",
"title" : "Kubernetes on Amazon",
"imageurl" : "https://i.ytimg.com/vi/QThadS3Soig/sddefault.jpg",
"url" : "https://youtu.be/QThadS3Soig",
"description" : "TEST"
},
{
"id" : "eyvLwK5C2dw",
"title" : "Kubernetes on Azure",
"imageurl" : "https://i.ytimg.com/vi/eyvLwK5C2dw/mqdefault.jpg?sqp=CISC_PoF&rs=AOn4CLDo7kizrJozB0pxBhxL9JbyiW_EPw",
"url" : "https://youtu.be/eyvLwK5C2dw",
"description" : "TEST"
}
]
```
If we refresh our page, we can now see two records!
# Improvements
Now that you have the fundamental knowledge of HTTP and Redis, </br>
you can update the code to retrieve 1 video by ID, or delete a video by ID. </br>
You can add search functionality and more! </br>
Let's update our `/ GET` handler to be able to return a single video
```
func HandleGetVideos(w http.ResponseWriter, r *http.Request){
id, ok := r.URL.Query()["id"]
if ok {
videoID := id[0]
video := getVideo(videoID)
if video.Id == "" { //video not found, or empty ID
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("{}"))
return
}
videoBytes, err := json.Marshal(video)
if err != nil {
panic(err)
}
w.Write(videoBytes)
return
}
videos := getVideos()
videoBytes, err := json.Marshal(videos)
if err != nil {
panic(err)
}
w.Write(videoBytes)
}
```
We can also update our `/update POST` endpoint to be able to update a single video
```
func HandleUpdateVideos(w http.ResponseWriter, r *http.Request){
if r.Method == "POST" {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
_, ok := r.URL.Query()["id"]
if ok {
var video video
err = json.Unmarshal(body, &video)
if err != nil {
w.WriteHeader(400)
fmt.Fprintf(w, "Bad request")
}
saveVideo(video)
return
}
var videos []video
err = json.Unmarshal(body, &videos)
if err != nil {
w.WriteHeader(400)
fmt.Fprintf(w, "Bad request")
}
saveVideos(videos)
return
} else {
w.WriteHeader(405)
fmt.Fprintf(w, "Method not Supported!")
}
}
```
# Build our Docker container
Full `dockerfile` :
```
FROM golang:1.15-alpine as dev
WORKDIR /work
FROM golang:1.15-alpine as build
WORKDIR /videos
COPY ./videos/* /videos/
RUN go build -o videos
FROM alpine as runtime
COPY --from=build /videos/videos /
CMD ./videos
```
Build :
```
cd golang\introduction\part-5.database.redis
docker build . -t videos
```
Run :
```
docker run -it -p 80:80 `
--net redis `
-e REDIS_SENTINELS="sentinel-0:5000,sentinel-1:5000,sentinel-2:5000" `
-e REDIS_MASTER_NAME="mymaster" `
-e REDIS_PASSWORD="a-very-complex-password-here" `
videos
```

View File

@ -1,5 +0,0 @@
module videos
go 1.15
require github.com/go-redis/redis/v8 v8.11.4

View File

@ -1,89 +0,0 @@
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

View File

@ -1,113 +0,0 @@
package main
import (
"net/http"
"encoding/json"
"io/ioutil"
"fmt"
"os"
"context"
"strings"
"github.com/go-redis/redis/v8"
)
var ctx = context.Background()
var redisClient *redis.Client
func main() {
var redis_sentinels = os.Getenv("REDIS_SENTINELS")
var redis_master = os.Getenv("REDIS_MASTER_NAME")
var redis_password = os.Getenv("REDIS_PASSWORD")
sentinelAddrs := strings.Split(redis_sentinels, ",")
rdb := redis.NewFailoverClient(&redis.FailoverOptions{
MasterName: redis_master,
SentinelAddrs: sentinelAddrs,
Password: redis_password,
DB: 0,
})
redisClient = rdb
rdb.Ping(ctx)
http.HandleFunc("/", HandleGetVideos)
http.HandleFunc("/update", HandleUpdateVideos)
http.ListenAndServe(":80", nil)
}
func HandleGetVideos(w http.ResponseWriter, r *http.Request){
id, ok := r.URL.Query()["id"]
if ok {
videoID := id[0]
video := getVideo(videoID)
if video.Id == "" { //video not found, or empty ID
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("{}"))
return
}
videoBytes, err := json.Marshal(video)
if err != nil {
panic(err)
}
w.Write(videoBytes)
return
}
videos := getVideos()
videoBytes, err := json.Marshal(videos)
if err != nil {
panic(err)
}
w.Write(videoBytes)
}
func HandleUpdateVideos(w http.ResponseWriter, r *http.Request){
if r.Method == "POST" {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
}
_, ok := r.URL.Query()["id"]
if ok {
var video video
err = json.Unmarshal(body, &video)
if err != nil {
w.WriteHeader(400)
fmt.Fprintf(w, "Bad request")
}
saveVideo(video)
return
}
var videos []video
err = json.Unmarshal(body, &videos)
if err != nil {
w.WriteHeader(400)
fmt.Fprintf(w, "Bad request")
}
saveVideos(videos)
return
} else {
w.WriteHeader(405)
fmt.Fprintf(w, "Method not Supported!")
}
}

View File

@ -1,64 +0,0 @@
package main
import (
"encoding/json"
"github.com/go-redis/redis/v8"
)
type video struct {
Id string
Title string
Description string
Imageurl string
Url string
}
func getVideos()(videos []video){
keys, err := redisClient.Keys(ctx,"*").Result()
if err != nil {
panic(err)
}
for _, key := range keys {
video := getVideo(key)
videos = append(videos, video)
}
return videos
}
func getVideo(id string)(video video) {
value, err := redisClient.Get(ctx, id).Result()
if err != nil {
panic(err)
}
if err != redis.Nil {
err = json.Unmarshal([]byte(value), &video)
}
return video
}
func saveVideo(video video)(){
videoBytes, err := json.Marshal(video)
if err != nil {
panic(err)
}
err = redisClient.Set(ctx, video.Id, videoBytes, 0).Err()
if err != nil {
panic(err)
}
}
func saveVideos(videos []video)(){
for _, video := range videos {
saveVideo(video)
}
}

View File

@ -1,431 +0,0 @@
# Introduction to Learning Go
<a href="https://youtu.be/jpKysZwllVw" title="golang-part-1"><img src="https://i.ytimg.com/vi/jpKysZwllVw/hqdefault.jpg" width="20%" alt="introduction to Go part 1" /></a>
Go can be downloaded from [golang.org](https://golang.org/doc/install) <br/>
Test your `go` installation:
```
go version
```
# Run Go in Docker
We can also run go in a small docker container: <br/>
```
cd golang\introduction
docker build --target dev . -t go
docker run -it -v ${PWD}:/work go sh
go version
```
# Code Structure
https://golang.org/doc/code.html
* Package:
- Source files in same directory that are compiled together
- Have visibility on all source files in the same package
* Modules:
- Collection of packages that are released together
Our repository can contain one or more go modules, but usually 1.
- At the root of the repo
`go.mod` Declares module path + import path for packages. (Where to download them)
- When we write our own program, we can define a module path
- This allows us to publish our code (if we want), so others can download it
- The module path could be something like `github.com/google/go-cmp`
- Makes it easy for other programs to consume our module
# Our first Program
* Create a folder containing our application
```
mkdir app
```
* Define a module path (github.com/docker-development-youtube-series/golang/introdouction/app)
```
# change directory to your application source code
cd app
# create a go module file
go mod init github.com/docker-development-youtube-series/golang/introdouction/app
```
* Create basic source code
In the `app` folder, create a program called `app.go`
Paste the following content into it.
```
package main
import "fmt"
func main() {
fmt.Println("Hello, world.")
}
```
* Run your application code
You can run your application
```
go run app.go
```
# Building our Program
Build your application into a static binary: <br/>
```
go build
```
This will produce a compiled program called `app`
You can run this program easily:
```
./app
```
# Install your application (optional)
"This command builds the app command, producing an executable binary. <br/>
It then installs that binary as $HOME/go/bin/app (or, under Windows, %USERPROFILE%\go\bin\app.exe)"
```
go install github.com/docker-development-youtube-series/golang/introdouction/app
```
# The Code
## Functions
In the video we cover writing functions. </br>
It allows us to execute a block of code <br/>
You want to give your function a single purpose <br/>
Functions can have an input and return an output <br/>
Well thought out functions makes it easier to write tests <br/>
Instead of doing a boring `x + y` function that adds two numbers, let's do something a little
more realistic but still basic:
```
// This function returns some data
// The data could be coming from a database, or a file.
// The person calling the function should not care
// Since the function does not leak its Data provider
func getData(inputs)(outputs){
}
// functions can take multiple inputs, and return multiple outputs
// lets say we have 1) customers and 2) the cities they are from
// we may want to 1) get a list of customers and 2) get a list of cities
// therefore we have 2 types of data, 1) customers 2) cities
// let's improve our function so its gets data based on the type
func getData(customerId int) (customer string) {
}
```
## Variables
To hold data in programming languages, we use variables. <br/>
Variables take up space in memory, so we want to keep it minimal. <br/>
Let's declare variables in our function
```
func getData(customerId int) (customer string) {
var firstName = "Marcel"
lastName := "Dempers"
fullName := firstName + " " + lastName
return fullName
//or we can return the computation instead of adding another variable!
return firstName + " " + lastName
//or we dont even need to declare variables :)
return "Marcel Dempers"
}
```
## Control Flows (if\else)
You can see we're not using the `customerId` input in our function. <br/>
Let's use it! <br/>
Control flows allow us to add "rules" to our code. </br>
"If this is the case, then do that, else do something else".
So let's say we have a customer ID 1 coming in, we may only want to
return our customer if it matches the `customerId`
```
func getData(customerId int) (customer string) {
if customerId == 1 {
return "Marcel Dempers"
} else if customerId == 2 {
return "Bob Smith"
} else {
return ""
}
}
```
Let's invoke our function :
```
//in the main() function
//get our customer
customer := getData(1)
fmt.Println(customer)
//get the wrong customer
customer := getData(3)
fmt.Println(customer)
```
## Arrays
At the moment, we can only return 1 customer at a time on our function. <br/>
Realistically we need the ability to return more data, not just a single customer. <br/>
Arrays allow us to make a collection of variables of the same type. <br/>
We can now return a list of customers! <br/>
Let's change our function to get an array of customers!
```
func getData() (customers [2]string) {
//create 1 record
customer := "Marcel Dempers"
//assign our customer to the array
customers[0] = customer
//OR we can assign it like this
customers[1] = "Bob Smith"
//send it back to the caller
return customers
}
```
Now we also have to change our calling function to expect an array:
```
customers := getData()
fmt.Println(customers)
```
## Slices
Since arrays are fixed size, Slices are a dynamically-sized view into arrays.
Let's create a slice instead of array so we can add customers dynamically!
```
func getData() (customers []string) {
//initialise our slice of type string
customers = []string{ "Marcel Dempers", "Bob Smith", "John Smith"}
//add more legendary customers dynamically
customers = append(customers, "Ben Spain")
customers = append(customers, "Aleem Janmohamed")
customers = append(customers, "Jamie le Notre")
customers = append(customers, "Victor Savkov")
customers = append(customers, "P The Admin")
customers = append(customers, "Adrian Oprea")
customers = append(customers, "Jonathan D")
//send it back to the caller
return customers
}
```
## Loops
Loops are used to iterate over collections, lists, arrays etc. <br/>
Let's say we need to loop through our customers
In the `main()` function, we can grab the list of customers and loop them.
In this demo, we'll cover a basic for loop, but there are several approaches to writing loops.
```
//loop forever
for {
//any code in here will run forever!
fmt.Println("Infinite Loop 1")
time.Sleep(time.Second)
//unless we break out the loop like this
break
}
//loop for x number of loops
for x := 0; x < 10; x++ {
//any code in here will run 10 times! (unless we break!)
fmt.Println(customers[x])
}
//loop for ALL our customer
for x, customer := range customers {
//we can access the "customer" variable in this approach
customer = customers[x]
fmt.Println(customer)
//OR
//we can use the supplied customer from the loop
// and silence the x variable, replace it with a _ character
fmt.Println(customer)
}
```
## Structs
So far so good, however, customer data is not useful as strings. <br/>
Customers can have a firstname, lastname, and more properties. <br/>
For this purpose we'd like to group some variables into a single variable. <br/>
This is what `struct` allows us to do. <br/>
Let's create a `struct` for our customer
Let's create a new `go` file called `customers.go`
```
package main
Customer struct {
FirstName string
LastName string
FullName string
}
```
Let's put it all together:
In `customers.go`, let's create a function to get customers
```
func GetCustomers()(customers []Customer) {
//we can declare customers like this:
marcel := Customer{ FirstName: "Marcel", LastName: "Dempers" }
customers = append(customers,
Customer{ FirstName: "Marcel", LastName: "Dempers" },
Customer{ FirstName: "Ben", LastName: "Spain" },
Customer{ FirstName: "Aleem", LastName: "Janmohamed" },
Customer{ FirstName: "Jamie", LastName: "le Notre" },
Customer{ FirstName: "Victor", LastName: "Savkov" },
Customer{ FirstName: "P", LastName: "The Admin" },
Customer{ FirstName: "Adrian", LastName: "Oprea" },
Customer{ FirstName: "Jonathan", LastName: "D" },
)
return customers
}
```
In `main()` we can now call our shiny new function
```
customers := GetCustomers()
for _, customer := range customers {
//we can access the "customer" variable in this approach
fmt.Println(customer)
}
```
# Docker
For our dev environment, we have a simple image using `go` <br/>
We also set a work directory and alias the target as `dev`
This means we can use this container layer as a development environment. <br/>
Later down the track we can add debuggers in here for example. <br/>
Checkout my debugging video for go: https://youtu.be/kToyI16IFxs <br/>
## Development environment
```
FROM golang:1.15 as dev
WORKDIR /work
```
## Building our code
```
FROM golang:1.15 as build
WORKDIR /app
COPY ./app/* /app/
RUN go build -o app
```
## The Runtime
```
FROM alpine as runtime
COPY --from=build /app/app /
CMD ./app
```
## Building the Container
```
docker build . -t customer-app
```
## Running the Container
```
docker run customer-app
```

View File

@ -1,3 +0,0 @@
{
"api_key" : "somesecretgoeshere"
}

View File

@ -1,58 +0,0 @@
package main
import (
"fmt" //to print messages to stdout
"log" //logging :)
//our web server that will host the mock
"github.com/buaazp/fasthttprouter"
"github.com/valyala/fasthttp"
"os"
"io/ioutil"
)
var configuration []byte
var secret []byte
func Response(ctx *fasthttp.RequestCtx) {
fmt.Fprintf(ctx, "Hello")
}
func Status(ctx *fasthttp.RequestCtx) {
fmt.Fprintf(ctx, "ok")
}
func ReadConfig(){
fmt.Println("reading config...")
config, e := ioutil.ReadFile("/configs/config.json")
if e != nil {
fmt.Printf("Error reading config file: %v\n", e)
os.Exit(1)
}
configuration = config
fmt.Println("config loaded!")
}
func ReadSecret(){
fmt.Println("reading secret...")
s, e := ioutil.ReadFile("/secrets/secret.json")
if e != nil {
fmt.Printf("Error reading secret file: %v\n", e)
os.Exit(1)
}
secret = s
fmt.Println("secret loaded!")
}
func main() {
fmt.Println("starting...")
ReadConfig()
ReadSecret()
router := fasthttprouter.New()
router.GET("/", Response)
router.GET("/status", Status)
log.Fatal(fasthttp.ListenAndServe(":5000", router.Handler))
}

View File

@ -1,11 +0,0 @@
global:
datacenter: vault-kubernetes-guide
client:
enabled: true
server:
replicas: 1
bootstrapExpect: 1
disruptionBudget:
maxUnavailable: 0

View File

@ -1,39 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: basic-secret
labels:
app: basic-secret
spec:
selector:
matchLabels:
app: basic-secret
replicas: 1
template:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/tls-skip-verify: "true"
vault.hashicorp.com/agent-inject-secret-helloworld: "secret/basic-secret/helloworld"
vault.hashicorp.com/agent-inject-template-helloworld: |
{{- with secret "secret/basic-secret/helloworld" -}}
{
"username" : "{{ .Data.username }}",
"password" : "{{ .Data.password }}"
}
{{- end }}
vault.hashicorp.com/role: "basic-secret-role"
labels:
app: basic-secret
spec:
serviceAccountName: basic-secret
containers:
- name: app
image: jweissig/app:0.0.1
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: basic-secret
labels:
app: basic-secret

View File

@ -1,52 +0,0 @@
# Basic Secret Injection
In order for us to start using secrets in vault, we need to setup a policy.
```
#Create a role for our app
kubectl -n vault exec -it vault-0 -- sh
vault write auth/kubernetes/role/basic-secret-role \
bound_service_account_names=basic-secret \
bound_service_account_namespaces=example-app \
policies=basic-secret-policy \
ttl=1h
```
The above maps our Kubernetes service account, used by our pod, to a policy.
Now lets create the policy to map our service account to a bunch of secrets
```
kubectl -n vault exec -it vault-0 -- sh
cat <<EOF > /home/vault/app-policy.hcl
path "secret/basic-secret/*" {
capabilities = ["read"]
}
EOF
vault policy write basic-secret-policy /home/vault/app-policy.hcl
```
Now our service account for our pod can access all secrets under `secret/basic-secret/*`
Lets create some secrets.
```
kubectl -n vault exec -it vault-0 -- sh
vault secrets enable -path=secret/ kv
vault kv put secret/basic-secret/helloworld username=dbuser password=sUp3rS3cUr3P@ssw0rd
```
Lets deploy our app and see if it works:
```
kubectl create ns example-app
kubectl -n example-app apply -f ./example-apps/basic-secret/deployment.yaml
kubectl -n example-app get pods
```
Once the pod is ready, the secret is injected into the pod at the following location:
```
kubectl -n example-app exec <pod-name> -- sh -c "cat /vault/secrets/helloworld"
```

View File

@ -1,7 +0,0 @@
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
- role: worker

View File

@ -1,626 +0,0 @@
---
# Source: consul/templates/server-disruptionbudget.yaml
# PodDisruptionBudget to prevent degrading the server cluster through
# voluntary cluster changes.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: consul-consul-server
namespace: vault
labels:
app: consul
chart: consul-helm
heritage: Helm
release: consul
component: server
spec:
maxUnavailable: 0
selector:
matchLabels:
app: consul
release: "consul"
component: server
---
# Source: consul/templates/client-serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: consul-consul-client
namespace: vault
labels:
app: consul
chart: consul-helm
heritage: Helm
release: consul
component: client
---
# Source: consul/templates/server-serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: consul-consul-server
namespace: vault
labels:
app: consul
chart: consul-helm
heritage: Helm
release: consul
component: server
---
# Source: consul/templates/client-config-configmap.yaml
# ConfigMap with extra configuration specified directly to the chart
# for client agents only.
apiVersion: v1
kind: ConfigMap
metadata:
name: consul-consul-client-config
namespace: vault
labels:
app: consul
chart: consul-helm
heritage: Helm
release: consul
component: client
data:
extra-from-values.json: |-
{}
central-config.json: |-
{
"enable_central_service_config": true
}
---
# Source: consul/templates/server-config-configmap.yaml
# StatefulSet to run the actual Consul server cluster.
apiVersion: v1
kind: ConfigMap
metadata:
name: consul-consul-server-config
namespace: vault
labels:
app: consul
chart: consul-helm
heritage: Helm
release: consul
component: server
data:
extra-from-values.json: |-
{}
central-config.json: |-
{
"enable_central_service_config": true
}
---
# Source: consul/templates/client-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: consul-consul-client
namespace: vault
labels:
app: consul
chart: consul-helm
heritage: Helm
release: consul
component: client
rules: []
---
# Source: consul/templates/server-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: consul-consul-server
namespace: vault
labels:
app: consul
chart: consul-helm
heritage: Helm
release: consul
component: server
rules: []
---
# Source: consul/templates/client-rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: consul-consul-client
namespace: vault
labels:
app: consul
chart: consul-helm
heritage: Helm
release: consul
component: client
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: consul-consul-client
subjects:
- kind: ServiceAccount
name: consul-consul-client
---
# Source: consul/templates/server-rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: consul-consul-server
namespace: vault
labels:
app: consul
chart: consul-helm
heritage: Helm
release: consul
component: server
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: consul-consul-server
subjects:
- kind: ServiceAccount
name: consul-consul-server
---
# Source: consul/templates/dns-service.yaml
# Service for Consul DNS.
apiVersion: v1
kind: Service
metadata:
name: consul-consul-dns
namespace: vault
labels:
app: consul
chart: consul-helm
heritage: Helm
release: consul
component: dns
spec:
type: ClusterIP
ports:
- name: dns-tcp
port: 53
protocol: "TCP"
targetPort: dns-tcp
- name: dns-udp
port: 53
protocol: "UDP"
targetPort: dns-udp
selector:
app: consul
release: "consul"
hasDNS: "true"
---
# Source: consul/templates/server-service.yaml
# Headless service for Consul server DNS entries. This service should only
# point to Consul servers. For access to an agent, one should assume that
# the agent is installed locally on the node and the NODE_IP should be used.
# If the node can't run a Consul agent, then this service can be used to
# communicate directly to a server agent.
apiVersion: v1
kind: Service
metadata:
name: consul-consul-server
namespace: vault
labels:
app: consul
chart: consul-helm
heritage: Helm
release: consul
component: server
annotations:
# This must be set in addition to publishNotReadyAddresses due
# to an open issue where it may not work:
# https://github.com/kubernetes/kubernetes/issues/58662
service.alpha.kubernetes.io/tolerate-unready-endpoints: "true"
spec:
clusterIP: None
# We want the servers to become available even if they're not ready
# since this DNS is also used for join operations.
publishNotReadyAddresses: true
ports:
- name: http
port: 8500
targetPort: 8500
- name: serflan-tcp
protocol: "TCP"
port: 8301
targetPort: 8301
- name: serflan-udp
protocol: "UDP"
port: 8301
targetPort: 8301
- name: serfwan-tcp
protocol: "TCP"
port: 8302
targetPort: 8302
- name: serfwan-udp
protocol: "UDP"
port: 8302
targetPort: 8302
- name: server
port: 8300
targetPort: 8300
- name: dns-tcp
protocol: "TCP"
port: 8600
targetPort: dns-tcp
- name: dns-udp
protocol: "UDP"
port: 8600
targetPort: dns-udp
selector:
app: consul
release: "consul"
component: server
---
# Source: consul/templates/ui-service.yaml
# UI Service for Consul Server
apiVersion: v1
kind: Service
metadata:
name: consul-consul-ui
namespace: vault
labels:
app: consul
chart: consul-helm
heritage: Helm
release: consul
component: ui
spec:
selector:
app: consul
release: "consul"
component: server
ports:
- name: http
port: 80
targetPort: 8500
---
# Source: consul/templates/client-daemonset.yaml
# DaemonSet to run the Consul clients on every node.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: consul-consul
namespace: vault
labels:
app: consul
chart: consul-helm
heritage: Helm
release: consul
component: client
spec:
selector:
matchLabels:
app: consul
chart: consul-helm
release: consul
component: client
hasDNS: "true"
template:
metadata:
labels:
app: consul
chart: consul-helm
release: consul
component: client
hasDNS: "true"
annotations:
"consul.hashicorp.com/connect-inject": "false"
"consul.hashicorp.com/config-checksum": 797b3593a73b78fc74f3b1e3b978107b3022d4649802185631f959f000234331
spec:
terminationGracePeriodSeconds: 10
serviceAccountName: consul-consul-client
securityContext:
fsGroup: 1000
runAsGroup: 1000
runAsNonRoot: true
runAsUser: 100
volumes:
- name: data
emptyDir: {}
- name: config
configMap:
name: consul-consul-client-config
containers:
- name: consul
image: "hashicorp/consul:1.11.1"
env:
- name: ADVERTISE_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: NODE
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: HOST_IP
valueFrom:
fieldRef:
fieldPath: status.hostIP
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: CONSUL_DISABLE_PERM_MGMT
value: "true"
command:
- "/bin/sh"
- "-ec"
- |
CONSUL_FULLNAME="consul-consul"
mkdir -p /consul/extra-config
cp /consul/config/extra-from-values.json /consul/extra-config/extra-from-values.json
[ -n "${HOST_IP}" ] && sed -Ei "s|HOST_IP|${HOST_IP?}|g" /consul/extra-config/extra-from-values.json
[ -n "${POD_IP}" ] && sed -Ei "s|POD_IP|${POD_IP?}|g" /consul/extra-config/extra-from-values.json
[ -n "${HOSTNAME}" ] && sed -Ei "s|HOSTNAME|${HOSTNAME?}|g" /consul/extra-config/extra-from-values.json
exec /usr/local/bin/docker-entrypoint.sh consul agent \
-node="${NODE}" \
-advertise="${ADVERTISE_IP}" \
-bind=0.0.0.0 \
-client=0.0.0.0 \
-node-meta=host-ip:${HOST_IP} \
-node-meta=pod-name:${HOSTNAME} \
-hcl='leave_on_terminate = true' \
-hcl='ports { grpc = 8502 }' \
-config-dir=/consul/config \
-datacenter=vault-kubernetes-guide \
-data-dir=/consul/data \
-retry-join="${CONSUL_FULLNAME}-server-0.${CONSUL_FULLNAME}-server.${NAMESPACE}.svc:8301" \
-config-file=/consul/extra-config/extra-from-values.json \
-domain=consul
volumeMounts:
- name: data
mountPath: /consul/data
- name: config
mountPath: /consul/config
ports:
- containerPort: 8500
hostPort: 8500
name: http
- containerPort: 8502
hostPort: 8502
name: grpc
- containerPort: 8301
protocol: "TCP"
name: serflan-tcp
- containerPort: 8301
protocol: "UDP"
name: serflan-udp
- containerPort: 8600
name: dns-tcp
protocol: "TCP"
- containerPort: 8600
name: dns-udp
protocol: "UDP"
readinessProbe:
# NOTE(mitchellh): when our HTTP status endpoints support the
# proper status codes, we should switch to that. This is temporary.
exec:
command:
- "/bin/sh"
- "-ec"
- |
curl http://127.0.0.1:8500/v1/status/leader \
2>/dev/null | grep -E '".+"'
resources:
limits:
cpu: 100m
memory: 100Mi
requests:
cpu: 100m
memory: 100Mi
securityContext:
null
---
# Source: consul/templates/server-statefulset.yaml
# StatefulSet to run the actual Consul server cluster.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: consul-consul-server
namespace: vault
labels:
app: consul
chart: consul-helm
heritage: Helm
release: consul
component: server
spec:
serviceName: consul-consul-server
podManagementPolicy: Parallel
replicas: 1
selector:
matchLabels:
app: consul
chart: consul-helm
release: consul
component: server
hasDNS: "true"
template:
metadata:
labels:
app: consul
chart: consul-helm
release: consul
component: server
hasDNS: "true"
annotations:
"consul.hashicorp.com/connect-inject": "false"
"consul.hashicorp.com/config-checksum": c9b100f895d5bda6a5c8bbebac73e1ab5bdc4cad06b04e72eb1b620677bfe41d
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: consul
release: "consul"
component: server
topologyKey: kubernetes.io/hostname
terminationGracePeriodSeconds: 30
serviceAccountName: consul-consul-server
securityContext:
fsGroup: 1000
runAsGroup: 1000
runAsNonRoot: true
runAsUser: 100
volumes:
- name: config
configMap:
name: consul-consul-server-config
containers:
- name: consul
image: "hashicorp/consul:1.11.1"
env:
- name: ADVERTISE_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: HOST_IP
valueFrom:
fieldRef:
fieldPath: status.hostIP
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: CONSUL_DISABLE_PERM_MGMT
value: "true"
command:
- "/bin/sh"
- "-ec"
- |
CONSUL_FULLNAME="consul-consul"
mkdir -p /consul/extra-config
cp /consul/config/extra-from-values.json /consul/extra-config/extra-from-values.json
[ -n "${HOST_IP}" ] && sed -Ei "s|HOST_IP|${HOST_IP?}|g" /consul/extra-config/extra-from-values.json
[ -n "${POD_IP}" ] && sed -Ei "s|POD_IP|${POD_IP?}|g" /consul/extra-config/extra-from-values.json
[ -n "${HOSTNAME}" ] && sed -Ei "s|HOSTNAME|${HOSTNAME?}|g" /consul/extra-config/extra-from-values.json
exec /usr/local/bin/docker-entrypoint.sh consul agent \
-advertise="${ADVERTISE_IP}" \
-bind=0.0.0.0 \
-bootstrap-expect=1 \
-client=0.0.0.0 \
-config-dir=/consul/config \
-datacenter=vault-kubernetes-guide \
-data-dir=/consul/data \
-domain=consul \
-hcl="connect { enabled = true }" \
-ui \
-retry-join="${CONSUL_FULLNAME}-server-0.${CONSUL_FULLNAME}-server.${NAMESPACE}.svc:8301" \
-serf-lan-port=8301 \
-config-file=/consul/extra-config/extra-from-values.json \
-server
volumeMounts:
- name: data-vault
mountPath: /consul/data
- name: config
mountPath: /consul/config
ports:
- name: http
containerPort: 8500
- name: serflan-tcp
containerPort: 8301
protocol: "TCP"
- name: serflan-udp
containerPort: 8301
protocol: "UDP"
- name: serfwan-tcp
containerPort: 8302
protocol: "TCP"
- name: serfwan-udp
containerPort: 8302
protocol: "UDP"
- name: server
containerPort: 8300
- name: dns-tcp
containerPort: 8600
protocol: "TCP"
- name: dns-udp
containerPort: 8600
protocol: "UDP"
readinessProbe:
# NOTE(mitchellh): when our HTTP status endpoints support the
# proper status codes, we should switch to that. This is temporary.
exec:
command:
- "/bin/sh"
- "-ec"
- |
curl http://127.0.0.1:8500/v1/status/leader \
2>/dev/null | grep -E '".+"'
failureThreshold: 2
initialDelaySeconds: 5
periodSeconds: 3
successThreshold: 1
timeoutSeconds: 5
resources:
limits:
cpu: 100m
memory: 100Mi
requests:
cpu: 100m
memory: 100Mi
securityContext:
null
volumeClaimTemplates:
- metadata:
name: data-vault
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
# Source: consul/templates/tests/test-runner.yaml
apiVersion: v1
kind: Pod
metadata:
name: "consul-consul-test"
namespace: vault
labels:
app: consul
chart: consul-helm
heritage: Helm
release: consul
annotations:
"helm.sh/hook": test-success
spec:
containers:
- name: consul-test
image: "hashicorp/consul:1.11.1"
env:
- name: HOST_IP
valueFrom:
fieldRef:
fieldPath: status.hostIP
- name: CONSUL_HTTP_ADDR
value: http://$(HOST_IP):8500
command:
- "/bin/sh"
- "-ec"
- |
consul members | tee members.txt
if [ $(grep -c consul-server members.txt) != $(grep consul-server members.txt | grep -c alive) ]
then
echo "Failed because not all consul servers are available"
exit 1
fi
restartPolicy: Never

View File

@ -1,710 +0,0 @@
---
# Source: vault/templates/server-disruptionbudget.yaml
# PodDisruptionBudget to prevent degrading the server cluster through
# voluntary cluster changes.
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: vault
namespace: vault
labels:
helm.sh/chart: vault-0.19.0
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
spec:
maxUnavailable: 1
selector:
matchLabels:
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
component: server
---
# Source: vault/templates/injector-serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-agent-injector
namespace: vault
labels:
app.kubernetes.io/name: vault-agent-injector
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
---
# Source: vault/templates/server-serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault
namespace: vault
labels:
helm.sh/chart: vault-0.19.0
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
---
# Source: vault/templates/server-config-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: vault-config
namespace: vault
labels:
helm.sh/chart: vault-0.19.0
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
data:
extraconfig-from-values.hcl: |-
disable_mlock = true
ui = true
listener "tcp" {
tls_disable = 0
address = "0.0.0.0:8200"
tls_cert_file = "/vault/userconfig/tls-server/tls.crt"
tls_key_file = "/vault/userconfig/tls-server/tls.key"
tls_min_version = "tls12"
}
storage "consul" {
path = "vault"
address = "consul-consul-server:8500"
}
---
# Source: vault/templates/injector-clusterrole.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: vault-agent-injector-clusterrole
labels:
app.kubernetes.io/name: vault-agent-injector
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
rules:
- apiGroups: ["admissionregistration.k8s.io"]
resources: ["mutatingwebhookconfigurations"]
verbs:
- "get"
- "list"
- "watch"
- "patch"
---
# Source: vault/templates/injector-clusterrolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: vault-agent-injector-binding
labels:
app.kubernetes.io/name: vault-agent-injector
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: vault-agent-injector-clusterrole
subjects:
- kind: ServiceAccount
name: vault-agent-injector
namespace: vault
---
# Source: vault/templates/server-clusterrolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: vault-server-binding
labels:
helm.sh/chart: vault-0.19.0
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: vault
namespace: vault
---
# Source: vault/templates/server-discovery-role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: vault
name: vault-discovery-role
labels:
helm.sh/chart: vault-0.19.0
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "watch", "list", "update", "patch"]
---
# Source: vault/templates/server-discovery-rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: vault-discovery-rolebinding
namespace: vault
labels:
helm.sh/chart: vault-0.19.0
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: vault-discovery-role
subjects:
- kind: ServiceAccount
name: vault
namespace: vault
---
# Source: vault/templates/injector-service.yaml
apiVersion: v1
kind: Service
metadata:
name: vault-agent-injector-svc
namespace: vault
labels:
app.kubernetes.io/name: vault-agent-injector
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
spec:
ports:
- name: https
port: 443
targetPort: 8080
selector:
app.kubernetes.io/name: vault-agent-injector
app.kubernetes.io/instance: vault
component: webhook
---
# Source: vault/templates/server-ha-active-service.yaml
# Service for active Vault pod
apiVersion: v1
kind: Service
metadata:
name: vault-active
namespace: vault
labels:
helm.sh/chart: vault-0.19.0
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
annotations:
spec:
publishNotReadyAddresses: true
ports:
- name: https
port: 8200
targetPort: 8200
- name: https-internal
port: 8201
targetPort: 8201
selector:
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
component: server
vault-active: "true"
---
# Source: vault/templates/server-ha-standby-service.yaml
# Service for standby Vault pod
apiVersion: v1
kind: Service
metadata:
name: vault-standby
namespace: vault
labels:
helm.sh/chart: vault-0.19.0
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
annotations:
spec:
publishNotReadyAddresses: true
ports:
- name: https
port: 8200
targetPort: 8200
- name: https-internal
port: 8201
targetPort: 8201
selector:
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
component: server
vault-active: "false"
---
# Source: vault/templates/server-headless-service.yaml
# Service for Vault cluster
apiVersion: v1
kind: Service
metadata:
name: vault-internal
namespace: vault
labels:
helm.sh/chart: vault-0.19.0
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
annotations:
spec:
clusterIP: None
publishNotReadyAddresses: true
ports:
- name: "https"
port: 8200
targetPort: 8200
- name: https-internal
port: 8201
targetPort: 8201
selector:
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
component: server
---
# Source: vault/templates/server-service.yaml
# Service for Vault cluster
apiVersion: v1
kind: Service
metadata:
name: vault
namespace: vault
labels:
helm.sh/chart: vault-0.19.0
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
annotations:
spec:
# We want the servers to become available even if they're not ready
# since this DNS is also used for join operations.
publishNotReadyAddresses: true
ports:
- name: https
port: 8200
targetPort: 8200
- name: https-internal
port: 8201
targetPort: 8201
selector:
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
component: server
---
# Source: vault/templates/ui-service.yaml
apiVersion: v1
kind: Service
metadata:
name: vault-ui
namespace: vault
labels:
helm.sh/chart: vault-0.19.0
app.kubernetes.io/name: vault-ui
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
spec:
selector:
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
component: server
publishNotReadyAddresses: true
ports:
- name: https
port: 8200
targetPort: 8200
type: ClusterIP
---
# Source: vault/templates/injector-deployment.yaml
# Deployment for the injector
apiVersion: apps/v1
kind: Deployment
metadata:
name: vault-agent-injector
namespace: vault
labels:
app.kubernetes.io/name: vault-agent-injector
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
component: webhook
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: vault-agent-injector
app.kubernetes.io/instance: vault
component: webhook
template:
metadata:
labels:
app.kubernetes.io/name: vault-agent-injector
app.kubernetes.io/instance: vault
component: webhook
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app.kubernetes.io/name: vault-agent-injector
app.kubernetes.io/instance: "vault"
component: webhook
topologyKey: kubernetes.io/hostname
serviceAccountName: "vault-agent-injector"
hostNetwork: false
securityContext:
runAsNonRoot: true
runAsGroup: 1000
runAsUser: 100
containers:
- name: sidecar-injector
resources:
limits:
cpu: 250m
memory: 256Mi
requests:
cpu: 50m
memory: 50Mi
image: "hashicorp/vault-k8s:0.14.1"
imagePullPolicy: "IfNotPresent"
securityContext:
allowPrivilegeEscalation: false
env:
- name: AGENT_INJECT_LISTEN
value: :8080
- name: AGENT_INJECT_LOG_LEVEL
value: info
- name: AGENT_INJECT_VAULT_ADDR
value: https://vault.vault.svc:8200
- name: AGENT_INJECT_VAULT_AUTH_PATH
value: auth/kubernetes
- name: AGENT_INJECT_VAULT_IMAGE
value: "hashicorp/vault:1.9.2"
- name: AGENT_INJECT_TLS_AUTO
value: vault-agent-injector-cfg
- name: AGENT_INJECT_TLS_AUTO_HOSTS
value: vault-agent-injector-svc,vault-agent-injector-svc.vault,vault-agent-injector-svc.vault.svc
- name: AGENT_INJECT_LOG_FORMAT
value: standard
- name: AGENT_INJECT_REVOKE_ON_SHUTDOWN
value: "false"
- name: AGENT_INJECT_CPU_REQUEST
value: "250m"
- name: AGENT_INJECT_CPU_LIMIT
value: "500m"
- name: AGENT_INJECT_MEM_REQUEST
value: "64Mi"
- name: AGENT_INJECT_MEM_LIMIT
value: "128Mi"
- name: AGENT_INJECT_DEFAULT_TEMPLATE
value: "map"
- name: AGENT_INJECT_TEMPLATE_CONFIG_EXIT_ON_RETRY_FAILURE
value: "true"
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
args:
- agent-inject
- 2>&1
livenessProbe:
httpGet:
path: /health/ready
port: 8080
scheme: HTTPS
failureThreshold: 2
initialDelaySeconds: 5
periodSeconds: 2
successThreshold: 1
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /health/ready
port: 8080
scheme: HTTPS
failureThreshold: 2
initialDelaySeconds: 5
periodSeconds: 2
successThreshold: 1
timeoutSeconds: 5
---
# Source: vault/templates/server-statefulset.yaml
# StatefulSet to run the actual vault server cluster.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: vault
namespace: vault
labels:
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
spec:
serviceName: vault-internal
podManagementPolicy: Parallel
replicas: 3
updateStrategy:
type: OnDelete
selector:
matchLabels:
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
component: server
template:
metadata:
labels:
helm.sh/chart: vault-0.19.0
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
component: server
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app.kubernetes.io/name: vault
app.kubernetes.io/instance: "vault"
component: server
topologyKey: kubernetes.io/hostname
terminationGracePeriodSeconds: 10
serviceAccountName: vault
securityContext:
runAsNonRoot: true
runAsGroup: 1000
runAsUser: 100
fsGroup: 1000
volumes:
- name: config
configMap:
name: vault-config
- name: userconfig-tls-server
secret:
secretName: tls-server
defaultMode: 420
- name: userconfig-tls-ca
secret:
secretName: tls-ca
defaultMode: 420
- name: home
emptyDir: {}
containers:
- name: vault
resources:
limits:
cpu: 2000m
memory: 16Gi
requests:
cpu: 500m
memory: 50Mi
image: hashicorp/vault:1.9.2
imagePullPolicy: IfNotPresent
command:
- "/bin/sh"
- "-ec"
args:
- |
cp /vault/config/extraconfig-from-values.hcl /tmp/storageconfig.hcl;
[ -n "${HOST_IP}" ] && sed -Ei "s|HOST_IP|${HOST_IP?}|g" /tmp/storageconfig.hcl;
[ -n "${POD_IP}" ] && sed -Ei "s|POD_IP|${POD_IP?}|g" /tmp/storageconfig.hcl;
[ -n "${HOSTNAME}" ] && sed -Ei "s|HOSTNAME|${HOSTNAME?}|g" /tmp/storageconfig.hcl;
[ -n "${API_ADDR}" ] && sed -Ei "s|API_ADDR|${API_ADDR?}|g" /tmp/storageconfig.hcl;
[ -n "${TRANSIT_ADDR}" ] && sed -Ei "s|TRANSIT_ADDR|${TRANSIT_ADDR?}|g" /tmp/storageconfig.hcl;
[ -n "${RAFT_ADDR}" ] && sed -Ei "s|RAFT_ADDR|${RAFT_ADDR?}|g" /tmp/storageconfig.hcl;
/usr/local/bin/docker-entrypoint.sh vault server -config=/tmp/storageconfig.hcl
securityContext:
allowPrivilegeEscalation: false
env:
- name: HOST_IP
valueFrom:
fieldRef:
fieldPath: status.hostIP
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: VAULT_K8S_POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: VAULT_K8S_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: VAULT_ADDR
value: "https://127.0.0.1:8200"
- name: VAULT_API_ADDR
value: "https://$(POD_IP):8200"
- name: SKIP_CHOWN
value: "true"
- name: SKIP_SETCAP
value: "true"
- name: HOSTNAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: VAULT_CLUSTER_ADDR
value: "https://$(HOSTNAME).vault-internal:8201"
- name: HOME
value: "/home/vault"
- name: "VAULT_CACERT"
value: "/vault/userconfig/tls-ca/tls.crt"
volumeMounts:
- name: config
mountPath: /vault/config
- name: userconfig-tls-server
readOnly: true
mountPath: /vault/userconfig/tls-server
- name: userconfig-tls-ca
readOnly: true
mountPath: /vault/userconfig/tls-ca
- name: home
mountPath: /home/vault
ports:
- containerPort: 8200
name: https
- containerPort: 8201
name: https-internal
- containerPort: 8202
name: https-rep
readinessProbe:
httpGet:
path: "/v1/sys/health?standbyok=true&sealedcode=204&uninitcode=204"
port: 8200
scheme: HTTPS
failureThreshold: 2
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 3
livenessProbe:
httpGet:
path: "/v1/sys/health?standbyok=true"
port: 8200
scheme: HTTPS
failureThreshold: 2
initialDelaySeconds: 60
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 3
lifecycle:
# Vault container doesn't receive SIGTERM from Kubernetes
# and after the grace period ends, Kube sends SIGKILL. This
# causes issues with graceful shutdowns such as deregistering itself
# from Consul (zombie services).
preStop:
exec:
command: [
"/bin/sh", "-c",
# Adding a sleep here to give the pod eviction a
# chance to propagate, so requests will not be made
# to this pod while it's terminating
"sleep 5 && kill -SIGTERM $(pidof vault)",
]
volumeClaimTemplates:
---
# Source: vault/templates/injector-mutating-webhook.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: vault-agent-injector-cfg
labels:
app.kubernetes.io/name: vault-agent-injector
app.kubernetes.io/instance: vault
app.kubernetes.io/managed-by: Helm
webhooks:
- name: vault.hashicorp.com
sideEffects: None
admissionReviewVersions:
- "v1beta1"
- "v1"
clientConfig:
service:
name: vault-agent-injector-svc
namespace: vault
path: "/mutate"
caBundle: ""
rules:
- operations: ["CREATE", "UPDATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
failurePolicy: Ignore
---
# Source: vault/templates/tests/server-test.yaml
apiVersion: v1
kind: Pod
metadata:
name: "vault-server-test"
namespace: vault
annotations:
"helm.sh/hook": test
spec:
containers:
- name: vault-server-test
image: hashicorp/vault:1.9.2
imagePullPolicy: IfNotPresent
env:
- name: VAULT_ADDR
value: https://vault.vault.svc:8200
- name: "VAULT_CACERT"
value: "/vault/userconfig/tls-ca/tls.crt"
command:
- /bin/sh
- -c
- |
echo "Checking for sealed info in 'vault status' output"
ATTEMPTS=10
n=0
until [ "$n" -ge $ATTEMPTS ]
do
echo "Attempt" $n...
vault status -format yaml | grep -E '^sealed: (true|false)' && break
n=$((n+1))
sleep 5
done
if [ $n -ge $ATTEMPTS ]; then
echo "timed out looking for sealed info in 'vault status' output"
exit 1
fi
exit 0
volumeMounts:
volumes:
restartPolicy: Never

View File

@ -1,203 +0,0 @@
# Hashicorp Vault Guide
<a href="https://youtu.be/2Owo4Ioo9tQ" title="hashicorp-vault"><img src="https://i.ytimg.com/vi/2Owo4Ioo9tQ/hqdefault.jpg" width="20%" alt="introduction hashicorp vault" /></a>
Requirements:
* Kubernetes 1.21
* Kind or Minikube
For this tutorial, I will be using Kubernetes 1.21.
If you are watching the old guide for Kubernetes 1.17, go [here](..\vault\readme.md)
Lets create a Kubernetes cluster to play with using [kind](https://kind.sigs.k8s.io/docs/user/quick-start/)
```
cd hashicorp/vault-2022
kind create cluster --name vault --image kindest/node:v1.21.1 --config kind.yaml
```
Next up, I will be running a small container where I will be doing all the work from:
You can skip this part if you already have `kubectl` and `helm` on your machine.
```
docker run -it --rm --net host -v ${HOME}/.kube/:/root/.kube/ -v ${PWD}:/work -w /work alpine sh
```
Install `kubectl`
```
apk add --no-cache curl
curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
chmod +x ./kubectl
mv ./kubectl /usr/local/bin/kubectl
```
Install `helm`
```
curl -LO https://get.helm.sh/helm-v3.7.2-linux-amd64.tar.gz
tar -C /tmp/ -zxvf helm-v3.7.2-linux-amd64.tar.gz
rm helm-v3.7.2-linux-amd64.tar.gz
mv /tmp/linux-amd64/helm /usr/local/bin/helm
chmod +x /usr/local/bin/helm
```
Now we have `helm` and `kubectl` and can access our `kind` cluster:
```
kubectl get nodes
NAME STATUS ROLES AGE VERSION
vault-control-plane Ready control-plane,master 37s v1.21.1
```
Let's add the Helm repositories, so we can access the Kubernetes manifests
```
helm repo add hashicorp https://helm.releases.hashicorp.com
```
## Storage: Consul
We will use a very basic Consul cluster for our Vault backend. </br>
Let's find what versions of Consul are available:
```
helm search repo hashicorp/consul --versions
```
We can use chart `0.39.0` which is the latest at the time of this demo
Let's create a manifests folder and grab the YAML:
```
mkdir manifests
helm template consul hashicorp/consul \
--namespace vault \
--version 0.39.0 \
-f consul-values.yaml \
> ./manifests/consul.yaml
```
Deploy the consul services:
```
kubectl create ns vault
kubectl -n vault apply -f ./manifests/consul.yaml
kubectl -n vault get pods
```
## TLS End to End Encryption
See steps in [./tls/ssl_generate_self_signed.md](./tls/ssl_generate_self_signed.md)
You'll need to generate TLS certs (or bring your own)
Remember not to check-in your TLS to GIT :)
Create the TLS secret
```
kubectl -n vault create secret tls tls-ca \
--cert ./tls/ca.pem \
--key ./tls/ca-key.pem
kubectl -n vault create secret tls tls-server \
--cert ./tls/vault.pem \
--key ./tls/vault-key.pem
```
## Generate Kubernetes Manifests
Let's find what versions of vault are available:
```
helm search repo hashicorp/vault --versions
```
In this demo I will use the `0.19.0` chart </br>
Let's firstly create a `values` file to customize vault.
Let's grab the manifests:
```
helm template vault hashicorp/vault \
--namespace vault \
--version 0.19.0 \
-f vault-values.yaml \
> ./manifests/vault.yaml
```
## Deployment
```
kubectl -n vault apply -f ./manifests/vault.yaml
kubectl -n vault get pods
```
## Initialising Vault
```
kubectl -n vault exec -it vault-0 -- sh
kubectl -n vault exec -it vault-1 -- sh
kubectl -n vault exec -it vault-2 -- sh
vault operator init
vault operator unseal
kubectl -n vault exec -it vault-0 -- vault status
kubectl -n vault exec -it vault-1 -- vault status
kubectl -n vault exec -it vault-2 -- vault status
```
## Web UI
Let's checkout the web UI:
```
kubectl -n vault get svc
kubectl -n vault port-forward svc/vault-ui 443:8200
```
Now we can access the web UI [here](https://localhost/)
## Enable Kubernetes Authentication
For the injector to be authorised to access vault, we need to enable K8s auth
```
kubectl -n vault exec -it vault-0 -- sh
vault login
vault auth enable kubernetes
vault write auth/kubernetes/config \
token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
kubernetes_host=https://${KUBERNETES_PORT_443_TCP_ADDR}:443 \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
issuer="https://kubernetes.default.svc.cluster.local"
exit
```
# Summary
So we have a vault, an injector, TLS end to end, stateful storage.
The injector can now inject secrets for pods from the vault.
Now we are ready to use the platform for different types of secrets:
## Secret Injection Guides
### Basic Secrets
Objective:
----------
* Let's create a basic secret in vault manually
* Application consumes the secret automatically
[Try it](./example-apps/basic-secret/readme.md)

View File

@ -1,13 +0,0 @@
{
"signing": {
"default": {
"expiry": "175200h"
},
"profiles": {
"default": {
"usages": ["signing", "key encipherment", "server auth", "client auth"],
"expiry": "175200h"
}
}
}
}

View File

@ -1,18 +0,0 @@
{
"hosts": [
"cluster.local"
],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "AU",
"L": "Melbourne",
"O": "Example",
"OU": "CA",
"ST": "Example"
}
]
}

View File

@ -1,40 +0,0 @@
# Use CFSSL to generate certificates
More about [CFSSL here]("https://github.com/cloudflare/cfssl")
```
cd hashicorp\vault-2022\tls
docker run -it --rm -v ${PWD}:/work -w /work debian bash
apt-get update && apt-get install -y curl &&
curl -L https://github.com/cloudflare/cfssl/releases/download/v1.6.1/cfssl_1.6.1_linux_amd64 -o /usr/local/bin/cfssl && \
curl -L https://github.com/cloudflare/cfssl/releases/download/v1.6.1/cfssljson_1.6.1_linux_amd64 -o /usr/local/bin/cfssljson && \
chmod +x /usr/local/bin/cfssl && \
chmod +x /usr/local/bin/cfssljson
#generate ca in /tmp
cfssl gencert -initca ca-csr.json | cfssljson -bare /tmp/ca
#generate certificate in /tmp
cfssl gencert \
-ca=/tmp/ca.pem \
-ca-key=/tmp/ca-key.pem \
-config=ca-config.json \
-hostname="vault,vault.vault.svc.cluster.local,vault.vault.svc,localhost,127.0.0.1" \
-profile=default \
ca-csr.json | cfssljson -bare /tmp/vault
```
view the files:
```
ls -l /tmp
```
access the files:
```
mv /tmp/* .
```

View File

@ -1,85 +0,0 @@
# Vault Helm Chart Value Overrides
global:
enabled: true
tlsDisable: false
injector:
enabled: true
# Use the Vault K8s Image https://github.com/hashicorp/vault-k8s/
image:
repository: "hashicorp/vault-k8s"
tag: "0.14.1"
resources:
requests:
memory: 50Mi
cpu: 50m
limits:
memory: 256Mi
cpu: 250m
server:
image:
repository: "hashicorp/vault"
tag: "1.9.2"
# These Resource Limits are in line with node requirements in the
# Vault Reference Architecture for a Small Cluster
resources:
requests:
memory: 50Mi
cpu: 500m
limits:
memory: 16Gi
cpu: 2000m
# For HA configuration and because we need to manually init the vault,
# we need to define custom readiness/liveness Probe settings
readinessProbe:
enabled: true
path: "/v1/sys/health?standbyok=true&sealedcode=204&uninitcode=204"
livenessProbe:
enabled: true
path: "/v1/sys/health?standbyok=true"
initialDelaySeconds: 60
# extraEnvironmentVars is a list of extra environment variables to set with the stateful set. These could be
# used to include variables required for auto-unseal.
extraEnvironmentVars:
VAULT_CACERT: /vault/userconfig/tls-ca/tls.crt
# extraVolumes is a list of extra volumes to mount. These will be exposed
# to Vault in the path `/vault/userconfig/<name>/`.
extraVolumes:
- type: secret
name: tls-server
- type: secret
name: tls-ca
standalone:
enabled: false
# Run Vault in "HA" mode.
ha:
enabled: true
replicas: 3
config: |
ui = true
listener "tcp" {
tls_disable = 0
address = "0.0.0.0:8200"
tls_cert_file = "/vault/userconfig/tls-server/tls.crt"
tls_key_file = "/vault/userconfig/tls-server/tls.key"
tls_min_version = "tls12"
}
storage "consul" {
path = "vault"
address = "consul-consul-server:8500"
}
# Vault UI
ui:
enabled: true
externalPort: 8200

View File

@ -1,39 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: basic-secret
labels:
app: basic-secret
spec:
selector:
matchLabels:
app: basic-secret
replicas: 1
template:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/tls-skip-verify: "true"
vault.hashicorp.com/agent-inject-secret-helloworld: "secret/basic-secret/helloworld"
vault.hashicorp.com/agent-inject-template-helloworld: |
{{- with secret "secret/basic-secret/helloworld" -}}
{
"username" : "{{ .Data.username }}",
"password" : "{{ .Data.password }}"
}
{{- end }}
vault.hashicorp.com/role: "basic-secret-role"
labels:
app: basic-secret
spec:
serviceAccountName: basic-secret
containers:
- name: app
image: jweissig/app:0.0.1
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: basic-secret
labels:
app: basic-secret

View File

@ -1,50 +0,0 @@
# Basic Secret Injection
In order for us to start using secrets in vault, we need to setup a policy.
```
#Create a role for our app
kubectl -n vault-example exec -it vault-example-0 sh
vault write auth/kubernetes/role/basic-secret-role \
bound_service_account_names=basic-secret \
bound_service_account_namespaces=vault-example \
policies=basic-secret-policy \
ttl=1h
```
The above maps our Kubernetes service account, used by our pod, to a policy.
Now lets create the policy to map our service account to a bunch of secrets
```
kubectl -n vault-example exec -it vault-example-0 sh
cat <<EOF > /home/vault/app-policy.hcl
path "secret/basic-secret/*" {
capabilities = ["read"]
}
EOF
vault policy write basic-secret-policy /home/vault/app-policy.hcl
exit
```
Now our service account for our pod can access all secrets under `secret/basic-secret/*`
Lets create some secrets.
```
kubectl -n vault-example exec -it vault-example-0 sh
vault secrets enable -path=secret/ kv
vault kv put secret/basic-secret/helloworld username=dbuser password=sUp3rS3cUr3P@ssw0rd
exit
```
Lets deploy our app and see if it works:
```
kubectl -n vault-example apply -f ./hashicorp/vault/example-apps/basic-secret/deployment.yaml
kubectl -n vault-example get pods
```

View File

@ -1,38 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: dynamic-postgres
labels:
app: dynamic-postgres
spec:
selector:
matchLabels:
app: dynamic-postgres
replicas: 1
template:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/tls-skip-verify: "true"
vault.hashicorp.com/agent-inject-secret-sql-role: "database/creds/sql-role"
vault.hashicorp.com/agent-inject-template-sql-role: |
{
{{- with secret "database/creds/sql-role" -}}
"db_connection": "host=postgres.postgress port=5432 user={{ .Data.username }} password={{ .Data.password }} dbname=postgresdb sslmode=disable"
{{- end }}
}
vault.hashicorp.com/role: "sql-role"
labels:
app: dynamic-postgres
spec:
serviceAccountName: dynamic-postgres
containers:
- name: app
image: jweissig/app:0.0.1
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: dynamic-postgres
labels:
app: dynamic-postgres

View File

@ -1,33 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: pgadmin-config
labels:
app: pgadmin
data:
PGADMIN_DEFAULT_EMAIL: admin@admin.com
PGADMIN_DEFAULT_PASSWORD: admin123
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: pgadmin
spec:
selector:
matchLabels:
app: pgadmin
replicas: 1
template:
metadata:
labels:
app: pgadmin
spec:
containers:
- name: pgadmin
image: dpage/pgadmin4
imagePullPolicy: "IfNotPresent"
ports:
- containerPort: 80
envFrom:
- configMapRef:
name: pgadmin-config

Some files were not shown because too many files have changed in this diff Show More