mirror of
https://github.com/marcel-dempers/docker-development-youtube-series.git
synced 2025-06-06 17:01:30 +00:00
Compare commits
No commits in common. "master" and "part1" have entirely different histories.
@ -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';
|
|
@ -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
|
|
37
.github/workflows/docker._yaml
vendored
37
.github/workflows/docker._yaml
vendored
@ -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
|
|
20
.github/workflows/self-hosted-runner._yaml
vendored
20
.github/workflows/self-hosted-runner._yaml
vendored
@ -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
21
.gitignore
vendored
@ -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
58
.vscode/launch.json
vendored
@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
38
README.md
38
README.md
@ -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>
|
|
||||||
|
@ -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.
|
|
||||||
```
|
|
@ -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"]
|
|
@ -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)
|
|
@ -1 +0,0 @@
|
|||||||
openai==0.28.0
|
|
@ -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
|
|
@ -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>
|
|
@ -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
|
|
@ -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
@ -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
|
|
@ -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
|
|
@ -1,4 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: example-app
|
|
@ -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
|
|
@ -1,11 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: example-service
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: example-app
|
|
||||||
ports:
|
|
||||||
- protocol: TCP
|
|
||||||
port: 80
|
|
||||||
targetPort: 5000
|
|
@ -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"]
|
|
@ -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();
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Information",
|
|
||||||
"Microsoft.AspNetCore": "Warning"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"]
|
|
@ -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"]
|
|
@ -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"
|
|
@ -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
|
|
@ -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"]
|
|
@ -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" });
|
|
||||||
}
|
|
@ -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
|
container_name: golang
|
||||||
volumes:
|
image: aimvector/golang:1.0.0
|
||||||
- ./c#/src/:/work/
|
build: ./golang
|
||||||
ports:
|
nodejs:
|
||||||
- 5000:5000
|
container_name: nodejs
|
||||||
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
|
image: aimvector/nodejs:1.0.0
|
||||||
container_name: golang
|
build: ./nodejs
|
||||||
image: aimvector/golang:1.0.0
|
python:
|
||||||
build:
|
container_name: python
|
||||||
context: ./golang
|
image: aimvector/python:1.0.0
|
||||||
target: prod
|
build: ./python
|
||||||
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
|
|
||||||
image: aimvector/nodejs:1.0.0
|
|
||||||
build:
|
|
||||||
context: ./nodejs
|
|
||||||
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:
|
|
||||||
- ./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
|
|
||||||
|
@ -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>
|
|
@ -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: {}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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: /
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
||||||
```
|
|
@ -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"]
|
|
@ -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 $!
|
|
@ -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: {}
|
|
||||||
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"environment" : "dev"
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
cd /go/src/work
|
|
||||||
dlv debug --headless --log -l 0.0.0.0:2345 --api-version=2
|
|
@ -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
|
|
@ -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
|
|
||||||
}
|
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
module github.com/docker-development-youtube-series/golang/introdouction/app
|
|
||||||
|
|
||||||
go 1.15
|
|
@ -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
|
|
@ -1,3 +0,0 @@
|
|||||||
FROM golang:1.15-alpine as dev
|
|
||||||
|
|
||||||
WORKDIR /work
|
|
@ -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 shouldn’t occur during normal operation, or that we aren’t 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"`
|
|
||||||
}
|
|
||||||
```
|
|
@ -1,3 +0,0 @@
|
|||||||
module videos
|
|
||||||
|
|
||||||
go 1.15
|
|
@ -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)
|
|
||||||
|
|
||||||
}
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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" : ""
|
|
||||||
}
|
|
||||||
]
|
|
@ -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
|
|
@ -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)
|
|
@ -1,3 +0,0 @@
|
|||||||
module videos
|
|
||||||
|
|
||||||
go 1.15
|
|
@ -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!")
|
|
||||||
}
|
|
||||||
}
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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" : ""
|
|
||||||
}
|
|
||||||
]
|
|
@ -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" ]
|
|
@ -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
|
|
||||||
```
|
|
@ -1,3 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
videos $@
|
|
@ -1,3 +0,0 @@
|
|||||||
module videos
|
|
||||||
|
|
||||||
go 1.15
|
|
@ -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)
|
|
||||||
|
|
||||||
}
|
|
Binary file not shown.
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
@ -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
|
|
@ -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
|
|
||||||
```
|
|
@ -1,5 +0,0 @@
|
|||||||
module videos
|
|
||||||
|
|
||||||
go 1.15
|
|
||||||
|
|
||||||
require github.com/go-redis/redis/v8 v8.11.4
|
|
@ -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=
|
|
@ -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!")
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
```
|
|
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"api_key" : "somesecretgoeshere"
|
|
||||||
}
|
|
@ -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))
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
global:
|
|
||||||
datacenter: vault-kubernetes-guide
|
|
||||||
|
|
||||||
client:
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
server:
|
|
||||||
replicas: 1
|
|
||||||
bootstrapExpect: 1
|
|
||||||
disruptionBudget:
|
|
||||||
maxUnavailable: 0
|
|
@ -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
|
|
@ -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"
|
|
||||||
```
|
|
@ -1,7 +0,0 @@
|
|||||||
kind: Cluster
|
|
||||||
apiVersion: kind.x-k8s.io/v1alpha4
|
|
||||||
nodes:
|
|
||||||
- role: control-plane
|
|
||||||
- role: worker
|
|
||||||
- role: worker
|
|
||||||
- role: worker
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"signing": {
|
|
||||||
"default": {
|
|
||||||
"expiry": "175200h"
|
|
||||||
},
|
|
||||||
"profiles": {
|
|
||||||
"default": {
|
|
||||||
"usages": ["signing", "key encipherment", "server auth", "client auth"],
|
|
||||||
"expiry": "175200h"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"hosts": [
|
|
||||||
"cluster.local"
|
|
||||||
],
|
|
||||||
"key": {
|
|
||||||
"algo": "rsa",
|
|
||||||
"size": 2048
|
|
||||||
},
|
|
||||||
"names": [
|
|
||||||
{
|
|
||||||
"C": "AU",
|
|
||||||
"L": "Melbourne",
|
|
||||||
"O": "Example",
|
|
||||||
"OU": "CA",
|
|
||||||
"ST": "Example"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -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/* .
|
|
||||||
```
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
||||||
```
|
|
@ -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
|
|
@ -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
Loading…
x
Reference in New Issue
Block a user