debian 12 + k8s 1.28.2 + metallb OK

vagrant
Grégory Lebreton 10 months ago
parent f25377c48b
commit 91473ac83f

@ -38,6 +38,8 @@ vagrant up
cp configs/config ~/.kube/
```
> On récupère la config du master pour intéragir directement avec la commande kubectl
### Déployer app :rocket:
```bash
@ -58,15 +60,33 @@ kubectl get all
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.7/config/manifests/metallb-native.yaml
```
> Cette config convient pour créer des services de load-balancer pour les cluster bare metal (non cloud)
doc: https://metallb.universe.tf/installation/
- Appliquer les configs pour metallb:
```bash
kubectl apply -f kubernetes/ipaddresspool.yml
kubectl apply -f kubernetes/l2advertisement.yml
```
> Définit les adresses IP utilisées par les services exposant les applicatons
- Modifier son fichier /etc/hosts:
```bash
echo '10.0.0.50 k8s.exemple' | sudo tee -a /etc/hosts
```
>> http://k8s-exemple
### Dashboard
- Obtenir un jeton pour s'authentifier à la dashboard:
```bash
kubectl -n kubernetes-dashboard get secret/admin-user -o go-template="{{.data.token | base64decode}}"
kubectl proxy
```
Adresse de la dashboard: http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/#/overview?namespace=kubernetes-dashboard
>> copier le jeton pour accéder à la dashboard

@ -0,0 +1 @@
kubeadm join 10.0.0.10:6443 --token 7o2su1.uc6nsyqlkz5wgl2e --discovery-token-ca-cert-hash sha256:e5abae8d5f46a9cb5a1784db93c57f7d5838ea922e84426abdb1dc15aca7c986

@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiIsImtpZCI6IlNiVHZDTzRJODRvdDB5cnptSFRKYzc5RjdtMTMzVURITjU0ZkFveGhfRVkifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlcm5ldGVzLWRhc2hib2FyZCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhZG1pbi11c2VyIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImFkbWluLXVzZXIiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiI2ZWZhMzM1Ni1iOWZkLTQ1NWEtYTE5Ni1hZTE5MjQ1OTgyNjMiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZXJuZXRlcy1kYXNoYm9hcmQ6YWRtaW4tdXNlciJ9.Ff1jeqmgYLg1YCqIsHMVuqkLUYNUJfRoqJbSSuEy2VDdAjx3aOKIh5u2qKappAVtbg4ZMAHiJpsGeTnuDFAEmTwZHbfw5zw6GPTHVUYVR-Mkb6GPfHXCB70NwGGebXamIR37ZiIhKd_boumjqaga1iNFKEeENFPsv5fhWV3wco5KPs6K_k8YDrbAJ6liUAElEZI6UK-u5dzsyT8i07izIAiZD89PZ4p4KWY-wQJA2z1dWMuwXVghg9J_M8rqvND7TCGg8eTAH0s_kwJsC_Nly-hjgoFGZJSmxlApVgIx5EJhO7Uw2S9MZWDI5cN3xC3PnNMVMcOgXCfnTkSg0_d9tA

@ -1,41 +1,199 @@
---
apiVersion: v1
kind: Secret
metadata:
name: postgres-credentials
type: Opaque
data:
user: c2FtcGxl
password: cGxlYXNlY2hhbmdlbWU=
---
apiVersion: apps/v1
kind: "Deployment"
kind: Deployment
metadata:
name: devops
name: flask
namespace: default
labels:
app: devops
app: flask
spec:
replicas: 3
replicas: 6
selector:
matchLabels:
app: devops
app: flask
template:
metadata:
labels:
app: devops
app: flask
spec:
containers:
- name: devops
image: hashicorp/http-echo:0.2.3
args:
- "-text=Hello World! This is a Devops Kubernetes app"
- name: flask
image: greglebreton/kubernetes-flask:vagrant
imagePullPolicy: Always
env:
- name: FLASK_ENV
value: development
- name: APP_SETTINGS
value: project.config.DevelopmentConfig
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
key: user
name: postgres-credentials
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
key: password
name: postgres-credentials
# - name: SQLALCHEMY_DATABASE_URI
# value: 'postgresql://sample:pleasechangeme@172.17.0.50:5432/books'
# - name: SQLALCHEMY_DATABASE_URI_DB2
# value: 'postgresql://sample:pleasechangeme@172.17.0.50:5432/books'
# - name: SQLALCHEMY_DATABASE_URI_DB3
# value: 'postgresql://sample:pleasechangeme@172.17.0.50:5432/books'
---
apiVersion: apps/v1
kind: Deployment
# kind: StatefulSet
metadata:
name: postgres
namespace: default
labels:
app: postgres
spec:
# serviceName: postgres
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:13-alpine
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: postgres-credentials
key: user
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-credentials
key: password
volumeMounts:
- mountPath: /var/lib/postgresql/data
name: postgres-volume-mount
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
volumes:
- name: postgres-volume-mount
persistentVolumeClaim:
claimName: postgres-pvc
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: default
creationTimestamp: null
labels:
name: vue
name: vue
spec:
progressDeadlineSeconds: 2147483647
replicas: 6
selector:
matchLabels:
app: vue
template:
metadata:
creationTimestamp: null
labels:
app: vue
spec:
containers:
- image: greglebreton/kubernetes-vue:latest
imagePullPolicy: Always
name: vue
resources: {}
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: postgres-pv
labels:
type: local
spec:
capacity:
storage: 2Gi
storageClassName: standard
accessModes:
- ReadWriteOnce
hostPath:
path: /data/postgres-pv
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
labels:
type: local
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
volumeName: postgres-pv
storageClassName: standard
---
apiVersion: v1
kind: Service
metadata:
name: postgres
# labels:
# app: postgres
spec:
clusterIP: 172.17.0.50
ports:
- port: 5432
selector:
app: postgres
---
apiVersion: v1
kind: Service
metadata:
labels:
app: devops
name: devops-lb
app: vue
name: vue-lb
annotations:
service.beta.kubernetes.io/load-balancer-source-ip: "10.0.0.51"
spec:
ports:
- port: 80
protocol: TCP
targetPort: 5678
targetPort: 8080
selector:
app: devops
app: vue
type: LoadBalancer

@ -0,0 +1,27 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: flask
namespace: default
labels:
app: flask
spec:
replicas: 6
selector:
matchLabels:
app: flask
template:
metadata:
labels:
app: flask
spec:
containers:
- name: flask
image: greglebreton/kubernetes-flask:vagrant
env:
- name: SQLALCHEMY_DATABASE_URI_DB1
value: 'postgres://sample:pleasechangeme@172.17.42.180:5432/books'
- name: SQLALCHEMY_DATABASE_URI_DB2
value: 'postgres://sample:pleasechangeme@172.17.42.180:5432/books'
- name: SQLALCHEMY_DATABASE_URI_DB3
value: 'postgres://sample:pleasechangeme@172.17.42.180:5432/books'

@ -11,7 +11,8 @@ apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.7.0
controller-gen.kubebuilder.io/version: v0.11.1
creationTimestamp: null
name: addresspools.metallb.io
spec:
conversion:
@ -212,18 +213,12 @@ spec:
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.7.0
controller-gen.kubebuilder.io/version: v0.11.1
creationTimestamp: null
name: bfdprofiles.metallb.io
spec:
@ -325,18 +320,12 @@ spec:
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.7.0
controller-gen.kubebuilder.io/version: v0.11.1
creationTimestamp: null
name: bgpadvertisements.metallb.io
spec:
@ -400,8 +389,9 @@ spec:
type: integer
communities:
description: The BGP communities to be associated with the announcement.
Each item can be a community of the form 1234:1234 or the name of
an alias defined in the Community CRD.
Each item can be a standard community of the form 1234:1234, a large
community of the form large:1234:1234:1234 or the name of an alias
defined in the Community CRD.
items:
type: string
type: array
@ -456,6 +446,7 @@ spec:
are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
type: array
ipAddressPools:
description: The list of IPAddressPools to advertise via this advertisement,
@ -520,6 +511,7 @@ spec:
are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
type: array
peers:
description: Peers limits the bgppeer to advertise the ips of the
@ -537,18 +529,13 @@ spec:
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.7.0
controller-gen.kubebuilder.io/version: v0.11.1
creationTimestamp: null
name: bgppeers.metallb.io
spec:
conversion:
@ -791,6 +778,7 @@ spec:
are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
type: array
password:
description: Authentication password for routers enforcing TCP MD5
@ -811,6 +799,7 @@ spec:
name must be unique.
type: string
type: object
x-kubernetes-map-type: atomic
peerASN:
description: AS number to expect from the remote end of the session.
format: int32
@ -832,6 +821,10 @@ spec:
sourceAddress:
description: Source address to use when establishing the session.
type: string
vrf:
description: To set if we want to peer with the BGPPeer using an interface
belonging to a host vrf
type: string
required:
- myASN
- peerASN
@ -845,18 +838,12 @@ spec:
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.7.0
controller-gen.kubebuilder.io/version: v0.11.1
creationTimestamp: null
name: communities.metallb.io
spec:
@ -897,7 +884,8 @@ spec:
type: string
value:
description: The BGP community value corresponding to the given
name.
name. Can be a standard community of the form 1234:1234 or
a large community of the form large:1234:1234:1234.
type: string
type: object
type: array
@ -910,18 +898,12 @@ spec:
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.7.0
controller-gen.kubebuilder.io/version: v0.11.1
creationTimestamp: null
name: ipaddresspools.metallb.io
spec:
@ -982,6 +964,132 @@ spec:
description: AvoidBuggyIPs prevents addresses ending with .0 and .255
to be used by a pool.
type: boolean
serviceAllocation:
description: AllocateTo makes ip pool allocation to specific namespace
and/or service. The controller will use the pool with lowest value
of priority in case of multiple matches. A pool with no priority
set will be used only if the pools with priority can't be used.
If multiple matching IPAddressPools are available it will check
for the availability of IPs sorting the matching IPAddressPools
by priority, starting from the highest to the lowest. If multiple
IPAddressPools have the same priority, choice will be random.
properties:
namespaceSelectors:
description: NamespaceSelectors list of label selectors to select
namespace(s) for ip pool, an alternative to using namespace
list.
items:
description: A label selector is a label query over a set of
resources. The result of matchLabels and matchExpressions
are ANDed. An empty label selector matches all objects. A
null label selector matches no objects.
properties:
matchExpressions:
description: matchExpressions is a list of label selector
requirements. The requirements are ANDed.
items:
description: A label selector requirement is a selector
that contains values, a key, and an operator that relates
the key and values.
properties:
key:
description: key is the label key that the selector
applies to.
type: string
operator:
description: operator represents a key's relationship
to a set of values. Valid operators are In, NotIn,
Exists and DoesNotExist.
type: string
values:
description: values is an array of string values.
If the operator is In or NotIn, the values array
must be non-empty. If the operator is Exists or
DoesNotExist, the values array must be empty. This
array is replaced during a strategic merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value} pairs.
A single {key,value} in the matchLabels map is equivalent
to an element of matchExpressions, whose key field is
"key", the operator is "In", and the values array contains
only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
type: array
namespaces:
description: Namespaces list of namespace(s) on which ip pool
can be attached.
items:
type: string
type: array
priority:
description: Priority priority given for ip pool while ip allocation
on a service.
type: integer
serviceSelectors:
description: ServiceSelectors list of label selector to select
service(s) for which ip pool can be used for ip allocation.
items:
description: A label selector is a label query over a set of
resources. The result of matchLabels and matchExpressions
are ANDed. An empty label selector matches all objects. A
null label selector matches no objects.
properties:
matchExpressions:
description: matchExpressions is a list of label selector
requirements. The requirements are ANDed.
items:
description: A label selector requirement is a selector
that contains values, a key, and an operator that relates
the key and values.
properties:
key:
description: key is the label key that the selector
applies to.
type: string
operator:
description: operator represents a key's relationship
to a set of values. Valid operators are In, NotIn,
Exists and DoesNotExist.
type: string
values:
description: values is an array of string values.
If the operator is In or NotIn, the values array
must be non-empty. If the operator is Exists or
DoesNotExist, the values array must be empty. This
array is replaced during a strategic merge patch.
items:
type: string
type: array
required:
- key
- operator
type: object
type: array
matchLabels:
additionalProperties:
type: string
description: matchLabels is a map of {key,value} pairs.
A single {key,value} in the matchLabels map is equivalent
to an element of matchExpressions, whose key field is
"key", the operator is "In", and the values array contains
only "value". The requirements are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
type: array
type: object
required:
- addresses
type: object
@ -995,18 +1103,12 @@ spec:
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.7.0
controller-gen.kubebuilder.io/version: v0.11.1
creationTimestamp: null
name: l2advertisements.metallb.io
spec:
@ -1111,6 +1213,7 @@ spec:
are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
type: array
ipAddressPools:
description: The list of IPAddressPools to advertise via this advertisement,
@ -1169,6 +1272,7 @@ spec:
are ANDed.
type: object
type: object
x-kubernetes-map-type: atomic
type: array
type: object
status:
@ -1179,12 +1283,6 @@ spec:
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
---
apiVersion: v1
kind: ServiceAccount
@ -1316,6 +1414,14 @@ rules:
- get
- list
- watch
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch
- apiGroups:
- metallb.io
resources:
@ -1384,10 +1490,17 @@ rules:
- ""
resources:
- services
- namespaces
verbs:
- get
- list
- watch
- apiGroups:
- ""
resources:
- nodes
verbs:
- list
- apiGroups:
- ""
resources:
@ -1411,6 +1524,8 @@ rules:
- use
- apiGroups:
- admissionregistration.k8s.io
resourceNames:
- metallb-webhook-configuration
resources:
- validatingwebhookconfigurations
- mutatingwebhookconfigurations
@ -1422,8 +1537,24 @@ rules:
- patch
- update
- watch
- apiGroups:
- admissionregistration.k8s.io
resources:
- validatingwebhookconfigurations
- mutatingwebhookconfigurations
verbs:
- list
- watch
- apiGroups:
- apiextensions.k8s.io
resourceNames:
- addresspools.metallb.io
- bfdprofiles.metallb.io
- bgpadvertisements.metallb.io
- bgppeers.metallb.io
- ipaddresspools.metallb.io
- l2advertisements.metallb.io
- communities.metallb.io
resources:
- customresourcedefinitions
verbs:
@ -1434,6 +1565,13 @@ rules:
- patch
- update
- watch
- apiGroups:
- apiextensions.k8s.io
resources:
- customresourcedefinitions
verbs:
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
@ -1448,6 +1586,7 @@ rules:
- services
- endpoints
- nodes
- namespaces
verbs:
- get
- list
@ -1539,6 +1678,15 @@ subjects:
namespace: metallb-system
---
apiVersion: v1
data:
excludel2.yaml: |
announcedInterfacesToExclude: ["^docker.*", "^cbr.*", "^dummy.*", "^virbr.*", "^lxcbr.*", "^veth.*", "^lo$", "^cali.*", "^tunl.*", "^flannel.*", "^kube-ipvs.*", "^cni.*", "^nodelocaldns.*"]
kind: ConfigMap
metadata:
name: metallb-excludel2
namespace: metallb-system
---
apiVersion: v1
kind: Secret
metadata:
name: webhook-server-cert
@ -1583,12 +1731,13 @@ spec:
- args:
- --port=7472
- --log-level=info
- --tls-min-version=VersionTLS12
env:
- name: METALLB_ML_SECRET_NAME
value: memberlist
- name: METALLB_DEPLOYMENT
value: controller
image: quay.io/metallb/controller:v0.13.7
image: quay.io/metallb/controller:main
livenessProbe:
failureThreshold: 3
httpGet:
@ -1679,12 +1828,9 @@ spec:
fieldPath: status.podIP
- name: METALLB_ML_LABELS
value: app=metallb,component=speaker
- name: METALLB_ML_SECRET_KEY
valueFrom:
secretKeyRef:
key: secretkey
name: memberlist
image: quay.io/metallb/speaker:v0.13.7
- name: METALLB_ML_SECRET_KEY_PATH
value: /etc/ml_secret_key
image: quay.io/metallb/speaker:main
livenessProbe:
failureThreshold: 3
httpGet:
@ -1720,6 +1866,13 @@ spec:
drop:
- ALL
readOnlyRootFilesystem: true
volumeMounts:
- mountPath: /etc/ml_secret_key
name: memberlist
readOnly: true
- mountPath: /etc/metallb
name: metallb-excludel2
readOnly: true
hostNetwork: true
nodeSelector:
kubernetes.io/os: linux
@ -1732,6 +1885,15 @@ spec:
- effect: NoSchedule
key: node-role.kubernetes.io/control-plane
operator: Exists
volumes:
- name: memberlist
secret:
defaultMode: 420
secretName: memberlist
- configMap:
defaultMode: 256
name: metallb-excludel2
name: metallb-excludel2
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration

@ -6,6 +6,12 @@ set -euxo pipefail
# Variable Declaration
# APT update
sudo apt-get update -y
# Dépendances
sudo apt-get install systemd-resolved curl gnupg -y
# DNS Setting
if [ ! -d /etc/systemd/resolved.conf.d ]; then
sudo mkdir /etc/systemd/resolved.conf.d/
@ -45,15 +51,24 @@ EOF
sudo sysctl --system
cat <<EOF | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/ /
EOF
cat <<EOF | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable:cri-o:$VERSION.list
deb http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/$VERSION/$OS/ /
EOF
# Add the GPG key for libcontainers stable repository
curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/$VERSION/$OS/Release.key | gpg --dearmor -o /usr/share/keyrings/libcontainers-archive-keyring.gpg
# Add the libcontainers stable repository to sources.list.d
echo "deb [signed-by=/usr/share/keyrings/libcontainers-archive-keyring.gpg] https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/ /" | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
# Add the cri-o repository to sources.list.d
echo "deb [signed-by=/usr/share/keyrings/libcontainers-archive-keyring.gpg] https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/$VERSION/$OS/ /" | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable:cri-o:$VERSION.list
curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/$VERSION/$OS/Release.key | sudo apt-key --keyring /etc/apt/trusted.gpg.d/libcontainers.gpg add -
curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/Release.key | sudo apt-key --keyring /etc/apt/trusted.gpg.d/libcontainers.gpg add -
# cat <<EOF | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable.list
# deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/ /
# EOF
# cat <<EOF | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:stable:cri-o:$VERSION.list
# deb http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/$VERSION/$OS/ /
# EOF
# curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/$VERSION/$OS/Release.key | sudo apt-key --keyring /etc/apt/trusted.gpg.d/libcontainers.gpg add -
# curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/Release.key | sudo apt-key --keyring /etc/apt/trusted.gpg.d/libcontainers.gpg add -
sudo apt-get update
sudo apt-get install cri-o cri-o-runc -y
@ -68,11 +83,20 @@ echo "CRI runtime installed successfully"
sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl
curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-archive-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update -y
sudo apt-get install -y kubelet="$KUBERNETES_VERSION" kubectl="$KUBERNETES_VERSION" kubeadm="$KUBERNETES_VERSION"
sudo apt-get install gnupg gnupg2 curl software-properties-common -y
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmour -o /etc/apt/trusted.gpg.d/cgoogle.gpg
sudo apt-add-repository "deb http://apt.kubernetes.io/ kubernetes-xenial main"
sudo apt-get update
sudo apt-get install kubelet kubeadm kubectl -y
sudo apt-mark hold kubelet kubeadm kubectl
# curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-archive-keyring.gpg
# echo "deb [signed-by=/etc/apt/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
# sudo apt-get update -y
# sudo apt-get install -y kubelet="$KUBERNETES_VERSION" kubectl="$KUBERNETES_VERSION" kubeadm="$KUBERNETES_VERSION"
sudo apt-get update -y
sudo apt-get install -y jq

@ -30,9 +30,12 @@ nodes:
# - host_path: ../images
# vm_path: /vagrant/images
software:
box: bento/ubuntu-22.04
# box: bento/ubuntu-22.04
box: debian/bookworm64
calico: 3.26.0
# To skip the dashboard installation, set its version to an empty value or comment it out:
dashboard: 2.7.0
kubernetes: 1.27.1-00
os: xUbuntu_22.04
# kubernetes: 1.27.1-00
kubernetes: 1.28.2
# os: xUbuntu_22.04
os: Debian_12

@ -0,0 +1,12 @@
{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}],
"stage-2"
],
"plugins": ["transform-vue-jsx", "transform-runtime"]
}

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

@ -0,0 +1,4 @@
/build/
/config/
/dist/
/*.js

@ -0,0 +1,50 @@
// https://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint'
},
env: {
browser: true,
},
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
extends: ['plugin:vue/essential', 'airbnb-base'],
// required to lint *.vue files
plugins: [
'vue'
],
// check if imports actually resolve
settings: {
'import/resolver': {
webpack: {
config: 'build/webpack.base.conf.js'
}
}
},
// add your custom rules here
rules: {
// don't require .vue extension when importing
'import/extensions': ['error', 'always', {
js: 'never',
vue: 'never'
}],
// disallow reassignment of function parameters
// disallow parameter object manipulation except for specific exclusions
'no-param-reassign': ['error', {
props: true,
ignorePropertyModificationsFor: [
'state', // for vuex state
'acc', // for reduce accumulators
'e' // for e.returnvalue
]
}],
// allow optionalDependencies
'import/no-extraneous-dependencies': ['error', {
optionalDependencies: ['test/unit/index.js']
}],
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
}

@ -0,0 +1,10 @@
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
"plugins": {
"postcss-import": {},
"postcss-url": {},
// to edit target browsers: use "browserslist" field in package.json
"autoprefixer": {}
}
}

@ -0,0 +1,17 @@
FROM node:15-alpine
RUN npm install -g http-server
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN ROOT_API=http://flask.k8s npm run build
EXPOSE 8080
CMD [ "http-server", "dist" ]

@ -0,0 +1,17 @@
FROM node:15-alpine
RUN npm install -g http-server
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN ROOT_API=http://flask.k8s npm run build
EXPOSE 8080
CMD [ "http-server", "dist" ]

@ -0,0 +1,21 @@
# client
> A Vue.js project
## Build Setup
``` bash
# install dependencies
npm install
# serve with hot reload at localhost:8080
npm run dev
# build for production with minification
npm run build
# build for production and view the bundle analyzer report
npm run build --report
```
For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).

@ -0,0 +1,41 @@
'use strict'
require('./check-versions')()
process.env.NODE_ENV = 'production'
const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')
const spinner = ora('building for production...')
spinner.start()
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, (err, stats) => {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
chunks: false,
chunkModules: false
}) + '\n\n')
if (stats.hasErrors()) {
console.log(chalk.red(' Build failed with errors.\n'))
process.exit(1)
}
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})

@ -0,0 +1,54 @@
'use strict'
const chalk = require('chalk')
const semver = require('semver')
const packageConfig = require('../package.json')
const shell = require('shelljs')
function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim()
}
const versionRequirements = [
{
name: 'node',
currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node
}
]
if (shell.which('npm')) {
versionRequirements.push({
name: 'npm',
currentVersion: exec('npm --version'),
versionRequirement: packageConfig.engines.npm
})
}
module.exports = function () {
const warnings = []
for (let i = 0; i < versionRequirements.length; i++) {
const mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' +
chalk.red(mod.currentVersion) + ' should be ' +
chalk.green(mod.versionRequirement)
)
}
}
if (warnings.length) {
console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:'))
console.log()
for (let i = 0; i < warnings.length; i++) {
const warning = warnings[i]
console.log(' ' + warning)
}
console.log()
process.exit(1)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

@ -0,0 +1,101 @@
'use strict'
const path = require('path')
const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const packageConfig = require('../package.json')
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap
}
}
const postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) {
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
const output = []
const loaders = exports.cssLoaders(options)
for (const extension in loaders) {
const loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
exports.createNotifierCallback = () => {
const notifier = require('node-notifier')
return (severity, errors) => {
if (severity !== 'error') return
const error = errors[0]
const filename = error.file && error.file.split('!').pop()
notifier.notify({
title: packageConfig.name,
message: severity + ': ' + error.name,
subtitle: filename || '',
icon: path.join(__dirname, 'logo.png')
})
}
}

@ -0,0 +1,22 @@
'use strict'
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap
module.exports = {
loaders: utils.cssLoaders({
sourceMap: sourceMapEnabled,
extract: isProduction
}),
cssSourceMap: sourceMapEnabled,
cacheBusting: config.dev.cacheBusting,
transformToRequire: {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: 'xlink:href'
}
}

@ -0,0 +1,92 @@
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
const createLintingRule = () => ({
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter'),
emitWarning: !config.dev.showEslintErrorsInOverlay
}
})
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: './src/main.js'
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
module: {
rules: [
...(config.dev.useEslint ? [createLintingRule()] : []),
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
node: {
// prevent webpack from injecting useless setImmediate polyfill because Vue
// source contains it (although only uses it if it's native).
setImmediate: false,
// prevent webpack from injecting mocks to Node native modules
// that does not make sense for the client
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
}
}

@ -0,0 +1,95 @@
'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')
const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)
const devWebpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
},
// cheap-module-eval-source-map is faster for development
devtool: config.dev.devtool,
// these devServer options should be customized in /config/index.js
devServer: {
clientLogLevel: 'warning',
historyApiFallback: {
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
],
},
hot: true,
contentBase: false, // since we use CopyWebpackPlugin.
compress: true,
host: HOST || config.dev.host,
port: PORT || config.dev.port,
open: config.dev.autoOpenBrowser,
overlay: config.dev.errorOverlay
? { warnings: false, errors: true }
: false,
publicPath: config.dev.assetsPublicPath,
proxy: config.dev.proxyTable,
quiet: true, // necessary for FriendlyErrorsPlugin
watchOptions: {
poll: config.dev.poll,
}
},
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
module.exports = new Promise((resolve, reject) => {
portfinder.basePort = process.env.PORT || config.dev.port
portfinder.getPort((err, port) => {
if (err) {
reject(err)
} else {
// publish the new Port, necessary for e2e tests
process.env.PORT = port
// add port to devServer config
devWebpackConfig.devServer.port = port
// Add FriendlyErrorsPlugin
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
compilationSuccessInfo: {
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
},
onErrors: config.dev.notifyOnErrors
? utils.createNotifierCallback()
: undefined
}))
resolve(devWebpackConfig)
}
})
})

@ -0,0 +1,145 @@
'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const env = require('../config/prod.env')
const webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
usePostCSS: true
})
},
devtool: config.build.productionSourceMap ? config.build.devtool : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
'process.env': env
}),
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
}),
// extract css into its own file
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
// Setting the following option to `false` will not extract CSS from codesplit chunks.
// Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
// It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
// increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
allChunks: true,
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } }
: { safe: true }
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}),
// keep module.id stable when vendor modules does not change
new webpack.HashedModuleIdsPlugin(),
// enable scope hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks (module) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
// This instance extracts shared chunks from code splitted chunks and bundles them
// in a separate chunk, similar to the vendor chunk
// see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
if (config.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
}
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig

@ -0,0 +1,7 @@
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})

@ -0,0 +1,76 @@
'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.
const path = require('path')
module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
// Various Dev Server settings
host: 'localhost', // can be overwritten by process.env.HOST
port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
autoOpenBrowser: false,
errorOverlay: true,
notifyOnErrors: true,
poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
// Use Eslint Loader?
// If true, your code will be linted during bundling and
// linting errors and warnings will be shown in the console.
useEslint: true,
// If true, eslint errors and warnings will also be shown in the error overlay
// in the browser.
showEslintErrorsInOverlay: false,
/**
* Source Maps
*/
// https://webpack.js.org/configuration/devtool/#development
devtool: 'cheap-module-eval-source-map',
// If you have problems debugging vue-files in devtools,
// set this to false - it *may* help
// https://vue-loader.vuejs.org/en/options.html#cachebusting
cacheBusting: true,
cssSourceMap: true
},
build: {
// Template for index.html
index: path.resolve(__dirname, '../dist/index.html'),
// Paths
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
/**
* Source Maps
*/
productionSourceMap: true,
// https://webpack.js.org/configuration/devtool/#production
devtool: '#source-map',
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report
}
}

@ -0,0 +1,5 @@
'use strict'
module.exports = {
NODE_ENV: '"production"',
ROOT_API: JSON.stringify(process.env.ROOT_API)
}

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Bookshelf</title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,74 @@
{
"name": "client",
"version": "1.0.0",
"description": "A Vue.js project",
"author": "Michael Herman michael@mherman.org",
"private": true,
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"lint": "eslint --ext .js,.vue src",
"build": "node build/build.js"
},
"dependencies": {
"axios": "^0.19.2",
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.21.2",
"vue": "^2.6.12",
"vue-router": "^3.5.1"
},
"devDependencies": {
"autoprefixer": "^7.1.2",
"babel-core": "^6.22.1",
"babel-eslint": "^8.2.1",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-loader": "^7.1.1",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-plugin-transform-vue-jsx": "^3.5.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"chalk": "^2.0.1",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.0",
"eslint": "^4.15.0",
"eslint-config-airbnb-base": "^11.3.0",
"eslint-friendly-formatter": "^3.0.0",
"eslint-import-resolver-webpack": "^0.8.3",
"eslint-loader": "^1.7.1",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-vue": "^4.0.0",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1",
"node-notifier": "^5.1.2",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"portfinder": "^1.0.13",
"postcss-import": "^11.0.0",
"postcss-loader": "^2.0.8",
"postcss-url": "^7.2.1",
"rimraf": "^2.6.0",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"uglifyjs-webpack-plugin": "^1.1.1",
"url-loader": "^0.5.8",
"vue-loader": "^13.3.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.5.2",
"webpack": "^3.6.0",
"webpack-bundle-analyzer": "^2.9.0",
"webpack-dev-server": "^2.9.1",
"webpack-merge": "^4.1.0"
},
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}

@ -0,0 +1,17 @@
<template>
<div id="app">
<router-view/>
</div>
</template>
<script>
export default {
name: 'App',
};
</script>
<style>
#app {
margin-top: 60px
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

@ -0,0 +1,12 @@
<template>
<div>
<b-alert variant="success" show>{{ message }}</b-alert>
<br>
</div>
</template>
<script>
export default {
props: ['message'],
};
</script>

@ -0,0 +1,255 @@
<template>
<div class="container">
<div class="row">
<div class="col-sm-10">
<h1>Books</h1>
<hr><br><br>
<alert :message=message v-if="showMessage"></alert>
<button type="button" class="btn btn-success btn-sm" v-b-modal.book-modal>Add Book</button>
<br><br>
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Title</th>
<th scope="col">Author</th>
<th scope="col">Read?</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="(book, index) in books" :key="index">
<td>{{ book.title }}</td>
<td>{{ book.author }}</td>
<td>
<span v-if="book.read">Yes</span>
<span v-else>No</span>
</td>
<td>
<button
type="button"
class="btn btn-warning btn-sm"
v-b-modal.book-update-modal
@click="editBook(book)">
Update
</button>
<button
type="button"
class="btn btn-danger btn-sm"
@click="onDeleteBook(book)">
Delete
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<b-modal ref="addBookModal"
id="book-modal"
title="Add a new book"
hide-footer>
<b-form @submit="onSubmit" @reset="onReset" class="w-100">
<b-form-group id="form-title-group"
label="Title:"
label-for="form-title-input">
<b-form-input id="form-title-input"
type="text"
v-model="addBookForm.title"
required
placeholder="Enter title">
</b-form-input>
</b-form-group>
<b-form-group id="form-author-group"
label="Author:"
label-for="form-author-input">
<b-form-input id="form-author-input"
type="text"
v-model="addBookForm.author"
required
placeholder="Enter author">
</b-form-input>
</b-form-group>
<b-form-group id="form-read-group">
<b-form-checkbox-group v-model="addBookForm.read" id="form-checks">
<b-form-checkbox value="true">Read?</b-form-checkbox>
</b-form-checkbox-group>
</b-form-group>
<b-button type="submit" variant="primary">Submit</b-button>
<b-button type="reset" variant="danger">Reset</b-button>
</b-form>
</b-modal>
<b-modal ref="editBookModal"
id="book-update-modal"
title="Update"
hide-footer>
<b-form @submit="onSubmitUpdate" @reset="onResetUpdate" class="w-100">
<b-form-group id="form-title-edit-group"
label="Title:"
label-for="form-title-edit-input">
<b-form-input id="form-title-edit-input"
type="text"
v-model="editForm.title"
required
placeholder="Enter title">
</b-form-input>
</b-form-group>
<b-form-group id="form-author-edit-group"
label="Author:"
label-for="form-author-edit-input">
<b-form-input id="form-author-edit-input"
type="text"
v-model="editForm.author"
required
placeholder="Enter author">
</b-form-input>
</b-form-group>
<b-form-group id="form-read-edit-group">
<b-form-checkbox-group v-model="editForm.read" id="form-checks">
<b-form-checkbox value="true">Read?</b-form-checkbox>
</b-form-checkbox-group>
</b-form-group>
<b-button type="submit" variant="primary">Update</b-button>
<b-button type="reset" variant="danger">Cancel</b-button>
</b-form>
</b-modal>
</div>
</template>
<script>
import axios from 'axios';
import Alert from './Alert';
export default {
data() {
return {
books: [],
addBookForm: {
title: '',
author: '',
read: [],
},
editForm: {
id: '',
title: '',
author: '',
read: [],
},
message: '',
showMessage: false,
ROOT_API: process.env.ROOT_API,
};
},
components: {
alert: Alert,
},
methods: {
getBooks() {
const path = `${this.ROOT_API}/books`;
axios.get(path)
.then((res) => {
this.books = res.data.books;
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
});
},
addBook(payload) {
const path = `${this.ROOT_API}/books`;
axios.post(path, payload)
.then(() => {
this.getBooks();
this.message = 'Book added!';
this.showMessage = true;
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
this.getBooks();
});
},
updateBook(payload, bookID) {
const path = `${this.ROOT_API}/books/${bookID}`;
axios.put(path, payload)
.then(() => {
this.getBooks();
this.message = 'Book updated!';
this.showMessage = true;
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
this.getBooks();
});
},
removeBook(bookID) {
const path = `${this.ROOT_API}/books/${bookID}`;
axios.delete(path)
.then(() => {
this.getBooks();
this.message = 'Book removed!';
this.showMessage = true;
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
this.getBooks();
});
},
initForm() {
this.addBookForm.title = '';
this.addBookForm.author = '';
this.addBookForm.read = [];
this.editForm.id = '';
this.editForm.title = '';
this.editForm.author = '';
this.editForm.read = [];
},
onSubmit(evt) {
evt.preventDefault();
this.$refs.addBookModal.hide();
let read = false;
if (this.addBookForm.read[0]) read = true;
const payload = {
title: this.addBookForm.title,
author: this.addBookForm.author,
read, // property shorthand
};
this.addBook(payload);
this.initForm();
},
onSubmitUpdate(evt) {
evt.preventDefault();
this.$refs.editBookModal.hide();
let read = false;
if (this.editForm.read[0]) read = true;
const payload = {
title: this.editForm.title,
author: this.editForm.author,
read,
};
this.updateBook(payload, this.editForm.id);
},
onReset(evt) {
evt.preventDefault();
this.$refs.addBookModal.hide();
this.initForm();
},
onResetUpdate(evt) {
evt.preventDefault();
this.$refs.editBookModal.hide();
this.initForm();
this.getBooks(); // why?
},
onDeleteBook(book) {
this.removeBook(book.id);
},
editBook(book) {
this.editForm = book;
},
},
created() {
this.getBooks();
},
};
</script>

@ -0,0 +1,113 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<h2>Essential Links</h2>
<ul>
<li>
<a
href="https://vuejs.org"
target="_blank"
>
Core Docs
</a>
</li>
<li>
<a
href="https://forum.vuejs.org"
target="_blank"
>
Forum
</a>
</li>
<li>
<a
href="https://chat.vuejs.org"
target="_blank"
>
Community Chat
</a>
</li>
<li>
<a
href="https://twitter.com/vuejs"
target="_blank"
>
Twitter
</a>
</li>
<br>
<li>
<a
href="http://vuejs-templates.github.io/webpack/"
target="_blank"
>
Docs for This Template
</a>
</li>
</ul>
<h2>Ecosystem</h2>
<ul>
<li>
<a
href="http://router.vuejs.org/"
target="_blank"
>
vue-router
</a>
</li>
<li>
<a
href="http://vuex.vuejs.org/"
target="_blank"
>
vuex
</a>
</li>
<li>
<a
href="http://vue-loader.vuejs.org/"
target="_blank"
>
vue-loader
</a>
</li>
<li>
<a
href="https://github.com/vuejs/awesome-vue"
target="_blank"
>
awesome-vue
</a>
</li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data() {
return {
msg: 'Welcome to Your Vue.js App',
};
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

@ -0,0 +1,34 @@
<template>
<div class="container">
<button type="button" class="btn btn-primary">{{ msg }}</button>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Ping',
data() {
return {
msg: '',
};
},
methods: {
getMessage() {
const path = 'http://hello.world/books/ping';
axios.get(path)
.then((res) => {
this.msg = res.data;
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
});
},
},
created() {
this.getMessage();
},
};
</script>

@ -0,0 +1,17 @@
import 'bootstrap/dist/css/bootstrap.css';
import BootstrapVue from 'bootstrap-vue';
import Vue from 'vue';
import App from './App';
import router from './router';
Vue.config.productionTip = false;
Vue.use(BootstrapVue);
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>',
});

@ -0,0 +1,22 @@
import Vue from 'vue';
import Router from 'vue-router';
import Ping from '@/components/Ping';
import Books from '@/components/Books';
Vue.use(Router);
export default new Router({
routes: [
{
path: '/',
name: 'Books',
component: Books,
},
{
path: '/ping',
name: 'Ping',
component: Ping,
},
],
mode: 'hash',
});

@ -0,0 +1,5 @@
# base image
FROM postgres:13-alpine
# run create.sql on init
ADD create.sql /docker-entrypoint-initdb.d

@ -0,0 +1 @@
CREATE DATABASE books;

@ -0,0 +1,4 @@
env
.dockerignore
Dockerfile
migrations

@ -0,0 +1,24 @@
# base image
FROM python:3.9.4-slim
# install netcat
RUN apt-get update && \
apt-get -y install netcat && \
apt-get clean
# set working directory
WORKDIR /usr/src/app
# add and install requirements
COPY ./requirements.txt /usr/src/app/requirements.txt
RUN pip install -r requirements.txt
# add entrypoint.sh
COPY ./entrypoint.sh /usr/src/app/entrypoint.sh
RUN chmod +x /usr/src/app/entrypoint.sh
# add app
COPY . /usr/src/app
# run server
CMD ["/usr/src/app/entrypoint.sh"]

@ -0,0 +1,11 @@
#!/bin/sh
echo "Waiting for postgres..."
while ! nc -z 172.17.0.50 5432; do
sleep 0.1
done
echo "PostgreSQL started"
gunicorn -b 0.0.0.0:5000 manage:app

@ -0,0 +1,40 @@
from flask.cli import FlaskGroup
from project import create_app, db
from project.api.models import Book
app = create_app()
cli = FlaskGroup(create_app=create_app)
@cli.command('recreate_db')
def recreate_db():
db.drop_all()
db.create_all()
db.session.commit()
@cli.command('seed_db')
def seed_db():
"""Seeds the database."""
db.session.add(Book(
title='On the Road',
author='Jack Kerouac',
read=True
))
db.session.add(Book(
title='Harry Potter and the Philosopher\'s Stone',
author='J. K. Rowling',
read=False
))
db.session.add(Book(
title='Green Eggs and Ham',
author='Dr. Seuss',
read=True
))
db.session.commit()
if __name__ == '__main__':
cli()

@ -0,0 +1,50 @@
import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_cors import CORS
from flask_migrate import Migrate
def create_app(script_info=None):
# instantiate the app
app = Flask(__name__)
# MULTI POSTGRES
# db_uri_db1 = os.environ.get('SQLALCHEMY_DATABASE_URI_DB1')
# db_uri_db2 = os.environ.get('SQLALCHEMY_DATABASE_URI_DB2')
# db_uri_db3 = os.environ.get('SQLALCHEMY_DATABASE_URI_DB3')
# app.config['SQLALCHEMY_DATABASE_URI_DB1'] = db_uri_db1
# app.config['SQLALCHEMY_DATABASE_URI_DB2'] = db_uri_db2
# app.config['SQLALCHEMY_DATABASE_URI_DB3'] = db_uri_db3
# ONE POSTGRES
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('SQLALCHEMY_DATABASE_URI')
# instantiate the extensions
db = SQLAlchemy()
migrate = Migrate()
# enable CORS
CORS(app)
# set config
app_settings = os.getenv('APP_SETTINGS')
app.config.from_object(app_settings)
# set up extensions
db.init_app(app)
migrate.init_app(app, db)
# register blueprints
from project.api.books import books_blueprint
app.register_blueprint(books_blueprint)
# shell context for flask cli
@app.shell_context_processor
def ctx():
return {'app': app, 'db': db}
return app

@ -0,0 +1,62 @@
import os
from flask import Blueprint, jsonify, request
from project.api.models import Book
from project import db
books_blueprint = Blueprint('books', __name__)
@books_blueprint.route('/books', methods=['GET', 'POST'])
def all_books():
response_object = {
'status': 'success',
'container_id': os.uname()[1]
}
if request.method == 'POST':
post_data = request.get_json()
title = post_data.get('title')
author = post_data.get('author')
read = post_data.get('read')
db.session.add(Book(title=title, author=author, read=read))
db.session.commit()
response_object['message'] = 'Book added!'
else:
response_object['books'] = [book.to_json() for book in Book.query.all()]
return jsonify(response_object)
@books_blueprint.route('/books/ping', methods=['GET'])
def ping():
return jsonify({
'status': 'success',
'message': 'pong!',
'container_id': os.uname()[1]
})
@books_blueprint.route('/books/<book_id>', methods=['PUT', 'DELETE'])
def single_book(book_id):
response_object = {
'status': 'success',
'container_id': os.uname()[1]
}
book = Book.query.filter_by(id=book_id).first()
if request.method == 'PUT':
post_data = request.get_json()
book.title = post_data.get('title')
book.author = post_data.get('author')
book.read = post_data.get('read')
db.session.commit()
response_object['message'] = 'Book updated!'
if request.method == 'DELETE':
db.session.delete(book)
db.session.commit()
response_object['message'] = 'Book removed!'
return jsonify(response_object)
if __name__ == '__main__':
app.run()

@ -0,0 +1,29 @@
import datetime
from flask import current_app
from sqlalchemy.sql import func
from project import db
class Book(db.Model):
__tablename__ = 'books'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
title = db.Column(db.String(255), nullable=False)
author = db.Column(db.String(255), nullable=False)
read = db.Column(db.Boolean(), default=False, nullable=False)
def __init__(self, title, author, read):
self.title = title
self.author = author
self.read = read
def to_json(self):
return {
'id': self.id,
'title': self.title,
'author': self.author,
'read': self.read
}

@ -0,0 +1,22 @@
import os
POSTGRES_USER = os.environ.get('POSTGRES_USER')
POSTGRES_PASSWORD = os.environ.get('POSTGRES_PASSWORD')
DATABASE_URL = f'postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@172.17.0.50:5432/books'
class BaseConfig:
"""Base configuration"""
DEBUG = False
TESTING = False
#SQLALCHEMY_TRACK_MODIFICATIONS = False
class DevelopmentConfig(BaseConfig):
"""Development configuration"""
SQLALCHEMY_DATABASE_URI = DATABASE_URL
class ProductionConfig(BaseConfig):
"""Production configuration"""
SQLALCHEMY_DATABASE_URI = DATABASE_URL

@ -0,0 +1,7 @@
flask==2.2.2
flask-sqlalchemy==3.0.2
Flask-Cors==3.0.10
flask-migrate==2.7.0
gunicorn==20.1.0
psycopg2-binary==2.8.6
Werkzeug==2.2.2
Loading…
Cancel
Save