mirror of
https://gitea.com/mcereda/oam.git
synced 2026-02-09 13:44:24 +00:00
chore: imported articles and examples from the private kb
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
FROM python:alpine
|
||||
|
||||
RUN pip3 --no-cache-dir install flask jsonpatch
|
||||
ADD webhook.py /
|
||||
|
||||
USER nobody
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT python3 /webhook.py
|
||||
263
examples/kubernetes/create an admission webhook/README.md
Normal file
263
examples/kubernetes/create an admission webhook/README.md
Normal file
@@ -0,0 +1,263 @@
|
||||
# Create an admission webhook
|
||||
|
||||
The example below will create a webhook which acts as both a `ValidatingAdmissionWebhook` and a `MutatingAdmissionWebhook`, but a real world one can act as only one of them. Or more. Your choice.
|
||||
The procedure is executed in a `minikube` cluster, and will use a self signed certificate for the webhook connection.
|
||||
|
||||
> Be aware of the pros and cons of an `AdmissionWebhook` before deploying one:
|
||||
>
|
||||
> - when deploying the resources it validates it will **need to be up and running**, or those resources will be rejected
|
||||
> - it will need to manage exception to avoid downtime
|
||||
|
||||
## Table of content <!-- omit in toc -->
|
||||
|
||||
1. [Concepts reminder](#concepts-reminder)
|
||||
1. [Check the webhook controllers are enabled](#check-the-webhook-controllers-are-enabled)
|
||||
1. [Write the webhook](#write-the-webhook)
|
||||
1. [Create a certificate](#create-a-certificate)
|
||||
1. [Configure the webhook](#configure-the-webhook)
|
||||
1. [Troubleshooting](#troubleshooting)
|
||||
1. [I cannot deploy the resources because the webhook cannot be reached](#i-cannot-deploy-the-resources-because-the-webhook-cannot-be-reached)
|
||||
1. [Further readings](#further-readings)
|
||||
1. [Sources](#sources)
|
||||
|
||||
## Concepts reminder
|
||||
|
||||
There are 2 special admission controllers in the list included in the Kubernetes `apiserver`:
|
||||
|
||||
- `ValidatingAdmissionWebhook`s, which can reject a request but cannot modify the object they are receiving in the admission request, and
|
||||
- `MutatingAdmissionWebhook`s, which can modify objects by creating a patch that will be sent back in the admission response
|
||||
|
||||
These send admission requests to external HTTP callbacks and receive admission responses. If these two controllers are enabled, a Kubernetes administrator can create and configure an admission webhook in the cluster.
|
||||
|
||||
To do this:
|
||||
|
||||
1. [check if the admission webhook controllers are enabled](#check-the-webhook-controllers-are-enabled) in the cluster, and configure them if needed
|
||||
1. [write the HTTP callback](#write-the-webhook) that will handle an admission requests; this can be a simple HTTP server that's deployed to the cluster, or even a serverless function just like in [Kelsey's validating webhook demo]
|
||||
1. [configure the webhook](#configure-the-webhook) through the `ValidatingWebhookConfiguration` and/or `MutatingWebhookConfiguration` resources
|
||||
|
||||
## Check the webhook controllers are enabled
|
||||
|
||||
Check:
|
||||
|
||||
1. if the `MutatingAdmissionWebhook` and `ValidatingAdmissionWebhook` admission controllers are listed in the correct order in the `admission-control` flag of `kube-apiserver`:
|
||||
|
||||
```sh
|
||||
$ kubectl get pods --namespace 'kube-system' 'kube-apiserver-minikube' -o 'yaml' \
|
||||
| yq -y '.spec.containers[]
|
||||
| select(.name == "kube-apiserver")
|
||||
| .command' \
|
||||
- \
|
||||
| grep -e '--enable-admission-plugins' \
|
||||
| tr ',' '\n' \
|
||||
| grep 'AdmissionWebhook'
|
||||
MutatingAdmissionWebhook
|
||||
ValidatingAdmissionWebhook
|
||||
```
|
||||
|
||||
1. if the admission registration API is enabled in your cluster by running the following:
|
||||
|
||||
```sh
|
||||
$ kubectl api-versions | grep 'admissionregistration.k8s.io'
|
||||
admissionregistration.k8s.io/v1
|
||||
admissionregistration.k8s.io/v1beta1
|
||||
```
|
||||
|
||||
## Write the webhook
|
||||
|
||||
Every language is fine as long as the webhook:
|
||||
|
||||
- accepts an [admission request];
|
||||
- spits out an [admission response];
|
||||
- uses a certificate to secure the connection, as all admission webhooks need to be on SSL; a self-signed certificate will be more than fine for testing
|
||||
|
||||
Example: [webhook.py]
|
||||
|
||||
After the webhook's creation:
|
||||
|
||||
1. create a containerized image of the webhook and save it to the registry
|
||||
Dockerfile: [Dockerfile]
|
||||
|
||||
```sh
|
||||
$ docker build -t webhook .
|
||||
|
||||
# on minikube
|
||||
# will also need imagePullPolicy=Never in the container's spec
|
||||
$ minikube cache add webhook
|
||||
```
|
||||
|
||||
1. (if needed, see [below](#create-a-certificate)) to generate the self signed CA, a `CertificateSigningRequest` and the certificate, then create a `Secret` based on this certificate;
|
||||
1. create a `Deployment` that will use the image created above; the service **must** be secured via SSL, so mount the secret created from the previous step as volumes in it
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: webhook
|
||||
namespace: test
|
||||
labels:
|
||||
app: webhook
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: webhook
|
||||
template:
|
||||
metadata:
|
||||
namespace: test
|
||||
labels:
|
||||
app: webhook
|
||||
spec:
|
||||
containers:
|
||||
- name: webhook
|
||||
image: webhook
|
||||
imagePullPolicy: Never # needed by minikube
|
||||
ports:
|
||||
- containerPort: 8443
|
||||
volumeMounts:
|
||||
- mountPath: /cert
|
||||
name: cert
|
||||
volumes:
|
||||
- name: cert
|
||||
secret:
|
||||
secretName: webhook
|
||||
```
|
||||
|
||||
1. create a `Service` pointing to the correct ports in same namespace as the `Deployment`
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: webhook
|
||||
namespace: test
|
||||
spec:
|
||||
selector:
|
||||
app: webhook
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 443
|
||||
targetPort: 8443
|
||||
```
|
||||
|
||||
### Create a certificate
|
||||
|
||||
For testing purposes, let's just reuse the [script][certificate script] originally written by the Istio team to generate a certificate signing request:
|
||||
|
||||
```sh
|
||||
cd /tmp
|
||||
curl --continue-at - --remote-name https://raw.githubusercontent.com/istio/istio/release-0.7/install/kubernetes/webhook-create-signed-cert.sh
|
||||
bash /tmp/webhook-create-signed-cert.sh --service webhook --namespace test --secret webhook
|
||||
cd -
|
||||
```
|
||||
|
||||
## Configure the webhook
|
||||
|
||||
Create a `ValidatingWebhookConfiguration` to register the service for validation upon pod creation:
|
||||
|
||||
```yaml
|
||||
apiVersion: admissionregistration.k8s.io/v1beta1
|
||||
kind: ValidatingWebhookConfiguration
|
||||
metadata:
|
||||
name: validating-webhook
|
||||
namespace: test
|
||||
webhooks:
|
||||
- name: webhook.example.com
|
||||
failurePolicy: Fail
|
||||
clientConfig:
|
||||
service:
|
||||
name: webhook
|
||||
namespace: test
|
||||
path: /validate
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources:
|
||||
- "pods"
|
||||
apiVersions:
|
||||
- "*"
|
||||
operations:
|
||||
- CREATE
|
||||
```
|
||||
|
||||
At the same way, create a `MutatingWebhookConfiguration` to register the service for mutation upon pod creation:
|
||||
|
||||
```yaml
|
||||
apiVersion: admissionregistration.k8s.io/v1beta1
|
||||
kind: MutatingWebhookConfiguration
|
||||
metadata:
|
||||
name: mutating-webhook
|
||||
namespace: test
|
||||
labels:
|
||||
component: mutating-controller
|
||||
webhooks:
|
||||
- name: webhook.example.com
|
||||
failurePolicy: Fail
|
||||
clientConfig:
|
||||
service:
|
||||
name: webhook
|
||||
namespace: test
|
||||
path: /mutate
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources:
|
||||
- "pods"
|
||||
apiVersions:
|
||||
- "*"
|
||||
operations:
|
||||
- CREATE
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### I cannot deploy the resources because the webhook cannot be reached
|
||||
|
||||
**Solution:** remove the AdmissionWebhook and the Service forwarding to it, then reapply its definition
|
||||
|
||||
## Further readings
|
||||
|
||||
- [Admission request]
|
||||
- [Admission response]
|
||||
- This example's [Dockerfile]
|
||||
- This example's [webhook source][webhook.py]
|
||||
- This example's [resources]
|
||||
- This example's [cert script]
|
||||
- [Extensible admission controllers]
|
||||
|
||||
## Sources
|
||||
|
||||
All the references in the [further readings] section, plus the following:
|
||||
|
||||
- [Creating your own admission controller]
|
||||
- [Diving into Kubernetes mutatingAdmissionWebhook]
|
||||
- [how to write validating and mutating admission controller webhooks in python for kubernetes]
|
||||
- [building a kubernetes mutating admission webhook]
|
||||
- [K8S admission webhooks]
|
||||
- [How to Master Admission Webhooks In Kubernetes]
|
||||
- [openshift's generic admission server]
|
||||
- [kelsey's validating webhook demo]
|
||||
- [morvencao's kube-mutating-webhook-tutorial]
|
||||
- [writing a very basic kubernetes mutating admission webhook]
|
||||
- Istio's [script][certificate script] to generate a certificate signing request
|
||||
|
||||
<!-- project's references -->
|
||||
[admission request]: https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#request
|
||||
[admission response]: https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#response
|
||||
[cert script]: create-signed-cert.sh
|
||||
[certificate script]: https://raw.githubusercontent.com/istio/istio/release-0.7/install/kubernetes/webhook-create-signed-cert.sh
|
||||
[dockerfile]: Dockerfile
|
||||
[extensible admission controllers]: https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/
|
||||
[resources]: resources.yaml
|
||||
[webhook.py]: webhook.py
|
||||
|
||||
<!-- internal references -->
|
||||
[further readings]: #further-readings
|
||||
|
||||
<!-- external references -->
|
||||
[building a kubernetes mutating admission webhook]: https://didil.medium.com/building-a-kubernetes-mutating-admission-webhook-7e48729523ed
|
||||
[creating your own admission controller]: https://docs.giantswarm.io/guides/creating-your-own-admission-controller/
|
||||
[diving into kubernetes mutatingadmissionwebhook]: https://medium.com/ibm-cloud/diving-into-kubernetes-mutatingadmissionwebhook-6ef3c5695f74
|
||||
[how to master admission webhooks in kubernetes]: https://digizoo.com.au/1376/mastering-admission-webhooks-in-kubernetes-gke-part-1/
|
||||
[how to write validating and mutating admission controller webhooks in python for kubernetes]: https://medium.com/analytics-vidhya/how-to-write-validating-and-mutating-admission-controller-webhooks-in-python-for-kubernetes-1e27862cb798
|
||||
[k8s admission webhooks]: https://banzaicloud.com/blog/k8s-admission-webhooks/
|
||||
[kelsey's validating webhook demo]: https://github.com/kelseyhightower/denyenv-validating-admission-webhook
|
||||
[morvencao's kube-mutating-webhook-tutorial]: https://github.com/morvencao/kube-mutating-webhook-tutorial
|
||||
[openshift's generic admission server]: https://github.com/openshift/generic-admission-server
|
||||
[writing a very basic kubernetes mutating admission webhook]: https://medium.com/ovni/writing-a-very-basic-kubernetes-mutating-admission-webhook-398dbbcb63ec
|
||||
131
examples/kubernetes/create an admission webhook/create-signed-cert.sh
Executable file
131
examples/kubernetes/create an admission webhook/create-signed-cert.sh
Executable file
@@ -0,0 +1,131 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Generate certificate suitable for use with an Istio webhook service.
|
||||
|
||||
This script uses k8s' CertificateSigningRequest API to a generate a
|
||||
certificate signed by k8s CA suitable for use with Istio webhook
|
||||
services. This requires permissions to create and approve CSR. See
|
||||
https://kubernetes.io/docs/tasks/tls/managing-tls-in-a-cluster for
|
||||
detailed explantion and additional instructions.
|
||||
|
||||
The server key/cert k8s CA cert are stored in a k8s secret.
|
||||
|
||||
usage: ${0} [OPTIONS]
|
||||
|
||||
The following flags are required.
|
||||
|
||||
--service Service name of webhook.
|
||||
--namespace Namespace where webhook service and secret reside.
|
||||
--secret Secret name for CA certificate and server certificate/key pair.
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case ${1} in
|
||||
--service)
|
||||
service="$2"
|
||||
shift
|
||||
;;
|
||||
--secret)
|
||||
secret="$2"
|
||||
shift
|
||||
;;
|
||||
--namespace)
|
||||
namespace="$2"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
[ -z "${service}" ] && service=istio-sidecar-injector
|
||||
[ -z "${secret}" ] && secret=sidecar-injector-certs
|
||||
[ -z "${namespace}" ] && namespace=istio-system
|
||||
|
||||
if [ ! -x "$(command -v openssl)" ]; then
|
||||
echo "openssl not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
csrName="${service}.${namespace}"
|
||||
tmpdir="$(mktemp -d)"
|
||||
echo "creating certs in tmpdir ${tmpdir} "
|
||||
|
||||
cat <<EOF >> "${tmpdir}/csr.conf"
|
||||
[req]
|
||||
req_extensions = v3_req
|
||||
distinguished_name = req_distinguished_name
|
||||
[req_distinguished_name]
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = serverAuth
|
||||
subjectAltName = @alt_names
|
||||
[alt_names]
|
||||
DNS.1 = ${service}
|
||||
DNS.2 = ${service}.${namespace}
|
||||
DNS.3 = ${service}.${namespace}.svc
|
||||
EOF
|
||||
|
||||
openssl genrsa -out "${tmpdir}/server-key.pem" 2048
|
||||
openssl req -new -key "${tmpdir}/server-key.pem" -subj "/CN=${service}.${namespace}.svc" -out ${tmpdir}/server.csr -config ${tmpdir}/csr.conf
|
||||
|
||||
# clean-up any previously created CSR for our service. Ignore errors if not present.
|
||||
kubectl delete csr "${csrName}" 2>/dev/null || true
|
||||
|
||||
# create server cert/key CSR and send to k8s API
|
||||
cat <<EOF | kubectl create -f -
|
||||
apiVersion: certificates.k8s.io/v1beta1
|
||||
kind: CertificateSigningRequest
|
||||
metadata:
|
||||
name: ${csrName}
|
||||
spec:
|
||||
groups:
|
||||
- system:authenticated
|
||||
request: $(cat "${tmpdir}/server.csr" | base64 | tr -d '\n')
|
||||
usages:
|
||||
- digital signature
|
||||
- key encipherment
|
||||
- server auth
|
||||
EOF
|
||||
|
||||
# verify CSR has been created
|
||||
while true; do
|
||||
kubectl get csr "${csrName}"
|
||||
if [ "$?" -eq 0 ]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# approve and fetch the signed certificate
|
||||
kubectl certificate approve "${csrName}"
|
||||
# verify certificate has been signed
|
||||
for x in $(seq 10); do
|
||||
serverCert="$(kubectl get csr "${csrName}" -o jsonpath='{.status.certificate}')"
|
||||
if [[ "${serverCert}" != '' ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [[ ${serverCert} == '' ]]; then
|
||||
echo "ERROR: After approving csr ${csrName}, the signed certificate did not appear on the resource. Giving up after 10 attempts." >&2
|
||||
echo "See https://istio.io/docs/setup/kubernetes/sidecar-injection.html for more details on troubleshooting." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "${serverCert}" | openssl base64 -d -A -out "${tmpdir}/server-cert.pem"
|
||||
|
||||
|
||||
# create the secret with CA cert and server cert/key
|
||||
kubectl create secret generic "${secret}" \
|
||||
--from-file="key.pem=${tmpdir}/server-key.pem" \
|
||||
--from-file="cert.pem=${tmpdir}/server-cert.pem" \
|
||||
--dry-run -o 'yaml' |
|
||||
kubectl -n "${namespace}" apply -f -
|
||||
@@ -0,0 +1,89 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: webhook
|
||||
namespace: test
|
||||
labels:
|
||||
app: webhook
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: webhook
|
||||
template:
|
||||
metadata:
|
||||
namespace: test
|
||||
labels:
|
||||
app: webhook
|
||||
spec:
|
||||
containers:
|
||||
- name: webhook
|
||||
image: webhook
|
||||
imagePullPolicy: Never # only needed locally
|
||||
ports:
|
||||
- containerPort: 8443
|
||||
volumeMounts:
|
||||
- mountPath: /cert
|
||||
name: cert
|
||||
volumes:
|
||||
- name: cert
|
||||
secret:
|
||||
secretName: webhook
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: webhook
|
||||
namespace: test
|
||||
spec:
|
||||
selector:
|
||||
app: webhook
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 443
|
||||
targetPort: 8443
|
||||
---
|
||||
apiVersion: admissionregistration.k8s.io/v1beta1
|
||||
kind: ValidatingWebhookConfiguration
|
||||
metadata:
|
||||
name: webhook-validate
|
||||
namespace: test
|
||||
webhooks:
|
||||
- name: test.example.com
|
||||
failurePolicy: Fail
|
||||
clientConfig:
|
||||
service:
|
||||
name: webhook
|
||||
namespace: test
|
||||
path: /validate
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources:
|
||||
- "pods"
|
||||
apiVersions:
|
||||
- "*"
|
||||
operations:
|
||||
- CREATE
|
||||
---
|
||||
apiVersion: admissionregistration.k8s.io/v1beta1
|
||||
kind: MutatingWebhookConfiguration
|
||||
metadata:
|
||||
name: webhook-mutate
|
||||
namespace: test
|
||||
labels:
|
||||
component: mutating-controller
|
||||
webhooks:
|
||||
- name: test.example.com
|
||||
failurePolicy: Fail
|
||||
clientConfig:
|
||||
service:
|
||||
name: webhook
|
||||
namespace: test
|
||||
path: /mutate
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources:
|
||||
- "pods"
|
||||
apiVersions:
|
||||
- "*"
|
||||
operations:
|
||||
- CREATE
|
||||
64
examples/kubernetes/create an admission webhook/webhook.py
Normal file
64
examples/kubernetes/create an admission webhook/webhook.py
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from base64 import b64encode
|
||||
from flask import Flask, request, jsonify
|
||||
from jsonpatch import JsonPatch
|
||||
|
||||
admission_webhook = Flask(__name__)
|
||||
|
||||
labels = [
|
||||
"app.kubernetes.io/component",
|
||||
"app.kubernetes.io/name",
|
||||
"app.kubernetes.io/part-of",
|
||||
"app.kubernetes.io/version"
|
||||
]
|
||||
|
||||
@admission_webhook.route('/validate', methods=['POST'])
|
||||
def validate():
|
||||
info = request.get_json()
|
||||
for label in labels:
|
||||
try:
|
||||
info["request"]["object"]["metadata"]["labels"][label]
|
||||
except:
|
||||
return validation_response(info["request"]["uid"], False, f"missing required label '{label}'")
|
||||
return validation_response(info["request"]["uid"], True, "request validated successfully")
|
||||
def validation_response(uid, allowed, message):
|
||||
return jsonify({
|
||||
"response": {
|
||||
"allowed": allowed,
|
||||
"status": {
|
||||
"message": message
|
||||
},
|
||||
"uid": uid
|
||||
}
|
||||
})
|
||||
|
||||
@admission_webhook.route('/mutate', methods=['POST'])
|
||||
def mutate():
|
||||
info = request.get_json()
|
||||
return mutation_response(
|
||||
info["request"]["uid"],
|
||||
True,
|
||||
"adding label 'revised'",
|
||||
json_patch = JsonPatch([{
|
||||
"op": "add",
|
||||
"path": "/metadata/labels/revised",
|
||||
"value": "yes"
|
||||
}])
|
||||
)
|
||||
def mutation_response(uid, allowed, message, json_patch):
|
||||
base64_patch = b64encode(json_patch.to_string().encode("utf-8")).decode("utf-8")
|
||||
return jsonify({
|
||||
"response": {
|
||||
"allowed": allowed,
|
||||
"status": {
|
||||
"message": message
|
||||
},
|
||||
"patchType": "JSONPatch",
|
||||
"patch": base64_patch,
|
||||
"uid": uid
|
||||
}
|
||||
})
|
||||
|
||||
if __name__ == '__main__':
|
||||
admission_webhook.run(host='0.0.0.0', port=8443, ssl_context=("/cert/cert.pem", "/cert/key.pem"))
|
||||
43
knowledge base/golang.md
Normal file
43
knowledge base/golang.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Golang
|
||||
|
||||
## Table of contents <!-- omit in toc -->
|
||||
|
||||
1. [Inspect a variable](#inspect-a-variable)
|
||||
1. [JSON data](#json-data)
|
||||
1. [Create a container image of a Go app](#create-a-container-image-of-a-go-app)
|
||||
1. [Further readings](#further-readings)
|
||||
1. [Sources](#sources)
|
||||
|
||||
## Inspect a variable
|
||||
|
||||
See [how to print struct variables in console]
|
||||
|
||||
## JSON data
|
||||
|
||||
See [deserializing json in go a tutorial]
|
||||
|
||||
## Create a container image of a Go app
|
||||
|
||||
See [building minimal docker containers for go applications] and [create the smallest and secured golang docker image based on scratch]
|
||||
|
||||
## Further readings
|
||||
|
||||
- [Building minimal Docker containers for Go applications]
|
||||
- [Create the smallest and secured Golang Docker image based on Scratch]
|
||||
- [Deserializing JSON in Go a tutorial]
|
||||
- [How to print struct variables in console]
|
||||
|
||||
## Sources
|
||||
|
||||
All the references in the [further readings] section, plus the following:
|
||||
|
||||
<!-- project's references -->
|
||||
|
||||
<!-- internal references -->
|
||||
[further readings]: #further-readings
|
||||
|
||||
<!-- external references -->
|
||||
[building minimal docker containers for go applications]: https://www.cloudbees.com/blog/building-minimal-docker-containers-for-go-applications/
|
||||
[create the smallest and secured golang docker image based on scratch]: https://medium.com/@chemidy/create-the-smallest-and-secured-golang-docker-image-based-on-scratch-4752223b7324
|
||||
[deserializing json in go a tutorial]: https://medium.com/@fsufitch/deserializing-json-in-go-a-tutorial-d042412958ea
|
||||
[how to print struct variables in console]: https://stackoverflow.com/questions/24512112/how-to-print-struct-variables-in-console#24512194
|
||||
Reference in New Issue
Block a user