├── hollowapp ├── tests │ ├── __init__.py │ └── tests.py ├── migrations │ ├── README │ ├── __pycache__ │ │ └── env.cpython-36.pyc │ ├── versions │ │ ├── __pycache__ │ │ │ └── 94ad24d5e159_.cpython-36.pyc │ │ └── 94ad24d5e159_.py │ ├── script.py.mako │ ├── alembic.ini │ └── env.py ├── app.db ├── static │ └── loading.gif ├── app │ ├── static │ │ ├── loading.gif │ │ ├── img │ │ │ ├── ahead.jpg │ │ │ ├── hollow.jpg │ │ │ ├── hollow.png │ │ │ └── newfile.jpg │ │ ├── css │ │ │ ├── hollowstyle.css │ │ │ └── bootstrap-responsive.min.css │ │ └── js │ │ │ └── bootstrap.min.js │ ├── auth │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── email.cpython-36.pyc │ │ │ ├── forms.cpython-36.pyc │ │ │ ├── routes.cpython-36.pyc │ │ │ └── __init__.cpython-36.pyc │ │ ├── email.py │ │ ├── forms.py │ │ └── routes.py │ ├── main │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── forms.cpython-36.pyc │ │ │ ├── routes.cpython-36.pyc │ │ │ └── __init__.cpython-36.pyc │ │ ├── forms.py │ │ └── routes.py │ ├── __pycache__ │ │ ├── cli.cpython-36.pyc │ │ ├── email.cpython-36.pyc │ │ ├── models.cpython-36.pyc │ │ ├── search.cpython-36.pyc │ │ ├── __init__.cpython-36.pyc │ │ └── translate.cpython-36.pyc │ ├── errors │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── __init__.cpython-36.pyc │ │ │ └── handlers.cpython-36.pyc │ │ └── handlers.py │ ├── templates │ │ ├── errors │ │ │ ├── 404.html │ │ │ └── 500.html │ │ ├── auth │ │ │ ├── register.html │ │ │ ├── reset_password.html │ │ │ ├── reset_password_request.html │ │ │ └── login.html │ │ ├── email │ │ │ ├── reset_password.txt │ │ │ └── reset_password.html │ │ ├── edit_profile.html │ │ ├── search.html │ │ ├── index.html │ │ ├── tasks.html │ │ ├── _post.html │ │ ├── _task.html │ │ ├── user.html │ │ └── base.html │ ├── email.py │ ├── translate.py │ ├── search.py │ ├── cli.py │ ├── __init__.py │ ├── models.py │ └── translations │ │ └── es │ │ └── LC_MESSAGES │ │ └── messages.po ├── babel.cfg ├── vars │ └── main.yml ├── .env ├── taskapp.py ├── Dockerfile ├── requirements.txt ├── config.py └── boot.sh ├── .gitignore ├── .DS_Store ├── 22-Jobs ├── Dockerfile ├── 1-job.yml └── 2-cronjob.yml ├── img └── kubernetesguide.png ├── 9-Ingress ├── 1-namespace.yml ├── 2-configmap.yml ├── 6-ingress-svc.yml ├── 4-backend.yml ├── 7-app.yml ├── 3-rbac.yml └── 5-ingress-ctrl.yml ├── 16-RBAC ├── 1-serviceaccount.yml ├── 2-role.yml ├── 3-rolebinding.yml └── 4-config-example.yml ├── 12-Secrets ├── 2-Secret.yml ├── 1-HollowDB.yml └── 3-HollowApp.yml ├── 13-PersistentVolumes ├── 4-secret.yml ├── 2-pvc.yml ├── 1-persistentVolume.yml ├── 3-database.yml └── 5-hollowapp.yml ├── 11-ConfigMaps ├── 2-ConfigMap.yml ├── 3-demo-test.yml ├── 1-HollowDB.yml └── 4-HollowApp.yml ├── 5-Endpoints ├── 3-test-container.sh ├── 2-endpoints.yml └── 1-deployment.yml ├── 14-CloudProviders-StorageClasses ├── 5-pvc.yml ├── 3-node-join-example.conf ├── 4-storageclass.yml ├── 1-vsphere.conf └── 2-vsphere-init.conf ├── 22-PodSecurityPolicies ├── 3-cluster-role.yml ├── 4-cluster-role-binding.yml ├── 5-deployment.yml ├── 1-deployment.yml └── 2-psp.yml ├── 15-Statefulsets ├── 1-configmap.yml ├── 2-services.yml └── 3-statefulset.yml ├── 8-Context └── example.yml ├── 7-Namespaces └── namespace.yml ├── 21-NetworkPolicies └── 1-networkpolicy.yml ├── 20-DaemonSets └── 1-daemonset.yml ├── 10-DNS └── 1-nginx-example.yml ├── 6-ServicePublishing ├── 1-servicepublishing.yml └── 2-servicepublishing.yml ├── 2-ReplicaSets └── replicaset.yml ├── 1-Pods └── pod.yml ├── 3-Deployments ├── Deployment1.yml └── Deployment2.yml ├── 19-TaintsAndTolerations ├── 1-deployment.yml └── 2-deployment.yml ├── 4-Services └── services.yml └── README.md /hollowapp/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.DS_Store -------------------------------------------------------------------------------- /hollowapp/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/.DS_Store -------------------------------------------------------------------------------- /22-Jobs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | RUN apk update 4 | RUN apk add curl -------------------------------------------------------------------------------- /hollowapp/app.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app.db -------------------------------------------------------------------------------- /img/kubernetesguide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/img/kubernetesguide.png -------------------------------------------------------------------------------- /9-Ingress/1-namespace.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: ingress 6 | 7 | -------------------------------------------------------------------------------- /hollowapp/static/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/static/loading.gif -------------------------------------------------------------------------------- /hollowapp/app/static/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/static/loading.gif -------------------------------------------------------------------------------- /hollowapp/app/static/img/ahead.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/static/img/ahead.jpg -------------------------------------------------------------------------------- /hollowapp/app/static/img/hollow.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/static/img/hollow.jpg -------------------------------------------------------------------------------- /hollowapp/app/static/img/hollow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/static/img/hollow.png -------------------------------------------------------------------------------- /hollowapp/app/static/img/newfile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/static/img/newfile.jpg -------------------------------------------------------------------------------- /hollowapp/babel.cfg: -------------------------------------------------------------------------------- 1 | [python: app/**.py] 2 | [jinja2: app/templates/**.html] 3 | extensions=jinja2.ext.autoescape,jinja2.ext.with_ 4 | -------------------------------------------------------------------------------- /hollowapp/app/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint('auth', __name__) 4 | 5 | from app.auth import routes 6 | -------------------------------------------------------------------------------- /hollowapp/app/main/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint('main', __name__) 4 | 5 | from app.main import routes 6 | -------------------------------------------------------------------------------- /16-RBAC/1-serviceaccount.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: hollowteam-user 6 | namespace: hollowteam -------------------------------------------------------------------------------- /hollowapp/app/__pycache__/cli.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/__pycache__/cli.cpython-36.pyc -------------------------------------------------------------------------------- /hollowapp/app/__pycache__/email.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/__pycache__/email.cpython-36.pyc -------------------------------------------------------------------------------- /hollowapp/app/__pycache__/models.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/__pycache__/models.cpython-36.pyc -------------------------------------------------------------------------------- /hollowapp/app/__pycache__/search.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/__pycache__/search.cpython-36.pyc -------------------------------------------------------------------------------- /hollowapp/app/errors/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint('errors', __name__) 4 | 5 | from app.errors import handlers 6 | -------------------------------------------------------------------------------- /hollowapp/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | flask_dbpass: "Password123" 3 | flask_dbname: "taskapp" 4 | flask_dbuser: "taskapp" 5 | flask_dbhost: "10.233.4.87" 6 | -------------------------------------------------------------------------------- /hollowapp/app/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /hollowapp/app/__pycache__/translate.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/__pycache__/translate.cpython-36.pyc -------------------------------------------------------------------------------- /hollowapp/app/auth/__pycache__/email.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/auth/__pycache__/email.cpython-36.pyc -------------------------------------------------------------------------------- /hollowapp/app/auth/__pycache__/forms.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/auth/__pycache__/forms.cpython-36.pyc -------------------------------------------------------------------------------- /hollowapp/app/auth/__pycache__/routes.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/auth/__pycache__/routes.cpython-36.pyc -------------------------------------------------------------------------------- /hollowapp/app/main/__pycache__/forms.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/main/__pycache__/forms.cpython-36.pyc -------------------------------------------------------------------------------- /hollowapp/app/main/__pycache__/routes.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/main/__pycache__/routes.cpython-36.pyc -------------------------------------------------------------------------------- /hollowapp/migrations/__pycache__/env.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/migrations/__pycache__/env.cpython-36.pyc -------------------------------------------------------------------------------- /hollowapp/app/auth/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/auth/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /hollowapp/app/main/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/main/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /hollowapp/app/errors/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/errors/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /hollowapp/app/errors/__pycache__/handlers.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/app/errors/__pycache__/handlers.cpython-36.pyc -------------------------------------------------------------------------------- /hollowapp/.env: -------------------------------------------------------------------------------- 1 | echo "SECRET_KEY=52cb883e323b48d78a0a36e8e951ba4a" 2 | echo "MAIL_SERVER=localhost" 3 | echo "MAIL_PORT=25" 4 | echo "ELASTICSEARCH_URI=http://localhost:9200" 5 | 6 | -------------------------------------------------------------------------------- /hollowapp/migrations/versions/__pycache__/94ad24d5e159_.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theITHollow/k8s-guide/HEAD/hollowapp/migrations/versions/__pycache__/94ad24d5e159_.cpython-36.pyc -------------------------------------------------------------------------------- /12-Secrets/2-Secret.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: hollow-secret 5 | data: 6 | db.string: bXlzcWwrcHlteXNxbDovL2hvbGxvd2FwcDpQYXNzd29yZDEyM0Bob2xsb3dkYjozMzA2L2hvbGxvd2FwcA== -------------------------------------------------------------------------------- /13-PersistentVolumes/4-secret.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: hollow-secret 5 | data: 6 | db.string: bXlzcWwrcHlteXNxbDovL2hvbGxvd2FwcDpQYXNzd29yZDEyM0Bob2xsb3dkYjozMzA2L2hvbGxvd2FwcA== -------------------------------------------------------------------------------- /hollowapp/app/templates/errors/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block app_content %} 4 |

{{ _('Not Found') }}

5 |

{{ _('Back') }}

6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /9-Ingress/2-configmap.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: nginx-ingress-controller-conf 6 | labels: 7 | app: nginx-ingress-lb 8 | namespace: ingress 9 | data: 10 | enable-vts-status: 'true' -------------------------------------------------------------------------------- /11-ConfigMaps/2-ConfigMap.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: hollow-config 5 | data: 6 | db.string: "mysql+pymysql://hollowapp:Password123@hollowdb:3306/hollowapp" #Key Value pair Value being a database connection string -------------------------------------------------------------------------------- /5-Endpoints/3-test-container.sh: -------------------------------------------------------------------------------- 1 | kubectl create -f https://k8s.io/examples/application/shell-demo.yaml 2 | 3 | kubectl exec -it shell-demo -- /bin/bash 4 | 5 | ### Execute after accessing shell 6 | apt-get update 7 | apt-get install curl 8 | curl external-web -------------------------------------------------------------------------------- /16-RBAC/2-role.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Role 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | name: hollowteam-full-access 6 | namespace: hollowteam 7 | rules: 8 | - apiGroups: ["", "extensions", "apps"] 9 | resources: ["*"] 10 | verbs: ["*"] -------------------------------------------------------------------------------- /13-PersistentVolumes/2-pvc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: PersistentVolumeClaim 4 | metadata: 5 | name: mysqlvol 6 | spec: 7 | storageClassName: manual 8 | accessModes: 9 | - ReadWriteOnce 10 | resources: 11 | requests: 12 | storage: 10Gi -------------------------------------------------------------------------------- /hollowapp/taskapp.py: -------------------------------------------------------------------------------- 1 | from app import create_app, db, cli 2 | from app.models import User, Post, Task 3 | 4 | app = create_app() 5 | cli.register(app) 6 | 7 | 8 | @app.shell_context_processor 9 | def make_shell_context(): 10 | return {'db': db, 'User': User, 'Post': Post, 'Task' : Task} 11 | -------------------------------------------------------------------------------- /11-ConfigMaps/3-demo-test.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: shell-demo 5 | spec: 6 | containers: 7 | - name: nginx 8 | image: nginx 9 | env: 10 | - name: DATABASE_URL 11 | valueFrom: 12 | configMapKeyRef: 13 | name: hollow-config 14 | key: db.string -------------------------------------------------------------------------------- /14-CloudProviders-StorageClasses/5-pvc.yml: -------------------------------------------------------------------------------- 1 | kind: PersistentVolumeClaim 2 | apiVersion: v1 3 | metadata: 4 | name: test-claim 5 | annotations: 6 | volume.beta.kubernetes.io/storage-class: vsphere-disk 7 | spec: 8 | accessModes: 9 | - ReadWriteOnce 10 | resources: 11 | requests: 12 | storage: 2Gi -------------------------------------------------------------------------------- /22-PodSecurityPolicies/3-cluster-role.yml: -------------------------------------------------------------------------------- 1 | kind: ClusterRole 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: default-restrictedrole 5 | rules: 6 | - apiGroups: 7 | - extensions 8 | resources: 9 | - podsecuritypolicies 10 | resourceNames: 11 | - default-restricted 12 | verbs: 13 | - use -------------------------------------------------------------------------------- /hollowapp/app/templates/errors/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block app_content %} 4 |

{{ _('An unexpected error has occurred') }}

5 |

{{ _('The administrator has been notified. Sorry for the inconvenience!') }}

6 |

{{ _('Back') }}

7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /13-PersistentVolumes/1-persistentVolume.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: mysqlvol 5 | spec: 6 | storageClassName: manual 7 | capacity: 8 | storage: 10Gi #Size of the volume 9 | accessModes: 10 | - ReadWriteOnce #type of access 11 | hostPath: 12 | path: "/mnt/data" #host location 13 | -------------------------------------------------------------------------------- /9-Ingress/6-ingress-svc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: nginx 6 | namespace: ingress 7 | spec: 8 | type: NodePort 9 | ports: 10 | - port: 80 11 | name: http 12 | nodePort: 32000 13 | - port: 18080 14 | name: http-mgmt 15 | selector: 16 | app: nginx-ingress-lb -------------------------------------------------------------------------------- /hollowapp/app/templates/auth/register.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import 'bootstrap/wtf.html' as wtf %} 3 | 4 | {% block app_content %} 5 |

{{ _('Register') }}

6 |
7 |
8 | {{ wtf.quick_form(form) }} 9 |
10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /hollowapp/app/templates/email/reset_password.txt: -------------------------------------------------------------------------------- 1 | Dear {{ user.username }}, 2 | 3 | To reset your password click on the following link: 4 | 5 | {{ url_for('auth.reset_password', token=token, _external=True) }} 6 | 7 | If you have not requested a password reset simply ignore this message. 8 | 9 | Sincerely, 10 | 11 | The Microblog Team 12 | -------------------------------------------------------------------------------- /14-CloudProviders-StorageClasses/3-node-join-example.conf: -------------------------------------------------------------------------------- 1 | apiVersion: kubeadm.k8s.io/v1alpha3 2 | kind: JoinConfiguration 3 | discoveryFile: config.yml 4 | token: cba33r.3f565pcpa8vbxdpu #token used to init the k8s cluster. Yours should be different 5 | nodeRegistration: 6 | kubeletExtraArgs: 7 | cloud-provider: vsphere #cloud provider is enabled on worker -------------------------------------------------------------------------------- /hollowapp/app/templates/edit_profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import 'bootstrap/wtf.html' as wtf %} 3 | 4 | {% block app_content %} 5 |

{{ _('Edit Profile') }}

6 |
7 |
8 | {{ wtf.quick_form(form) }} 9 |
10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /15-Statefulsets/1-configmap.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: mysql 5 | labels: 6 | app: mysql 7 | data: 8 | master.cnf: | 9 | # Apply this config only on the master. 10 | [mysqld] 11 | log-bin 12 | slave.cnf: | 13 | # Apply this config only on slaves. 14 | [mysqld] 15 | super-read-only 16 | -------------------------------------------------------------------------------- /8-Context/example.yml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: hollow-namespace 5 | labels: 6 | name: hollow-namespace 7 | --- 8 | apiVersion: v1 9 | kind: Pod 10 | metadata: 11 | name: nginx 12 | namespace: hollow-namespace 13 | labels: 14 | name: nginx 15 | spec: 16 | containers: 17 | - name: nginx 18 | image: nginx -------------------------------------------------------------------------------- /hollowapp/app/templates/auth/reset_password.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import 'bootstrap/wtf.html' as wtf %} 3 | 4 | {% block app_content %} 5 |

{{ _('Reset Your Password') }}

6 |
7 |
8 | {{ wtf.quick_form(form) }} 9 |
10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /7-Namespaces/namespace.yml: -------------------------------------------------------------------------------- 1 | kind: Namespace 2 | apiVersion: v1 3 | metadata: 4 | name: hollow-namespace 5 | labels: 6 | name: hollow-namespace 7 | --- 8 | apiVersion: v1 9 | kind: Pod 10 | metadata: 11 | name: nginx 12 | namespace: hollow-namespace 13 | labels: 14 | name: nginx 15 | spec: 16 | containers: 17 | - name: nginx 18 | image: nginx -------------------------------------------------------------------------------- /hollowapp/app/templates/auth/reset_password_request.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import 'bootstrap/wtf.html' as wtf %} 3 | 4 | {% block app_content %} 5 |

{{ _('Reset Password') }}

6 |
7 |
8 | {{ wtf.quick_form(form) }} 9 |
10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /16-RBAC/3-rolebinding.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: RoleBinding 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | name: hollowteam-role-binding 6 | namespace: hollowteam 7 | subjects: 8 | - kind: ServiceAccount 9 | name: hollowteam-user 10 | namespace: hollowteam 11 | roleRef: 12 | apiGroup: rbac.authorization.k8s.io 13 | kind: Role 14 | name: hollowteam-full-access -------------------------------------------------------------------------------- /22-PodSecurityPolicies/4-cluster-role-binding.yml: -------------------------------------------------------------------------------- 1 | kind: ClusterRoleBinding 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | metadata: 4 | name: default-psp-rolebinding 5 | subjects: 6 | - kind: Group 7 | name: system:serviceaccounts 8 | namespace: kube-system 9 | roleRef: 10 | kind: ClusterRole 11 | name: default-restrictedrole 12 | apiGroup: rbac.authorization.k8s.io -------------------------------------------------------------------------------- /14-CloudProviders-StorageClasses/4-storageclass.yml: -------------------------------------------------------------------------------- 1 | kind: StorageClass 2 | apiVersion: storage.k8s.io/v1 3 | metadata: 4 | name: vsphere-disk #name of the default storage class 5 | annotations: 6 | storageclass.kubernetes.io/is-default-class: "true" #Make this a default storage class. There can be only 1 or it won't work. 7 | provisioner: kubernetes.io/vsphere-volume 8 | parameters: 9 | diskformat: thin -------------------------------------------------------------------------------- /hollowapp/app/errors/handlers.py: -------------------------------------------------------------------------------- 1 | from flask import render_template 2 | from app import db 3 | from app.errors import bp 4 | 5 | 6 | @bp.app_errorhandler(404) 7 | def not_found_error(error): 8 | return render_template('errors/404.html'), 404 9 | 10 | 11 | @bp.app_errorhandler(500) 12 | def internal_error(error): 13 | db.session.rollback() 14 | return render_template('errors/500.html'), 500 15 | -------------------------------------------------------------------------------- /22-Jobs/1-job.yml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: Job 3 | metadata: 4 | name: hollow-curl 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: hollow-curl 10 | image: theithollow/hollowapp-blog:curl 11 | args: 12 | - /bin/sh 13 | - -c 14 | - curl https://theithollow.com 15 | restartPolicy: Never 16 | backoffLimit: 4 -------------------------------------------------------------------------------- /22-Jobs/2-cronjob.yml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1beta1 2 | kind: CronJob 3 | metadata: 4 | name: hollow-curl 5 | spec: 6 | schedule: "0 * * * *" 7 | jobTemplate: 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: hollow-curl 13 | image: theithollow/hollowapp-blog:curl 14 | args: 15 | - /bin/sh 16 | - -c 17 | - curl https://theithollow.com 18 | restartPolicy: OnFailure -------------------------------------------------------------------------------- /14-CloudProviders-StorageClasses/1-vsphere.conf: -------------------------------------------------------------------------------- 1 | [Global] 2 | user = "k8s@hollow.local" 3 | password = "Password123" 4 | port = "443" 5 | insecure-flag = "1" 6 | datacenters = "HollowLab" 7 | 8 | [VirtualCenter "vcenter1.hollow.local"] 9 | 10 | [Workspace] 11 | server = "vcenter1.hollow.local" 12 | datacenter = "HollowLab" 13 | default-datastore="Synology02-NFS01" 14 | resourcepool-path="HollowCluster/Resources" 15 | folder = "Kubernetes" 16 | 17 | [Disk] 18 | scsicontrollertype = pvscsi 19 | -------------------------------------------------------------------------------- /21-NetworkPolicies/1-networkpolicy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: networking.k8s.io/v1 3 | kind: NetworkPolicy 4 | metadata: 5 | name: hollow-db-policy #policy name 6 | spec: 7 | podSelector: 8 | matchLabels: 9 | app: hollowdb #pod applied to 10 | policyTypes: 11 | - Ingress #Ingress and/or Egress 12 | ingress: 13 | - from: 14 | - podSelector: 15 | matchLabels: 16 | app: hollowapp #pod allowed 17 | ports: 18 | - protocol: TCP 19 | port: 3306 #port allowed -------------------------------------------------------------------------------- /16-RBAC/4-config-example.yml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Config 3 | preferences: {} 4 | users: 5 | - name: [user here] 6 | user: 7 | token: [client token here] 8 | clusters: 9 | - cluster: 10 | certificate-authority-data: [client certificate here] 11 | server: https://[ip or dns entry here for cluster:6443 12 | name: [cluster name.local] 13 | contexts: 14 | - context: 15 | cluster: [cluster name.local] 16 | user: [user here] 17 | namespace: [namespace] 18 | name: [context name] 19 | current-context: [set context] -------------------------------------------------------------------------------- /22-PodSecurityPolicies/5-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: escalated 5 | labels: 6 | app: escalated 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: escalated 12 | template: 13 | metadata: 14 | labels: 15 | app: escalated 16 | spec: 17 | containers: 18 | - name: not-escalated 19 | image: busybox 20 | command: ["sleep", "3600"] 21 | securityContext: 22 | allowPrivilegeEscalation: true -------------------------------------------------------------------------------- /22-PodSecurityPolicies/1-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: not-escalated 5 | labels: 6 | app: not-escalated 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: not-escalated 12 | template: 13 | metadata: 14 | labels: 15 | app: not-escalated 16 | spec: 17 | containers: 18 | - name: not-escalated 19 | image: busybox 20 | command: ["sleep", "3600"] 21 | securityContext: 22 | allowPrivilegeEscalation: false -------------------------------------------------------------------------------- /hollowapp/app/templates/email/reset_password.html: -------------------------------------------------------------------------------- 1 |

Dear {{ user.username }},

2 |

3 | To reset your password 4 | 5 | click here 6 | . 7 |

8 |

Alternatively, you can paste the following link in your browser's address bar:

9 |

{{ url_for('auth.reset_password', token=token, _external=True) }}

10 |

If you have not requested a password reset simply ignore this message.

11 |

Sincerely,

12 |

The Microblog Team

13 | -------------------------------------------------------------------------------- /5-Endpoints/2-endpoints.yml: -------------------------------------------------------------------------------- 1 | kind: "Service" 2 | apiVersion: "v1" 3 | metadata: 4 | name: "external-web" 5 | spec: 6 | ports: 7 | - 8 | name: "apache" 9 | protocol: "TCP" 10 | port: 80 11 | targetPort: 80 12 | --- 13 | kind: "Endpoints" 14 | apiVersion: "v1" 15 | metadata: 16 | name: "external-web" 17 | subsets: 18 | - 19 | addresses: 20 | - 21 | ip: "10.10.50.53" #The IP Address of the external web server 22 | ports: 23 | - 24 | port: 80 25 | name: "apache" -------------------------------------------------------------------------------- /hollowapp/app/email.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from flask import current_app 3 | from flask_mail import Message 4 | from app import mail 5 | 6 | 7 | def send_async_email(app, msg): 8 | with app.app_context(): 9 | mail.send(msg) 10 | 11 | 12 | def send_email(subject, sender, recipients, text_body, html_body): 13 | msg = Message(subject, sender=sender, recipients=recipients) 14 | msg.body = text_body 15 | msg.html = html_body 16 | Thread(target=send_async_email, 17 | args=(current_app._get_current_object(), msg)).start() 18 | -------------------------------------------------------------------------------- /20-DaemonSets/1-daemonset.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: DaemonSet 3 | metadata: 4 | name: daemonset-example 5 | labels: 6 | app: daemonset-example 7 | spec: 8 | selector: 9 | matchLabels: 10 | app: daemonset-example 11 | template: 12 | metadata: 13 | labels: 14 | app: daemonset-example 15 | spec: 16 | tolerations: 17 | - key: node-role.kubernetes.io/master 18 | effect: NoSchedule 19 | containers: 20 | - name: busybox 21 | image: busybox 22 | args: 23 | - sleep 24 | - "10000" -------------------------------------------------------------------------------- /hollowapp/app/templates/auth/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% import 'bootstrap/wtf.html' as wtf %} 3 | 4 | {% block app_content %} 5 |

{{ _('Sign In') }}

6 |
7 |
8 | {{ wtf.quick_form(form) }} 9 |
10 |
11 |
12 |

{{ _('New User?') }} {{ _('Click to Register!') }}

13 |

14 | {{ _('Forgot Your Password?') }} 15 | {{ _('Click to Reset It') }} 16 |

17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /hollowapp/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /15-Statefulsets/2-services.yml: -------------------------------------------------------------------------------- 1 | # Headless service for stable DNS entries of StatefulSet members. 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: mysql 6 | labels: 7 | app: mysql 8 | spec: 9 | ports: 10 | - name: mysql 11 | port: 3306 12 | clusterIP: None 13 | selector: 14 | app: mysql 15 | --- 16 | # Client service for connecting to any MySQL instance for reads. 17 | # For writes, you must instead connect to the master: mysql-0.mysql. 18 | apiVersion: v1 19 | kind: Service 20 | metadata: 21 | name: mysql-read 22 | labels: 23 | app: mysql 24 | spec: 25 | ports: 26 | - name: mysql 27 | port: 3306 28 | selector: 29 | app: mysql -------------------------------------------------------------------------------- /hollowapp/app/auth/email.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, current_app 2 | from flask_babel import _ 3 | from app.email import send_email 4 | 5 | 6 | def send_password_reset_email(user): 7 | token = user.get_reset_password_token() 8 | send_email(_('[Microblog] Reset Your Password'), 9 | sender=current_app.config['ADMINS'][0], 10 | recipients=[user.email], 11 | text_body=render_template('email/reset_password.txt', 12 | user=user, token=token), 13 | html_body=render_template('email/reset_password.html', 14 | user=user, token=token)) 15 | -------------------------------------------------------------------------------- /hollowapp/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6-alpine 2 | 3 | RUN adduser -D ubuntu && \ 4 | mkdir -p /home/ubuntu 5 | 6 | WORKDIR /home/ubuntu 7 | COPY ./ ./ 8 | RUN apk add --update \ 9 | && apk add --no-cache --virtual .build-deps \ 10 | mariadb-dev \ 11 | gcc \ 12 | musl-dev \ 13 | && apk del .build-deps && \ 14 | apk add --no-cache mysql-client && \ 15 | python -m venv venv && \ 16 | venv/bin/pip install --upgrade setuptools && \ 17 | venv/bin/pip install -r requirements.txt && \ 18 | venv/bin/pip install gunicorn PyMySQL==0.8.1 && \ 19 | chmod +x boot.sh && \ 20 | chown -R ubuntu:ubuntu /home/ubuntu/ 21 | 22 | ENV FLASK_APP taskapp.py 23 | USER ubuntu 24 | EXPOSE 5000 25 | ENTRYPOINT ["./boot.sh"] -------------------------------------------------------------------------------- /22-PodSecurityPolicies/2-psp.yml: -------------------------------------------------------------------------------- 1 | apiVersion: policy/v1beta1 2 | kind: PodSecurityPolicy 3 | metadata: 4 | name: default-restricted 5 | spec: 6 | privileged: false 7 | hostNetwork: false 8 | allowPrivilegeEscalation: false #This is the main setting we're looking at in this blog post. 9 | defaultAllowPrivilegeEscalation: false 10 | hostPID: false 11 | hostIPC: false 12 | runAsUser: 13 | rule: RunAsAny 14 | fsGroup: 15 | rule: RunAsAny 16 | seLinux: 17 | rule: RunAsAny 18 | supplementalGroups: 19 | rule: RunAsAny 20 | volumes: 21 | - 'configMap' 22 | - 'downwardAPI' 23 | - 'emptyDir' 24 | - 'persistentVolumeClaim' 25 | - 'secret' 26 | - 'projected' 27 | allowedCapabilities: 28 | - '*' -------------------------------------------------------------------------------- /10-DNS/1-nginx-example.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nginx-deployment 5 | labels: 6 | app: nginx 7 | spec: 8 | replicas: 2 9 | selector: 10 | matchLabels: 11 | app: nginx 12 | template: 13 | metadata: 14 | labels: 15 | app: nginx 16 | spec: 17 | containers: 18 | - name: nginx-container 19 | image: nginx 20 | ports: 21 | - containerPort: 80 22 | --- 23 | apiVersion: v1 24 | kind: Service 25 | metadata: 26 | name: nginxsvc 27 | spec: 28 | type: NodePort 29 | ports: 30 | - name: http 31 | port: 80 32 | targetPort: 80 33 | nodePort: 30001 34 | protocol: TCP 35 | selector: 36 | app: nginx -------------------------------------------------------------------------------- /6-ServicePublishing/1-servicepublishing.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nginx-deployment 5 | labels: 6 | app: nginx 7 | spec: 8 | replicas: 2 9 | selector: 10 | matchLabels: 11 | app: nginx 12 | template: 13 | metadata: 14 | labels: 15 | app: nginx 16 | spec: 17 | containers: 18 | - name: nginx-container 19 | image: nginx 20 | ports: 21 | - containerPort: 80 22 | --- 23 | apiVersion: v1 24 | kind: Service 25 | metadata: 26 | name: ingress-nginx 27 | spec: 28 | type: ClusterIP 29 | ports: 30 | - name: http 31 | port: 80 32 | targetPort: 80 33 | protocol: TCP 34 | selector: 35 | app: nginx -------------------------------------------------------------------------------- /12-Secrets/1-HollowDB.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: hollowdb 5 | labels: 6 | app: hollowdb 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: hollowdb 12 | template: 13 | metadata: 14 | labels: 15 | app: hollowdb 16 | spec: 17 | containers: 18 | - name: mysql 19 | image: theithollow/hollowapp-blog:dbv1 20 | imagePullPolicy: Always 21 | ports: 22 | - containerPort: 3306 23 | --- 24 | apiVersion: v1 25 | kind: Service 26 | metadata: 27 | name: hollowdb 28 | spec: 29 | ports: 30 | - name: mysql 31 | port: 3306 32 | targetPort: 3306 33 | protocol: TCP 34 | selector: 35 | app: hollowdb -------------------------------------------------------------------------------- /6-ServicePublishing/2-servicepublishing.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: nginx-deployment 5 | labels: 6 | app: nginx 7 | spec: 8 | replicas: 2 9 | selector: 10 | matchLabels: 11 | app: nginx 12 | template: 13 | metadata: 14 | labels: 15 | app: nginx 16 | spec: 17 | containers: 18 | - name: nginx-container 19 | image: nginx 20 | ports: 21 | - containerPort: 80 22 | --- 23 | apiVersion: v1 24 | kind: Service 25 | metadata: 26 | name: ingress-nginx 27 | spec: 28 | type: NodePort 29 | ports: 30 | - name: http 31 | port: 80 32 | targetPort: 80 33 | nodePort: 30001 34 | protocol: TCP 35 | selector: 36 | app: nginx -------------------------------------------------------------------------------- /11-ConfigMaps/1-HollowDB.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: hollowdb 5 | labels: 6 | app: hollowdb 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: hollowdb 12 | template: 13 | metadata: 14 | labels: 15 | app: hollowdb 16 | spec: 17 | containers: 18 | - name: mysql 19 | image: theithollow/hollowapp-blog:dbv1 20 | imagePullPolicy: Always 21 | ports: 22 | - containerPort: 3306 23 | --- 24 | apiVersion: v1 25 | kind: Service 26 | metadata: 27 | name: hollowdb 28 | spec: 29 | ports: 30 | - name: mysql 31 | port: 3306 32 | targetPort: 3306 33 | protocol: TCP 34 | selector: 35 | app: hollowdb -------------------------------------------------------------------------------- /hollowapp/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==0.9.6 2 | Babel==2.5.1 3 | blinker==1.4 4 | certifi==2017.7.27.1 5 | chardet==3.0.4 6 | click==6.7 7 | dominate==2.3.1 8 | elasticsearch==6.0.0 9 | Flask==1.0 10 | Flask-Babel==0.11.2 11 | Flask-Bootstrap==3.3.7.1 12 | Flask-Login==0.4.0 13 | Flask-Mail==0.9.1 14 | Flask-Migrate==2.1.1 15 | Flask-Moment==0.5.2 16 | Flask-SQLAlchemy==2.3.2 17 | Flask-WTF==0.14.2 18 | guess-language-spirit==0.5.3 19 | idna==2.6 20 | itsdangerous==0.24 21 | Jinja2==2.10 22 | Mako==1.0.7 23 | MarkupSafe==1.0 24 | PyJWT==1.5.3 25 | python-dateutil==2.6.1 26 | python-dotenv==0.7.1 27 | python-editor==1.0.3 28 | pytz==2017.2 29 | requests==2.20.0 30 | six==1.11.0 31 | SQLAlchemy==1.1.14 32 | urllib3==1.24.2 33 | visitor==0.1.3 34 | Werkzeug==0.15.3 35 | WTForms==2.1 36 | gunicorn 37 | pathlib 38 | unittest-xml-reporting 39 | coverage -------------------------------------------------------------------------------- /hollowapp/app/templates/search.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block app_content %} 4 |

{{ _('Search Results') }}

5 | {% for post in posts %} 6 | {% include '_post.html' %} 7 | {% endfor %} 8 | 22 | {% endblock %} 23 | -------------------------------------------------------------------------------- /hollowapp/app/translate.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | from flask import current_app 4 | from flask_babel import _ 5 | 6 | 7 | def translate(text, source_language, dest_language): 8 | if 'MS_TRANSLATOR_KEY' not in current_app.config or \ 9 | not current_app.config['MS_TRANSLATOR_KEY']: 10 | return _('Error: the translation service is not configured.') 11 | auth = { 12 | 'Ocp-Apim-Subscription-Key': current_app.config['MS_TRANSLATOR_KEY']} 13 | r = requests.get('https://api.microsofttranslator.com/v2/Ajax.svc' 14 | '/Translate?text={}&from={}&to={}'.format( 15 | text, source_language, dest_language), 16 | headers=auth) 17 | if r.status_code != 200: 18 | return _('Error: the translation service failed.') 19 | return json.loads(r.content.decode('utf-8-sig')) 20 | -------------------------------------------------------------------------------- /2-ReplicaSets/replicaset.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 #version of the API to use 2 | kind: ReplicaSet #What kind of object we're deploying 3 | metadata: #information about our object we're deploying 4 | name: nginx-replicaset 5 | spec: #specifications for our object 6 | replicas: 2 #The number of pods that should always be running 7 | selector: #which pods the replica set should be responsible for 8 | matchLabels: 9 | app: nginx #any pods with labels matching this I'm responsible for. 10 | template: #The pod template that gets deployed 11 | metadata: 12 | labels: #A tag on the pods created 13 | app: nginx 14 | spec: 15 | containers: 16 | - name: nginx-container #the name of the container within the pod 17 | image: nginx #which container image should be pulled 18 | ports: 19 | - containerPort: 80 #the port of the container within the pod -------------------------------------------------------------------------------- /1-Pods/pod.yml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: v1 #version of the API to use 3 | kind: Pod #What kind of object we're deploying 4 | metadata: #information about our object we're deploying 5 | name: nginx-pod 6 | spec: #specifications for our object 7 | containers: 8 | - name: nginx-container #the name of the container within the pod 9 | image: nginx #which container image should be pulled 10 | ports: 11 | - containerPort: 80 #the port of the container within the pod 12 | apiVersion: v1 #version of the API to use 13 | kind: Pod #What kind of object we're deploying 14 | metadata: #information about our object we're deploying 15 | name: nginx-pod 16 | spec: #specifications for our object 17 | containers: 18 | - name: nginx-container #the name of the container within the pod 19 | image: nginx #which container image should be pulled 20 | ports: 21 | - containerPort: 80 #the port of the container within the pod -------------------------------------------------------------------------------- /hollowapp/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | basedir = os.path.abspath(os.path.dirname(__file__)) 5 | load_dotenv(os.path.join(basedir, '.env')) 6 | 7 | 8 | class Config(object): 9 | SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess' 10 | SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ 11 | 'sqlite:///' + os.path.join(basedir, 'app.db') 12 | SQLALCHEMY_TRACK_MODIFICATIONS = False 13 | MAIL_SERVER = 'smtp.googlemail.com' 14 | MAIL_PORT = int(os.environ.get('MAIL_PORT') or 587) 15 | MAIL_USE_TLS = 1 16 | MAIL_USERNAME = os.environ.get('MAIL_USERNAME') 17 | MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD') 18 | ADMINS = ['user@domain.com'] 19 | LANGUAGES = ['en', 'es'] 20 | MS_TRANSLATOR_KEY = os.environ.get('MS_TRANSLATOR_KEY') 21 | ELASTICSEARCH_URI = os.environ.get('ELASTICSEARCH_URI') 22 | POSTS_PER_PAGE = 25 23 | -------------------------------------------------------------------------------- /hollowapp/migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /3-Deployments/Deployment1.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 #version of the API to use 2 | kind: Deployment #What kind of object we're deploying 3 | metadata: #information about our object we're deploying 4 | name: nginx-deployment #Name of the deployment 5 | labels: #A tag on the deployments created 6 | app: nginx 7 | spec: #specifications for our object 8 | replicas: 2 #The number of pods that should always be running 9 | selector: #which pods the replica set should be responsible for 10 | matchLabels: 11 | app: nginx #any pods with labels matching this I'm responsible for. 12 | template: #The pod template that gets deployed 13 | metadata: 14 | labels: #A tag on the replica sets created 15 | app: nginx 16 | spec: 17 | containers: 18 | - name: nginx-container #the name of the container within the pod 19 | image: nginx #which container image should be pulled 20 | ports: 21 | - containerPort: 80 #the port of the container within the pod -------------------------------------------------------------------------------- /13-PersistentVolumes/3-database.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: hollowdb 5 | labels: 6 | app: hollowdb 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: hollowdb 12 | strategy: 13 | type: Recreate 14 | template: 15 | metadata: 16 | labels: 17 | app: hollowdb 18 | spec: 19 | containers: 20 | - name: mysql 21 | image: theithollow/hollowapp-blog:dbv1 22 | imagePullPolicy: Always 23 | ports: 24 | - containerPort: 3306 25 | volumeMounts: 26 | - name: mysqlstorage 27 | mountPath: /var/lib/mysql 28 | volumes: 29 | - name: mysqlstorage 30 | persistentVolumeClaim: 31 | claimName: mysqlvol 32 | --- 33 | apiVersion: v1 34 | kind: Service 35 | metadata: 36 | name: hollowdb 37 | spec: 38 | ports: 39 | - name: mysql 40 | port: 3306 41 | targetPort: 3306 42 | protocol: TCP 43 | selector: 44 | app: hollowdb -------------------------------------------------------------------------------- /hollowapp/app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import 'bootstrap/wtf.html' as wtf %} 3 | 4 | {% block app_content %} 5 |

{{ _('Hi, %(username)s!', username=current_user.username) }}

6 | {% if form %} 7 | {{ wtf.quick_form(form) }} 8 |
9 | {% endif %} 10 | {% for post in posts %} 11 | {% include '_post.html' %} 12 | {% endfor %} 13 | 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /hollowapp/app/templates/tasks.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% import 'bootstrap/wtf.html' as wtf %} 3 | 4 | {% block app_content %} 5 |

{{ _('Hi, %(username)s!', username=current_user.username) }}

6 | {% if form %} 7 | {{ wtf.quick_form(form) }} 8 |
9 | {% endif %} 10 | {% for task in tasks %} 11 | {% include '_task.html' %} 12 | {% endfor %} 13 | 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /hollowapp/app/search.py: -------------------------------------------------------------------------------- 1 | from flask import current_app 2 | 3 | 4 | def add_to_index(index, model): 5 | if not current_app.elasticsearch: 6 | return 7 | payload = {} 8 | for field in model.__searchable__: 9 | payload[field] = getattr(model, field) 10 | current_app.elasticsearch.index(index=index, doc_type=index, id=model.id, 11 | body=payload) 12 | 13 | 14 | def remove_from_index(index, model): 15 | if not current_app.elasticsearch: 16 | return 17 | current_app.elasticsearch.delete(index=index, doc_type=index, id=model.id) 18 | 19 | 20 | def query_index(index, query, page, per_page): 21 | if not current_app.elasticsearch: 22 | return [], 0 23 | search = current_app.elasticsearch.search( 24 | index=index, doc_type=index, 25 | body={'query': {'multi_match': {'query': query, 'fields': ['*']}}, 26 | 'from': (page - 1) * per_page, 'size': per_page}) 27 | ids = [int(hit['_id']) for hit in search['hits']['hits']] 28 | return ids, search['hits']['total'] 29 | -------------------------------------------------------------------------------- /9-Ingress/4-backend.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: default-backend 6 | namespace: ingress 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: default-backend 12 | template: 13 | metadata: 14 | labels: 15 | app: default-backend 16 | spec: 17 | terminationGracePeriodSeconds: 60 18 | containers: 19 | - name: default-backend 20 | image: gcr.io/google_containers/defaultbackend:1.0 21 | livenessProbe: 22 | httpGet: 23 | path: /healthz 24 | port: 8080 25 | scheme: HTTP 26 | initialDelaySeconds: 30 27 | timeoutSeconds: 5 28 | ports: 29 | - containerPort: 8080 30 | resources: 31 | limits: 32 | cpu: 10m 33 | memory: 20Mi 34 | requests: 35 | cpu: 10m 36 | memory: 20Mi 37 | --- 38 | apiVersion: v1 39 | kind: Service 40 | metadata: 41 | name: default-backend 42 | namespace: ingress 43 | spec: 44 | ports: 45 | - port: 80 46 | protocol: TCP 47 | targetPort: 8080 48 | selector: 49 | app: default-backend -------------------------------------------------------------------------------- /14-CloudProviders-StorageClasses/2-vsphere-init.conf: -------------------------------------------------------------------------------- 1 | apiVersion: kubeadm.k8s.io/v1beta1 2 | kind: InitConfiguration 3 | bootstrapTokens: 4 | - groups: 5 | - system:bootstrappers:kubeadm:default-node-token 6 | token: cba33r.3f565pcpa8vbxdpu #generate your own token here prior or use an autogenerated token 7 | ttl: 0s 8 | usages: 9 | - signing 10 | - authentication 11 | nodeRegistration: 12 | kubeletExtraArgs: 13 | cloud-provider: "vsphere" 14 | cloud-config: "/etc/kubernetes/vsphere.conf" 15 | --- 16 | apiVersion: kubeadm.k8s.io/v1beta1 17 | kind: ClusterConfiguration 18 | kubernetesVersion: v1.16.2 19 | apiServer: 20 | extraArgs: 21 | cloud-provider: "vsphere" 22 | cloud-config: "/etc/kubernetes/vsphere.conf" 23 | extraVolumes: 24 | - name: cloud 25 | hostPath: "/etc/kubernetes/vsphere.conf" 26 | mountPath: "/etc/kubernetes/vsphere.conf" 27 | controllerManager: 28 | extraArgs: 29 | cloud-provider: "vsphere" 30 | cloud-config: "/etc/kubernetes/vsphere.conf" 31 | extraVolumes: 32 | - name: cloud 33 | hostPath: "/etc/kubernetes/vsphere.conf" 34 | mountPath: "/etc/kubernetes/vsphere.conf" 35 | networking: 36 | podSubnet: "10.244.0.0/16" -------------------------------------------------------------------------------- /3-Deployments/Deployment2.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 #version of the API to use 2 | kind: Deployment #What kind of object we're deploying 3 | metadata: #information about our object we're deploying 4 | name: nginx-deployment #Name of the deployment 5 | labels: #A tag on the deployments created 6 | app: nginx 7 | spec: #specifications for our object 8 | strategy: 9 | type: RollingUpdate 10 | rollingUpdate: #Update Pods a certain number at a time 11 | maxUnavailable: 1 #Total number of pods that can be unavailable at once 12 | maxSurge: 1 #Maximum number of pods that can be deployed above desired state 13 | replicas: 6 #The number of pods that should always be running 14 | selector: #which pods the replica set should be responsible for 15 | matchLabels: 16 | app: nginx #any pods with labels matching this I'm responsible for. 17 | template: #The pod template that gets deployed 18 | metadata: 19 | labels: #A tag on the replica sets created 20 | app: nginx 21 | spec: 22 | containers: 23 | - name: nginx-container #the name of the container within the pod 24 | image: nginx:1.7.9 #which container image should be pulled 25 | ports: 26 | - containerPort: 80 #the port of the container within the pod 27 | -------------------------------------------------------------------------------- /19-TaintsAndTolerations/1-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 #version of the API to use 2 | kind: Deployment #What kind of object we're deploying 3 | metadata: #information about our object we're deploying 4 | name: nginx-deployment #Name of the deployment 5 | labels: #A tag on the deployments created 6 | app: nginx 7 | spec: #specifications for our object 8 | strategy: 9 | type: RollingUpdate 10 | rollingUpdate: #Update Pods a certain number at a time 11 | maxUnavailable: 1 #Total number of pods that can be unavailable at once 12 | maxSurge: 1 #Maximum number of pods that can be deployed above desired state 13 | replicas: 3 #The number of pods that should always be running 14 | selector: #which pods the replica set should be responsible for 15 | matchLabels: 16 | app: nginx #any pods with labels matching this I'm responsible for. 17 | template: #The pod template that gets deployed 18 | metadata: 19 | labels: #A tag on the replica sets created 20 | app: nginx 21 | spec: 22 | containers: 23 | - name: nginx-container #the name of the container within the pod 24 | image: nginx:1.7.9 #which container image should be pulled 25 | ports: 26 | - containerPort: 80 #the port of the container within the pod -------------------------------------------------------------------------------- /19-TaintsAndTolerations/2-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 #version of the API to use 2 | kind: Deployment #What kind of object we're deploying 3 | metadata: #information about our object we're deploying 4 | name: nginx-deployment #Name of the deployment 5 | labels: #A tag on the deployments created 6 | app: nginx 7 | spec: #specifications for our object 8 | strategy: 9 | type: RollingUpdate 10 | rollingUpdate: #Update Pods a certain number at a time 11 | maxUnavailable: 1 #Total number of pods that can be unavailable at once 12 | maxSurge: 1 #Maximum number of pods that can be deployed above desired state 13 | replicas: 3 #The number of pods that should always be running 14 | selector: #which pods the replica set should be responsible for 15 | matchLabels: 16 | app: nginx #any pods with labels matching this I'm responsible for. 17 | template: #The pod template that gets deployed 18 | metadata: 19 | labels: #A tag on the replica sets created 20 | app: nginx 21 | spec: 22 | containers: 23 | - name: nginx-container #the name of the container within the pod 24 | image: nginx:1.7.9 #which container image should be pulled 25 | ports: 26 | - containerPort: 80 #the port of the container within the pod -------------------------------------------------------------------------------- /hollowapp/app/cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | import click 3 | 4 | 5 | def register(app): 6 | @app.cli.group() 7 | def translate(): 8 | """Translation and localization commands.""" 9 | pass 10 | 11 | @translate.command() 12 | @click.argument('lang') 13 | def init(lang): 14 | """Initialize a new language.""" 15 | if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): 16 | raise RuntimeError('extract command failed') 17 | if os.system( 18 | 'pybabel init -i messages.pot -d app/translations -l ' + lang): 19 | raise RuntimeError('init command failed') 20 | os.remove('messages.pot') 21 | 22 | @translate.command() 23 | def update(): 24 | """Update all languages.""" 25 | if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'): 26 | raise RuntimeError('extract command failed') 27 | if os.system('pybabel update -i messages.pot -d app/translations'): 28 | raise RuntimeError('update command failed') 29 | os.remove('messages.pot') 30 | 31 | @translate.command() 32 | def compile(): 33 | """Compile all languages.""" 34 | if os.system('pybabel compile -d app/translations'): 35 | raise RuntimeError('compile command failed') 36 | -------------------------------------------------------------------------------- /hollowapp/boot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | SQLROOTPASS="Password123" 3 | STS_REMOTESERVER="mysql-0.mysql" # container where writable SQL lives 4 | POD_REMOTESERVER="hollowdb" 5 | MYSQLPASS="Password123" 6 | DATABASE="hollowapp" 7 | USER="hollowapp" 8 | 9 | #### Create Database 10 | if [ "$DATABASE_URL" = "mysql+pymysql://hollowapp:Password123@mysql-0.mysql:3306/hollowapp"]; then 11 | mysql -h $STS_REMOTESERVER -uroot -e "CREATE DATABASE IF NOT EXISTS $DATABASE;" 12 | mysql -h $STS_REMOTESERVER -uroot -e "GRANT ALL PRIVILEGES ON $DATABASE.* TO '$USER'@'%' IDENTIFIED BY '$MYSQLPASS';" 13 | mysql -h $STS_REMOTESERVER -uroot -e "FLUSH PRIVILEGES;" 14 | else 15 | mysql -h $POD_REMOTESERVER -uroot -p$SQLROOTPASS -e "CREATE DATABASE IF NOT EXISTS $DATABASE;" 16 | mysql -h $POD_REMOTESERVER -uroot -p$SQLROOTPASS -e "GRANT ALL PRIVILEGES ON $DATABASE.* TO '$USER'@'%' IDENTIFIED BY '$MYSQLPASS';" 17 | mysql -h $POD_REMOTESERVER -uroot -p$SQLROOTPASS -e "FLUSH PRIVILEGES;" 18 | fi 19 | 20 | 21 | #### Start App 22 | source venv/bin/activate 23 | while true; do 24 | flask db upgrade 25 | if [[ "$?" == "0" ]]; then 26 | break 27 | fi 28 | echo Upgrade command failed, retrying in 5 secs... 29 | sleep 5 30 | done 31 | flask translate compile 32 | exec gunicorn -b :5000 --access-logfile - --error-logfile - taskapp:app -------------------------------------------------------------------------------- /9-Ingress/7-app.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: hollowapp 6 | name: hollowapp 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: hollowapp 12 | strategy: 13 | type: Recreate 14 | template: 15 | metadata: 16 | labels: 17 | app: hollowapp 18 | spec: 19 | containers: 20 | - name: hollowapp 21 | image: theithollow/hollowapp-blog:allin1-v2 22 | imagePullPolicy: Always 23 | ports: 24 | - containerPort: 5000 25 | env: 26 | - name: SECRET_KEY 27 | value: "my-secret-key" 28 | --- 29 | apiVersion: v1 30 | kind: Service 31 | metadata: 32 | name: hollowapp 33 | labels: 34 | app: hollowapp 35 | spec: 36 | type: ClusterIP 37 | ports: 38 | - port: 5000 39 | protocol: TCP 40 | targetPort: 5000 41 | selector: 42 | app: hollowapp 43 | --- 44 | apiVersion: networking.k8s.io/v1beta1 45 | kind: Ingress #ingress resource 46 | metadata: 47 | name: hollowapp 48 | labels: 49 | app: hollowapp 50 | spec: 51 | rules: 52 | - host: hollowapp.hollow.local #only match connections to hollowapp.hollow.local. 53 | http: 54 | paths: 55 | - path: / #root path 56 | backend: 57 | serviceName: hollowapp 58 | servicePort: 5000 59 | -------------------------------------------------------------------------------- /12-Secrets/3-HollowApp.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: hollowapp 6 | name: hollowapp 7 | spec: 8 | replicas: 3 9 | selector: 10 | matchLabels: 11 | app: hollowapp 12 | strategy: 13 | type: Recreate 14 | template: 15 | metadata: 16 | labels: 17 | app: hollowapp 18 | spec: 19 | containers: 20 | - name: hollowapp 21 | image: theithollow/hollowapp-blog:allin1 22 | imagePullPolicy: Always 23 | ports: 24 | - containerPort: 5000 25 | env: 26 | - name: SECRET_KEY 27 | value: "my-secret-key" 28 | - name: DATABASE_URL 29 | valueFrom: 30 | secretKeyRef: 31 | name: hollow-secret 32 | key: db.string 33 | --- 34 | apiVersion: v1 35 | kind: Service 36 | metadata: 37 | name: hollowapp 38 | labels: 39 | app: hollowapp 40 | spec: 41 | type: ClusterIP 42 | ports: 43 | - port: 5000 44 | protocol: TCP 45 | targetPort: 5000 46 | selector: 47 | app: hollowapp 48 | --- 49 | apiVersion: extensions/v1beta1 50 | kind: Ingress 51 | metadata: 52 | name: hollowapp 53 | labels: 54 | app: hollowapp 55 | spec: 56 | rules: 57 | - host: hollowapp.hollow.local 58 | http: 59 | paths: 60 | - path: / 61 | backend: 62 | serviceName: hollowapp 63 | servicePort: 5000 -------------------------------------------------------------------------------- /hollowapp/app/templates/_post.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 29 | 30 |
4 | 5 | 6 | 7 | 9 | {% set user_link %} 10 | 11 | {{ post.author.username }} 12 | 13 | {% endset %} 14 | {{ _('%(username)s said %(when)s', 15 | username=user_link, when=moment(post.timestamp).fromNow()) }} 16 |
17 | {{ post.body }} 18 | {% if post.language and post.language != g.locale %} 19 |

20 | 21 | {{ _('Translate') }} 26 | 27 | {% endif %} 28 |
31 | -------------------------------------------------------------------------------- /13-PersistentVolumes/5-hollowapp.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: hollowapp 6 | name: hollowapp 7 | spec: 8 | replicas: 3 9 | selector: 10 | matchLabels: 11 | app: hollowapp 12 | strategy: 13 | type: Recreate 14 | template: 15 | metadata: 16 | labels: 17 | app: hollowapp 18 | spec: 19 | containers: 20 | - name: hollowapp 21 | image: theithollow/hollowapp-blog:allin1 22 | imagePullPolicy: Always 23 | ports: 24 | - containerPort: 5000 25 | env: 26 | - name: SECRET_KEY 27 | value: "my-secret-key" 28 | - name: DATABASE_URL 29 | valueFrom: 30 | secretKeyRef: 31 | name: hollow-secret 32 | key: db.string 33 | --- 34 | apiVersion: v1 35 | kind: Service 36 | metadata: 37 | name: hollowapp 38 | labels: 39 | app: hollowapp 40 | spec: 41 | type: ClusterIP 42 | ports: 43 | - port: 5000 44 | protocol: TCP 45 | targetPort: 5000 46 | selector: 47 | app: hollowapp 48 | --- 49 | apiVersion: extensions/v1beta1 50 | kind: Ingress 51 | metadata: 52 | name: hollowapp 53 | labels: 54 | app: hollowapp 55 | spec: 56 | rules: 57 | - host: hollowapp.hollow.local 58 | http: 59 | paths: 60 | - path: / 61 | backend: 62 | serviceName: hollowapp 63 | servicePort: 5000 -------------------------------------------------------------------------------- /11-ConfigMaps/4-HollowApp.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: hollowapp 6 | name: hollowapp 7 | spec: 8 | replicas: 3 9 | selector: 10 | matchLabels: 11 | app: hollowapp 12 | strategy: 13 | type: Recreate 14 | template: 15 | metadata: 16 | labels: 17 | app: hollowapp 18 | spec: 19 | containers: 20 | - name: hollowapp 21 | image: theithollow/hollowapp-blog:allin1 22 | imagePullPolicy: Always 23 | ports: 24 | - containerPort: 5000 25 | env: 26 | - name: SECRET_KEY 27 | value: "my-secret-key" 28 | - name: DATABASE_URL 29 | valueFrom: 30 | configMapKeyRef: 31 | name: hollow-config 32 | key: db.string 33 | --- 34 | apiVersion: v1 35 | kind: Service 36 | metadata: 37 | name: hollowapp 38 | labels: 39 | app: hollowapp 40 | spec: 41 | type: ClusterIP 42 | ports: 43 | - port: 5000 44 | protocol: TCP 45 | targetPort: 5000 46 | selector: 47 | app: hollowapp 48 | --- 49 | apiVersion: networking.k8s.io/v1beta1 50 | kind: Ingress 51 | metadata: 52 | name: hollowapp 53 | labels: 54 | app: hollowapp 55 | spec: 56 | rules: 57 | - host: hollowapp.hollow.local 58 | http: 59 | paths: 60 | - path: / 61 | backend: 62 | serviceName: hollowapp 63 | servicePort: 5000 -------------------------------------------------------------------------------- /hollowapp/app/templates/_task.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 29 | 30 |
4 | 5 | 6 | 7 | 9 | {% set user_link %} 10 | 11 | {{ task.author.username }} 12 | 13 | {% endset %} 14 | {{ _('%(username)s said %(when)s', 15 | username=user_link, when=moment(task.timestamp).fromNow()) }} 16 |
17 | {{ task.name }}
{{ task.description }}
18 | {% if task.language and task.language != g.locale %} 19 |

20 | 21 | {{ _('Translate') }} 26 | 27 | {% endif %} 28 |
31 | -------------------------------------------------------------------------------- /9-Ingress/3-rbac.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: nginx 6 | namespace: ingress 7 | --- 8 | kind: ClusterRole 9 | apiVersion: rbac.authorization.k8s.io/v1beta1 10 | metadata: 11 | name: nginx-role 12 | namespace: ingress 13 | rules: 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - configmaps 18 | - endpoints 19 | - nodes 20 | - pods 21 | - secrets 22 | verbs: 23 | - list 24 | - watch 25 | - apiGroups: 26 | - "" 27 | resources: 28 | - nodes 29 | verbs: 30 | - get 31 | - apiGroups: 32 | - "" 33 | resources: 34 | - services 35 | verbs: 36 | - get 37 | - list 38 | - update 39 | - watch 40 | - apiGroups: 41 | - extensions 42 | resources: 43 | - ingresses 44 | verbs: 45 | - get 46 | - list 47 | - watch 48 | - apiGroups: 49 | - "" 50 | resources: 51 | - events 52 | verbs: 53 | - create 54 | - patch 55 | - apiGroups: 56 | - extensions 57 | resources: 58 | - ingresses/status 59 | verbs: 60 | - update 61 | --- 62 | kind: ClusterRoleBinding 63 | apiVersion: rbac.authorization.k8s.io/v1beta1 64 | metadata: 65 | name: nginx-role 66 | namespace: ingress 67 | roleRef: 68 | apiGroup: rbac.authorization.k8s.io 69 | kind: ClusterRole 70 | name: nginx-role 71 | subjects: 72 | - kind: ServiceAccount 73 | name: nginx 74 | namespace: ingress -------------------------------------------------------------------------------- /4-Services/services.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 #version of the API to use 2 | kind: Deployment #What kind of object we're deploying 3 | metadata: #information about our object we're deploying 4 | name: nginx-deployment #Name of the deployment 5 | labels: #A tag on the deployments created 6 | app: nginx 7 | spec: #specifications for our object 8 | replicas: 2 #The number of pods that should always be running 9 | selector: #which pods the replica set should be responsible for 10 | matchLabels: 11 | app: nginx #any pods with labels matching this I'm responsible for. 12 | template: #The pod template that gets deployed 13 | metadata: 14 | labels: #A tag on the replica sets created 15 | app: nginx 16 | spec: 17 | containers: 18 | - name: nginx-container #the name of the container within the pod 19 | image: nginx #which container image should be pulled 20 | ports: 21 | - containerPort: 80 #the port of the container within the pod 22 | --- 23 | apiVersion: v1 #version of the API to use 24 | kind: Service #What kind of object we're deploying 25 | metadata: #information about our object we're deploying 26 | name: ingress-nginx #Name of the service 27 | spec: #specifications for our object 28 | type: NodePort #Ignore for now discussed in a future post 29 | ports: #Ignore for now discussed in a future post 30 | - name: http 31 | port: 80 32 | targetPort: 80 33 | nodePort: 30002 34 | protocol: TCP 35 | selector: #Label selector used to identify pods 36 | app: nginx -------------------------------------------------------------------------------- /5-Endpoints/1-deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 #version of the API to use 2 | kind: Deployment #What kind of object we're deploying 3 | metadata: #information about our object we're deploying 4 | name: nginx-deployment #Name of the deployment 5 | labels: #A tag on the deployments created 6 | app: nginx 7 | spec: #specifications for our object 8 | replicas: 2 #The number of pods that should always be running 9 | selector: #which pods the replica set should be responsible for 10 | matchLabels: 11 | app: nginx #any pods with labels matching this I'm responsible for. 12 | template: #The pod template that gets deployed 13 | metadata: 14 | labels: #A tag on the replica sets created 15 | app: nginx 16 | spec: 17 | containers: 18 | - name: nginx-container #the name of the container within the pod 19 | image: nginx #which container image should be pulled 20 | ports: 21 | - containerPort: 80 #the port of the container within the pod 22 | --- 23 | apiVersion: v1 #version of the API to use 24 | kind: Service #What kind of object we're deploying 25 | metadata: #information about our object we're deploying 26 | name: ingress-nginx #Name of the service 27 | spec: #specifications for our object 28 | type: NodePort #Ignore for now discussed in a future post 29 | ports: #Ignore for now discussed in a future post 30 | - name: http 31 | port: 80 32 | targetPort: 80 33 | nodePort: 30001 34 | protocol: TCP 35 | selector: #Label selector used to identify pods 36 | app: nginx -------------------------------------------------------------------------------- /9-Ingress/5-ingress-ctrl.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: nginx-ingress-controller 6 | namespace: ingress 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: nginx-ingress-lb 12 | revisionHistoryLimit: 3 13 | template: 14 | metadata: 15 | labels: 16 | app: nginx-ingress-lb 17 | spec: 18 | terminationGracePeriodSeconds: 60 19 | serviceAccount: nginx 20 | containers: 21 | - name: nginx-ingress-controller 22 | image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.9.0 23 | imagePullPolicy: Always 24 | readinessProbe: 25 | httpGet: 26 | path: /healthz 27 | port: 10254 28 | scheme: HTTP 29 | livenessProbe: 30 | httpGet: 31 | path: /healthz 32 | port: 10254 33 | scheme: HTTP 34 | initialDelaySeconds: 10 35 | timeoutSeconds: 5 36 | args: 37 | - /nginx-ingress-controller 38 | - --default-backend-service=$(POD_NAMESPACE)/default-backend 39 | - --configmap=\$(POD_NAMESPACE)/nginx-ingress-controller-conf 40 | - --v=2 41 | env: 42 | - name: POD_NAME 43 | valueFrom: 44 | fieldRef: 45 | fieldPath: metadata.name 46 | - name: POD_NAMESPACE 47 | valueFrom: 48 | fieldRef: 49 | fieldPath: metadata.namespace 50 | ports: 51 | - containerPort: 80 52 | - containerPort: 18080 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### theITHollow Kubernetes Guide 2 | 3 | This repository includes the code samples from the [Getting Started With Kubernetes ](https://theithollow.com/2019/01/26/getting-started-with-kubernetes/) blog posts from [theITHollow](theithollow.com). 4 | 5 | Each of the folders starting with a number, correspond with one of the blog posts in the list. 6 | 7 | Also included within this repo is the application used in the examples. 8 | 9 | The blog posts in the series should provide a good jump start in to using Kubernetes and the code samples will provide useful hands on with the concepts. 10 | 11 | NOTE: In order to follow the guide, you will need to have a Kubernetes cluster installed, that has integration with a cloud provider such as vSphere, AWS, GCP, etc. The examples shown require things such as Load Balancers, and elastic storage to fully follow the guide. 12 | 13 | ![theITHollow k8s Guide](img/kubernetesguide.png) 14 | 15 | [1-Pods](1-Pods) 16 | 17 | [2-Replica Sets](2-ReplicaSets) 18 | 19 | [3-Deployments](3-Deployments) 20 | 21 | [4-Services](4-Services) 22 | 23 | [5-Endpoints](5-Endpoints) 24 | 25 | [6-Service Publishing](6-ServicePublishing) 26 | 27 | [7-Namespaces](7-Namespaces) 28 | 29 | [8-Context](8-Context) 30 | 31 | [9-Ingress](9-Ingress) 32 | 33 | [10-DNS](10-DNS) 34 | 35 | [11-Config Maps](11-ConfigMaps) 36 | 37 | [12-Secrets](12-Secrets) 38 | 39 | [13-Persistent Volumes](13-PersistentVolumes) 40 | 41 | [14-Cloud Providers - Storage Classes](14-CloudProviders-StorageClasses) 42 | 43 | [15-RBAC](15-RBAC) 44 | 45 | [18-Taints and Tolerations](18-TaintsAndTolerations) 46 | 47 | [19-Daemon Sets](19-DaemonSets) 48 | 49 | [20-Network Policies](20-NetworkPolicies) 50 | 51 | [21-Pod Security Policies](21-PodSecurityPolicies) 52 | -------------------------------------------------------------------------------- /hollowapp/app/main/forms.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | from flask_wtf import FlaskForm 3 | from wtforms import StringField, SubmitField, TextAreaField 4 | from wtforms.validators import ValidationError, DataRequired, Length 5 | from flask_babel import _, lazy_gettext as _l 6 | from app.models import User 7 | 8 | 9 | class EditProfileForm(FlaskForm): 10 | username = StringField(_l('Username'), validators=[DataRequired()]) 11 | about_me = TextAreaField(_l('About me'), 12 | validators=[Length(min=0, max=140)]) 13 | submit = SubmitField(_l('Submit')) 14 | 15 | def __init__(self, original_username, *args, **kwargs): 16 | super(EditProfileForm, self).__init__(*args, **kwargs) 17 | self.original_username = original_username 18 | 19 | def validate_username(self, username): 20 | if username.data != self.original_username: 21 | user = User.query.filter_by(username=self.username.data).first() 22 | if user is not None: 23 | raise ValidationError(_('Please use a different username.')) 24 | 25 | 26 | class PostForm(FlaskForm): 27 | post = TextAreaField(_l('Say something'), validators=[DataRequired()]) 28 | submit = SubmitField(_l('Submit')) 29 | 30 | class TaskForm(FlaskForm): 31 | name = TextAreaField(_l('Task Name'), validators=[DataRequired()]) 32 | description = TextAreaField(_l('Task Description')) 33 | submit = SubmitField(_l('Submit')) 34 | 35 | class SearchForm(FlaskForm): 36 | q = StringField(_l('Search'), validators=[DataRequired()]) 37 | 38 | def __init__(self, *args, **kwargs): 39 | if 'formdata' not in kwargs: 40 | kwargs['formdata'] = request.args 41 | if 'csrf_enabled' not in kwargs: 42 | kwargs['csrf_enabled'] = False 43 | super(SearchForm, self).__init__(*args, **kwargs) 44 | -------------------------------------------------------------------------------- /hollowapp/app/templates/user.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block app_content %} 4 | 5 | 6 | 7 | 22 | 23 |
8 |

{{ _('User') }}: {{ user.username }}

9 | {% if user.about_me %}

{{ user.about_me }}

{% endif %} 10 | {% if user.last_seen %} 11 |

{{ _('Last seen on') }}: {{ moment(user.last_seen).format('LLL') }}

12 | {% endif %} 13 |

{{ _('%(count)d followers', count=user.followers.count()) }}, {{ _('%(count)d following', count=user.followed.count()) }}

14 | {% if user == current_user %} 15 |

{{ _('Edit your profile') }}

16 | {% elif not current_user.is_following(user) %} 17 |

{{ _('Follow') }}

18 | {% else %} 19 |

{{ _('Unfollow') }}

20 | {% endif %} 21 |
24 | {% for post in posts %} 25 | {% include '_post.html' %} 26 | {% endfor %} 27 | 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /hollowapp/app/auth/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, PasswordField, BooleanField, SubmitField 3 | from wtforms.validators import ValidationError, DataRequired, Email, EqualTo 4 | from flask_babel import _, lazy_gettext as _l 5 | from app.models import User 6 | 7 | 8 | class LoginForm(FlaskForm): 9 | username = StringField(_l('Username'), validators=[DataRequired()]) 10 | password = PasswordField(_l('Password'), validators=[DataRequired()]) 11 | remember_me = BooleanField(_l('Remember Me')) 12 | submit = SubmitField(_l('Sign In')) 13 | 14 | 15 | class RegistrationForm(FlaskForm): 16 | username = StringField(_l('Username'), validators=[DataRequired()]) 17 | email = StringField(_l('Email'), validators=[DataRequired(), Email()]) 18 | password = PasswordField(_l('Password'), validators=[DataRequired()]) 19 | password2 = PasswordField( 20 | _l('Repeat Password'), validators=[DataRequired(), 21 | EqualTo('password')]) 22 | submit = SubmitField(_l('Register')) 23 | 24 | def validate_username(self, username): 25 | user = User.query.filter_by(username=username.data).first() 26 | if user is not None: 27 | raise ValidationError(_('Please use a different username.')) 28 | 29 | def validate_email(self, email): 30 | user = User.query.filter_by(email=email.data).first() 31 | if user is not None: 32 | raise ValidationError(_('Please use a different email address.')) 33 | 34 | 35 | class ResetPasswordRequestForm(FlaskForm): 36 | email = StringField(_l('Email'), validators=[DataRequired(), Email()]) 37 | submit = SubmitField(_l('Request Password Reset')) 38 | 39 | 40 | class ResetPasswordForm(FlaskForm): 41 | password = PasswordField(_l('Password'), validators=[DataRequired()]) 42 | password2 = PasswordField( 43 | _l('Repeat Password'), validators=[DataRequired(), 44 | EqualTo('password')]) 45 | submit = SubmitField(_l('Request Password Reset')) 46 | -------------------------------------------------------------------------------- /hollowapp/app/static/css/hollowstyle.css: -------------------------------------------------------------------------------- 1 | .navbar-default { 2 | background-color: #b40937; 3 | border-color: #ffffff; 4 | } 5 | 6 | .navbar-default .navbar-brand { 7 | color: #ffffff; 8 | } 9 | .navbar-default .navbar-brand:hover, 10 | .navbar-default .navbar-brand:focus { 11 | color: #000000; 12 | } 13 | .navbar-default .navbar-text { 14 | color: #ffffff; 15 | } 16 | .navbar-default .navbar-nav > li > a { 17 | color: #ffffff; 18 | } 19 | .navbar-default .navbar-nav > li > a:hover, 20 | .navbar-default .navbar-nav > li > a:focus { 21 | color: #000000; 22 | } 23 | .navbar-default .navbar-nav > .active > a, 24 | .navbar-default .navbar-nav > .active > a:hover, 25 | .navbar-default .navbar-nav > .active > a:focus { 26 | color: #000000; 27 | background-color: #ffffff; 28 | } 29 | .navbar-default .navbar-nav > .open > a, 30 | .navbar-default .navbar-nav > .open > a:hover, 31 | .navbar-default .navbar-nav > .open > a:focus { 32 | color: #000000; 33 | background-color: #ffffff; 34 | } 35 | .navbar-default .navbar-toggle { 36 | border-color: #ffffff; 37 | } 38 | .navbar-default .navbar-toggle:hover, 39 | .navbar-default .navbar-toggle:focus { 40 | background-color: #ffffff; 41 | } 42 | .navbar-default .navbar-toggle .icon-bar { 43 | background-color: #ffffff; 44 | } 45 | .navbar-default .navbar-collapse, 46 | .navbar-default .navbar-form { 47 | border-color: #ffffff; 48 | } 49 | .navbar-default .navbar-link { 50 | color: #ffffff; 51 | } 52 | .navbar-default .navbar-link:hover { 53 | color: #000000; 54 | } 55 | 56 | @media (max-width: 767px) { 57 | .navbar-default .navbar-nav .open .dropdown-menu > li > a { 58 | color: #ffffff; 59 | } 60 | .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, 61 | .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { 62 | color: #000000; 63 | } 64 | .navbar-default .navbar-nav .open .dropdown-menu > .active > a, 65 | .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, 66 | .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { 67 | color: #000000; 68 | background-color: #ffffff; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /hollowapp/migrations/versions/94ad24d5e159_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 94ad24d5e159 4 | Revises: 5 | Create Date: 2018-01-11 09:39:13.349049 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '94ad24d5e159' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('user', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('username', sa.String(length=64), nullable=True), 24 | sa.Column('email', sa.String(length=120), nullable=True), 25 | sa.Column('password_hash', sa.String(length=128), nullable=True), 26 | sa.Column('about_me', sa.String(length=140), nullable=True), 27 | sa.Column('last_seen', sa.DateTime(), nullable=True), 28 | sa.PrimaryKeyConstraint('id') 29 | ) 30 | op.create_index(op.f('ix_user_email'), 'user', ['email'], unique=True) 31 | op.create_index(op.f('ix_user_username'), 'user', ['username'], unique=True) 32 | op.create_table('followers', 33 | sa.Column('follower_id', sa.Integer(), nullable=True), 34 | sa.Column('followed_id', sa.Integer(), nullable=True), 35 | sa.ForeignKeyConstraint(['followed_id'], ['user.id'], ), 36 | sa.ForeignKeyConstraint(['follower_id'], ['user.id'], ) 37 | ) 38 | op.create_table('post', 39 | sa.Column('id', sa.Integer(), nullable=False), 40 | sa.Column('body', sa.String(length=140), nullable=True), 41 | sa.Column('timestamp', sa.DateTime(), nullable=True), 42 | sa.Column('user_id', sa.Integer(), nullable=True), 43 | sa.Column('language', sa.String(length=5), nullable=True), 44 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), 45 | sa.PrimaryKeyConstraint('id') 46 | ) 47 | op.create_index(op.f('ix_post_timestamp'), 'post', ['timestamp'], unique=False) 48 | op.create_table('task', 49 | sa.Column('id', sa.Integer(), nullable=False), 50 | sa.Column('name', sa.String(length=140), nullable=True), 51 | sa.Column('description', sa.String(length=140), nullable=True), 52 | sa.Column('timestamp', sa.DateTime(), nullable=True), 53 | sa.Column('user_id', sa.Integer(), nullable=True), 54 | sa.Column('language', sa.String(length=5), nullable=True), 55 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), 56 | sa.PrimaryKeyConstraint('id') 57 | ) 58 | op.create_index(op.f('ix_task_timestamp'), 'task', ['timestamp'], unique=False) 59 | # ### end Alembic commands ### 60 | 61 | 62 | def downgrade(): 63 | # ### commands auto generated by Alembic - please adjust! ### 64 | op.drop_index(op.f('ix_task_timestamp'), table_name='task') 65 | op.drop_table('task') 66 | op.drop_index(op.f('ix_post_timestamp'), table_name='post') 67 | op.drop_table('post') 68 | op.drop_table('followers') 69 | op.drop_index(op.f('ix_user_username'), table_name='user') 70 | op.drop_index(op.f('ix_user_email'), table_name='user') 71 | op.drop_table('user') 72 | # ### end Alembic commands ### 73 | -------------------------------------------------------------------------------- /hollowapp/app/__init__.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging.handlers import SMTPHandler, RotatingFileHandler 3 | import os 4 | from flask import Flask, request, current_app 5 | from flask_sqlalchemy import SQLAlchemy 6 | from flask_migrate import Migrate 7 | from flask_login import LoginManager 8 | from flask_mail import Mail 9 | from flask_bootstrap import Bootstrap 10 | from flask_moment import Moment 11 | from flask_babel import Babel, lazy_gettext as _l 12 | from elasticsearch import Elasticsearch 13 | from config import Config 14 | 15 | db = SQLAlchemy() 16 | migrate = Migrate() 17 | login = LoginManager() 18 | login.login_view = 'auth.login' 19 | login.login_message = _l('Please log in to access this page.') 20 | mail = Mail() 21 | bootstrap = Bootstrap() 22 | moment = Moment() 23 | babel = Babel() 24 | 25 | 26 | def create_app(config_class=Config): 27 | app = Flask(__name__) 28 | app.config.from_object(config_class) 29 | 30 | db.init_app(app) 31 | migrate.init_app(app, db) 32 | login.init_app(app) 33 | mail.init_app(app) 34 | bootstrap.init_app(app) 35 | moment.init_app(app) 36 | babel.init_app(app) 37 | app.elasticsearch = Elasticsearch([app.config['ELASTICSEARCH_URI']]) \ 38 | if app.config['ELASTICSEARCH_URI'] else None 39 | 40 | from app.errors import bp as errors_bp 41 | app.register_blueprint(errors_bp) 42 | 43 | from app.auth import bp as auth_bp 44 | app.register_blueprint(auth_bp, url_prefix='/auth') 45 | 46 | from app.main import bp as main_bp 47 | app.register_blueprint(main_bp) 48 | 49 | if not app.debug and not app.testing: 50 | if app.config['MAIL_SERVER']: 51 | auth = None 52 | if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']: 53 | auth = (app.config['MAIL_USERNAME'], 54 | app.config['MAIL_PASSWORD']) 55 | secure = None 56 | if app.config['MAIL_USE_TLS']: 57 | secure = () 58 | mail_handler = SMTPHandler( 59 | mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']), 60 | fromaddr='no-reply@' + app.config['MAIL_SERVER'], 61 | toaddrs=app.config['ADMINS'], subject='Microblog Failure', 62 | credentials=auth, secure=secure) 63 | mail_handler.setLevel(logging.ERROR) 64 | app.logger.addHandler(mail_handler) 65 | 66 | if not os.path.exists('logs'): 67 | os.mkdir('logs') 68 | file_handler = RotatingFileHandler('logs/microblog.log', 69 | maxBytes=10240, backupCount=10) 70 | file_handler.setFormatter(logging.Formatter( 71 | '%(asctime)s %(levelname)s: %(message)s ' 72 | '[in %(pathname)s:%(lineno)d]')) 73 | file_handler.setLevel(logging.INFO) 74 | app.logger.addHandler(file_handler) 75 | 76 | app.logger.setLevel(logging.INFO) 77 | app.logger.info('Microblog startup') 78 | 79 | return app 80 | 81 | 82 | @babel.localeselector 83 | def get_locale(): 84 | return request.accept_languages.best_match(current_app.config['LANGUAGES']) 85 | 86 | 87 | from app import models 88 | -------------------------------------------------------------------------------- /hollowapp/migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | import logging 6 | 7 | # this is the Alembic Config object, which provides 8 | # access to the values within the .ini file in use. 9 | config = context.config 10 | 11 | # Interpret the config file for Python logging. 12 | # This line sets up loggers basically. 13 | fileConfig(config.config_file_name) 14 | logger = logging.getLogger('alembic.env') 15 | 16 | # add your model's MetaData object here 17 | # for 'autogenerate' support 18 | # from myapp import mymodel 19 | # target_metadata = mymodel.Base.metadata 20 | from flask import current_app 21 | config.set_main_option('sqlalchemy.url', 22 | current_app.config.get('SQLALCHEMY_DATABASE_URI')) 23 | target_metadata = current_app.extensions['migrate'].db.metadata 24 | 25 | # other values from the config, defined by the needs of env.py, 26 | # can be acquired: 27 | # my_important_option = config.get_main_option("my_important_option") 28 | # ... etc. 29 | 30 | 31 | def run_migrations_offline(): 32 | """Run migrations in 'offline' mode. 33 | 34 | This configures the context with just a URL 35 | and not an Engine, though an Engine is acceptable 36 | here as well. By skipping the Engine creation 37 | we don't even need a DBAPI to be available. 38 | 39 | Calls to context.execute() here emit the given string to the 40 | script output. 41 | 42 | """ 43 | url = config.get_main_option("sqlalchemy.url") 44 | context.configure(url=url) 45 | 46 | with context.begin_transaction(): 47 | context.run_migrations() 48 | 49 | 50 | def run_migrations_online(): 51 | """Run migrations in 'online' mode. 52 | 53 | In this scenario we need to create an Engine 54 | and associate a connection with the context. 55 | 56 | """ 57 | 58 | # this callback is used to prevent an auto-migration from being generated 59 | # when there are no changes to the schema 60 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 61 | def process_revision_directives(context, revision, directives): 62 | if getattr(config.cmd_opts, 'autogenerate', False): 63 | script = directives[0] 64 | if script.upgrade_ops.is_empty(): 65 | directives[:] = [] 66 | logger.info('No changes in schema detected.') 67 | 68 | engine = engine_from_config(config.get_section(config.config_ini_section), 69 | prefix='sqlalchemy.', 70 | poolclass=pool.NullPool) 71 | 72 | connection = engine.connect() 73 | context.configure(connection=connection, 74 | target_metadata=target_metadata, 75 | process_revision_directives=process_revision_directives, 76 | **current_app.extensions['migrate'].configure_args) 77 | 78 | try: 79 | with context.begin_transaction(): 80 | context.run_migrations() 81 | finally: 82 | connection.close() 83 | 84 | if context.is_offline_mode(): 85 | run_migrations_offline() 86 | else: 87 | run_migrations_online() 88 | -------------------------------------------------------------------------------- /hollowapp/app/auth/routes.py: -------------------------------------------------------------------------------- 1 | from flask import render_template, redirect, url_for, flash, request 2 | from werkzeug.urls import url_parse 3 | from flask_login import login_user, logout_user, current_user 4 | from flask_babel import _ 5 | from app import db 6 | from app.auth import bp 7 | from app.auth.forms import LoginForm, RegistrationForm, \ 8 | ResetPasswordRequestForm, ResetPasswordForm 9 | from app.models import User 10 | from app.auth.email import send_password_reset_email 11 | 12 | 13 | @bp.route('/login', methods=['GET', 'POST']) 14 | def login(): 15 | if current_user.is_authenticated: 16 | return redirect(url_for('main.index')) 17 | form = LoginForm() 18 | if form.validate_on_submit(): 19 | user = User.query.filter_by(username=form.username.data).first() 20 | if user is None or not user.check_password(form.password.data): 21 | flash(_('Invalid username or password')) 22 | return redirect(url_for('auth.login')) 23 | login_user(user, remember=form.remember_me.data) 24 | next_page = request.args.get('next') 25 | if not next_page or url_parse(next_page).netloc != '': 26 | next_page = url_for('main.index') 27 | return redirect(next_page) 28 | return render_template('auth/login.html', title='Sign In', form=form) 29 | 30 | 31 | @bp.route('/logout') 32 | def logout(): 33 | logout_user() 34 | return redirect(url_for('main.index')) 35 | 36 | 37 | @bp.route('/register', methods=['GET', 'POST']) 38 | def register(): 39 | if current_user.is_authenticated: 40 | return redirect(url_for('main.index')) 41 | form = RegistrationForm() 42 | if form.validate_on_submit(): 43 | user = User(username=form.username.data, email=form.email.data) 44 | user.set_password(form.password.data) 45 | db.session.add(user) 46 | db.session.commit() 47 | flash(_('Congratulations, you are now a registered user!')) 48 | return redirect(url_for('auth.login')) 49 | return render_template('auth/register.html', title='Register', form=form) 50 | 51 | 52 | @bp.route('/reset_password_request', methods=['GET', 'POST']) 53 | def reset_password_request(): 54 | if current_user.is_authenticated: 55 | return redirect(url_for('main.index')) 56 | form = ResetPasswordRequestForm() 57 | if form.validate_on_submit(): 58 | user = User.query.filter_by(email=form.email.data).first() 59 | if user: 60 | send_password_reset_email(user) 61 | flash( 62 | _('Check your email for the instructions to reset your password')) 63 | return redirect(url_for('auth.login')) 64 | return render_template('auth/reset_password_request.html', 65 | title='Reset Password', form=form) 66 | 67 | 68 | @bp.route('/reset_password/', methods=['GET', 'POST']) 69 | def reset_password(token): 70 | if current_user.is_authenticated: 71 | return redirect(url_for('main.index')) 72 | user = User.verify_reset_password_token(token) 73 | if not user: 74 | return redirect(url_for('main.index')) 75 | form = ResetPasswordForm() 76 | if form.validate_on_submit(): 77 | user.set_password(form.password.data) 78 | db.session.commit() 79 | flash(_('Your password has been reset.')) 80 | return redirect(url_for('auth.login')) 81 | return render_template('auth/reset_password.html', form=form) 82 | -------------------------------------------------------------------------------- /hollowapp/tests/tests.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | import sys, unittest, xmlrunner 3 | base_directory = Path(__file__).absolute().parent.parent 4 | sys.path.insert(0, str(base_directory)) 5 | from datetime import datetime, timedelta 6 | from app import create_app, db 7 | from app.models import User, Post 8 | from config import Config 9 | 10 | class TestConfig(Config): 11 | TESTING = True 12 | SQLALCHEMY_DATABASE_URI = 'sqlite://' 13 | 14 | class UserModelCase(unittest.TestCase): 15 | def setUp(self): 16 | self.app=create_app(TestConfig) 17 | self.app_context = self.app.app_context() 18 | self.app_context.push() 19 | db.create_all() 20 | 21 | def tearDown(self): 22 | db.session.remove() 23 | db.drop_all() 24 | self.app_context.pop() 25 | 26 | def test_password_hashing(self): 27 | u = User(username='susan') 28 | u.set_password('cat') 29 | self.assertFalse(u.check_password('dog')) 30 | self.assertTrue(u.check_password('cat')) 31 | 32 | def test_follow(self): 33 | u1 = User(username='john', email='john@example.com') 34 | u2 = User(username='susan', email='susan@example.com') 35 | db.session.add(u1) 36 | db.session.add(u2) 37 | db.session.commit() 38 | self.assertEqual(u1.followed.all(), []) 39 | self.assertEqual(u1.followers.all(), []) 40 | 41 | u1.follow(u2) 42 | db.session.commit() 43 | self.assertTrue(u1.is_following(u2)) 44 | self.assertEqual(u1.followed.count(), 1) 45 | self.assertEqual(u1.followed.first().username, 'susan') 46 | self.assertEqual(u2.followers.count(), 1) 47 | self.assertEqual(u2.followers.first().username, 'john') 48 | 49 | u1.unfollow(u2) 50 | db.session.commit() 51 | self.assertFalse(u1.is_following(u2)) 52 | self.assertEqual(u1.followed.count(), 0) 53 | self.assertEqual(u2.followers.count(), 0) 54 | 55 | def test_follow_posts(self): 56 | # create four users 57 | u1 = User(username='john', email='john@example.com') 58 | u2 = User(username='susan', email='susan@example.com') 59 | u3 = User(username='mary', email='mary@example.com') 60 | u4 = User(username='david', email='david@example.com') 61 | db.session.add_all([u1, u2, u3, u4]) 62 | 63 | # create four posts 64 | now = datetime.utcnow() 65 | p1 = Post(body="post from john", author=u1, 66 | timestamp=now + timedelta(seconds=1)) 67 | p2 = Post(body="post from susan", author=u2, 68 | timestamp=now + timedelta(seconds=4)) 69 | p3 = Post(body="post from mary", author=u3, 70 | timestamp=now + timedelta(seconds=3)) 71 | p4 = Post(body="post from david", author=u4, 72 | timestamp=now + timedelta(seconds=2)) 73 | db.session.add_all([p1, p2, p3, p4]) 74 | db.session.commit() 75 | 76 | # setup the followers 77 | u1.follow(u2) # john follows susan 78 | u1.follow(u4) # john follows david 79 | u2.follow(u3) # susan follows mary 80 | u3.follow(u4) # mary follows david 81 | db.session.commit() 82 | 83 | # check the followed posts of each user 84 | f1 = u1.followed_posts().all() 85 | f2 = u2.followed_posts().all() 86 | f3 = u3.followed_posts().all() 87 | f4 = u4.followed_posts().all() 88 | self.assertEqual(f1, [p2, p4, p1]) 89 | self.assertEqual(f2, [p2, p3]) 90 | self.assertEqual(f3, [p3, p4]) 91 | self.assertEqual(f4, [p4]) 92 | 93 | if __name__ == '__main__': 94 | 95 | unittest.main( 96 | testRunner=xmlrunner.XMLTestRunner(output='test-reports'), 97 | failfast=False, 98 | buffer=False, 99 | catchbreak=False, 100 | verbosity=2) -------------------------------------------------------------------------------- /hollowapp/app/templates/base.html: -------------------------------------------------------------------------------- 1 | {% block doc -%} 2 | 3 | 4 | {%- block html %} 5 | 6 | {%- block head %} 7 | 8 | {% block title %} 9 | {% if title %} 10 | {{ title }} - HollowApp 11 | {% else %} 12 | {{ _('Welcome to HollowApp') }} 13 | {% endif %} 14 | {% endblock %} 15 | 16 | 17 | {%- block metas %} 18 | 19 | {%- endblock metas %} 20 | 21 | {%- block styles %} 22 | 23 | 24 | {%- endblock styles %} 25 | {%- endblock head %} 26 | 27 | 28 | {% block body -%} 29 | {% block navbar %} 30 | 64 | {% endblock %} 65 | {% block content %} 66 |
67 | {% with messages = get_flashed_messages() %} 68 | {% if messages %} 69 | {% for message in messages %} 70 | 71 | {% endfor %} 72 | {% endif %} 73 | {% endwith %} 74 | 75 | {# application content needs to be provided in the app_content block #} 76 | {% block app_content %}{% endblock %} 77 |
78 | {% endblock content%} 79 | 80 | {% block scripts %} 81 | 82 | 83 | {%- endblock scripts %} 84 | {%- endblock body %} 85 | 86 | {%- endblock html %} 87 | 88 | {% endblock doc -%} 89 | -------------------------------------------------------------------------------- /hollowapp/app/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from hashlib import md5 3 | from time import time 4 | from flask import current_app 5 | from flask_login import UserMixin 6 | from werkzeug.security import generate_password_hash, check_password_hash 7 | import jwt 8 | from app import db, login 9 | from app.search import add_to_index, remove_from_index, query_index 10 | 11 | 12 | class SearchableMixin(object): 13 | @classmethod 14 | def search(cls, expression, page, per_page): 15 | ids, total = query_index(cls.__tablename__, expression, page, per_page) 16 | if total == 0: 17 | return cls.query.filter_by(id=0), 0 18 | when = [] 19 | for i in range(len(ids)): 20 | when.append((ids[i], i)) 21 | return cls.query.filter(cls.id.in_(ids)).order_by( 22 | db.case(when, value=Post.id)), total 23 | 24 | @classmethod 25 | def before_commit(cls, session): 26 | session._changes = { 27 | 'add': [obj for obj in session.new if isinstance(obj, cls)], 28 | 'update': [obj for obj in session.dirty if isinstance(obj, cls)], 29 | 'delete': [obj for obj in session.deleted if isinstance(obj, cls)] 30 | } 31 | 32 | @classmethod 33 | def after_commit(cls, session): 34 | for obj in session._changes['add']: 35 | add_to_index(cls.__tablename__, obj) 36 | for obj in session._changes['update']: 37 | add_to_index(cls.__tablename__, obj) 38 | for obj in session._changes['delete']: 39 | remove_from_index(cls.__tablename__, obj) 40 | session._changes = None 41 | 42 | @classmethod 43 | def reindex(cls): 44 | for obj in cls.query: 45 | add_to_index(cls.__tablename__, obj) 46 | 47 | 48 | followers = db.Table( 49 | 'followers', 50 | db.Column('follower_id', db.Integer, db.ForeignKey('user.id')), 51 | db.Column('followed_id', db.Integer, db.ForeignKey('user.id')) 52 | ) 53 | 54 | 55 | class User(UserMixin, db.Model): 56 | id = db.Column(db.Integer, primary_key=True) 57 | username = db.Column(db.String(64), index=True, unique=True) 58 | email = db.Column(db.String(120), index=True, unique=True) 59 | password_hash = db.Column(db.String(128)) 60 | posts = db.relationship('Post', backref='author', lazy='dynamic') 61 | tasks = db.relationship('Task', backref='author', lazy='dynamic') 62 | about_me = db.Column(db.String(140)) 63 | last_seen = db.Column(db.DateTime) 64 | followed = db.relationship( 65 | 'User', secondary=followers, 66 | primaryjoin=(followers.c.follower_id == id), 67 | secondaryjoin=(followers.c.followed_id == id), 68 | backref=db.backref('followers', lazy='dynamic'), lazy='dynamic') 69 | 70 | def __repr__(self): 71 | return ''.format(self.username) 72 | 73 | def set_password(self, password): 74 | self.password_hash = generate_password_hash(password) 75 | 76 | def check_password(self, password): 77 | return check_password_hash(self.password_hash, password) 78 | 79 | def avatar(self, size): 80 | digest = md5(self.email.lower().encode('utf-8')).hexdigest() 81 | return 'https://www.gravatar.com/avatar/{}?d=identicon&s={}'.format( 82 | digest, size) 83 | 84 | def follow(self, user): 85 | if not self.is_following(user): 86 | self.followed.append(user) 87 | 88 | def unfollow(self, user): 89 | if self.is_following(user): 90 | self.followed.remove(user) 91 | 92 | def is_following(self, user): 93 | return self.followed.filter( 94 | followers.c.followed_id == user.id).count() > 0 95 | 96 | def followed_posts(self): 97 | followed = Post.query.join( 98 | followers, (followers.c.followed_id == Post.user_id)).filter( 99 | followers.c.follower_id == self.id) 100 | own = Post.query.filter_by(user_id=self.id) 101 | return followed.union(own).order_by(Post.timestamp.desc()) 102 | 103 | def owned_tasks(self): 104 | own = Task.query.filter_by(user_id=self.id) 105 | return (own).order_by(Task.timestamp.desc()) 106 | 107 | def get_reset_password_token(self, expires_in=600): 108 | return jwt.encode( 109 | {'reset_password': self.id, 'exp': time() + expires_in}, 110 | current_app.config['SECRET_KEY'], 111 | algorithm='HS256').decode('utf-8') 112 | 113 | @staticmethod 114 | def verify_reset_password_token(token): 115 | try: 116 | id = jwt.decode(token, current_app.config['SECRET_KEY'], 117 | algorithms=['HS256'])['reset_password'] 118 | except: 119 | return 120 | return User.query.get(id) 121 | 122 | 123 | @login.user_loader 124 | def load_user(id): 125 | return User.query.get(int(id)) 126 | 127 | 128 | class Post(SearchableMixin, db.Model): 129 | __searchable__ = ['body'] 130 | id = db.Column(db.Integer, primary_key=True) 131 | body = db.Column(db.String(140)) 132 | timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) 133 | user_id = db.Column(db.Integer, db.ForeignKey('user.id')) 134 | language = db.Column(db.String(5)) 135 | 136 | def __repr__(self): 137 | return ''.format(self.body) 138 | 139 | 140 | db.event.listen(db.session, 'before_commit', Post.before_commit) 141 | db.event.listen(db.session, 'after_commit', Post.after_commit) 142 | 143 | class Task(SearchableMixin, db.Model): 144 | __searchable__ = ['task'] 145 | id = db.Column(db.Integer, primary_key=True) 146 | name = db.Column(db.String(140)) 147 | description = db.Column(db.String(140)) 148 | timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) 149 | user_id = db.Column(db.Integer, db.ForeignKey('user.id')) 150 | language = db.Column(db.String(5)) 151 | 152 | def __repr__(self): 153 | return ''.format(self.task) 154 | -------------------------------------------------------------------------------- /15-Statefulsets/3-statefulset.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: mysql 5 | spec: 6 | selector: 7 | matchLabels: 8 | app: mysql 9 | serviceName: mysql 10 | replicas: 2 11 | template: 12 | metadata: 13 | labels: 14 | app: mysql 15 | spec: 16 | initContainers: 17 | - name: init-mysql 18 | image: mysql:5.7 19 | command: 20 | - bash 21 | - "-c" 22 | - | 23 | set -ex 24 | # Generate mysql server-id from pod ordinal index. 25 | [[ `hostname` =~ -([0-9]+)$ ]] || exit 1 26 | ordinal=${BASH_REMATCH[1]} 27 | echo [mysqld] > /mnt/conf.d/server-id.cnf 28 | # Add an offset to avoid reserved server-id=0 value. 29 | echo server-id=$((100 + $ordinal)) >> /mnt/conf.d/server-id.cnf 30 | # Copy appropriate conf.d files from config-map to emptyDir. 31 | if [[ $ordinal -eq 0 ]]; then 32 | cp /mnt/config-map/master.cnf /mnt/conf.d/ 33 | else 34 | cp /mnt/config-map/slave.cnf /mnt/conf.d/ 35 | fi 36 | volumeMounts: 37 | - name: conf 38 | mountPath: /mnt/conf.d 39 | - name: config-map 40 | mountPath: /mnt/config-map 41 | - name: clone-mysql 42 | image: gcr.io/google-samples/xtrabackup:1.0 43 | command: 44 | - bash 45 | - "-c" 46 | - | 47 | set -ex 48 | # Skip the clone if data already exists. 49 | [[ -d /var/lib/mysql/mysql ]] && exit 0 50 | # Skip the clone on master (ordinal index 0). 51 | [[ `hostname` =~ -([0-9]+)$ ]] || exit 1 52 | ordinal=${BASH_REMATCH[1]} 53 | [[ $ordinal -eq 0 ]] && exit 0 54 | # Clone data from previous peer. 55 | ncat --recv-only mysql-$(($ordinal-1)).mysql 3307 | xbstream -x -C /var/lib/mysql 56 | # Prepare the backup. 57 | xtrabackup --prepare --target-dir=/var/lib/mysql 58 | volumeMounts: 59 | - name: data 60 | mountPath: /var/lib/mysql 61 | subPath: mysql 62 | - name: conf 63 | mountPath: /etc/mysql/conf.d 64 | containers: 65 | - name: mysql 66 | image: mysql:5.7 67 | env: 68 | - name: MYSQL_ALLOW_EMPTY_PASSWORD 69 | value: "1" 70 | ports: 71 | - name: mysql 72 | containerPort: 3306 73 | volumeMounts: 74 | - name: data 75 | mountPath: /var/lib/mysql 76 | subPath: mysql 77 | - name: conf 78 | mountPath: /etc/mysql/conf.d 79 | resources: 80 | requests: 81 | cpu: 500m 82 | memory: 1Gi 83 | livenessProbe: 84 | exec: 85 | command: ["mysqladmin", "ping"] 86 | initialDelaySeconds: 30 87 | periodSeconds: 10 88 | timeoutSeconds: 5 89 | readinessProbe: 90 | exec: 91 | # Check we can execute queries over TCP (skip-networking is off). 92 | command: ["mysql", "-h", "127.0.0.1", "-e", "SELECT 1"] 93 | initialDelaySeconds: 5 94 | periodSeconds: 2 95 | timeoutSeconds: 1 96 | - name: xtrabackup 97 | image: gcr.io/google-samples/xtrabackup:1.0 98 | ports: 99 | - name: xtrabackup 100 | containerPort: 3307 101 | command: 102 | - bash 103 | - "-c" 104 | - | 105 | set -ex 106 | cd /var/lib/mysql 107 | 108 | # Determine binlog position of cloned data, if any. 109 | if [[ -f xtrabackup_slave_info ]]; then 110 | # XtraBackup already generated a partial "CHANGE MASTER TO" query 111 | # because we're cloning from an existing slave. 112 | mv xtrabackup_slave_info change_master_to.sql.in 113 | # Ignore xtrabackup_binlog_info in this case (it's useless). 114 | rm -f xtrabackup_binlog_info 115 | elif [[ -f xtrabackup_binlog_info ]]; then 116 | # We're cloning directly from master. Parse binlog position. 117 | [[ `cat xtrabackup_binlog_info` =~ ^(.*?)[[:space:]]+(.*?)$ ]] || exit 1 118 | rm xtrabackup_binlog_info 119 | echo "CHANGE MASTER TO MASTER_LOG_FILE='${BASH_REMATCH[1]}',\ 120 | MASTER_LOG_POS=${BASH_REMATCH[2]}" > change_master_to.sql.in 121 | fi 122 | 123 | # Check if we need to complete a clone by starting replication. 124 | if [[ -f change_master_to.sql.in ]]; then 125 | echo "Waiting for mysqld to be ready (accepting connections)" 126 | until mysql -h 127.0.0.1 -e "SELECT 1"; do sleep 1; done 127 | 128 | echo "Initializing replication from clone position" 129 | # In case of container restart, attempt this at-most-once. 130 | mv change_master_to.sql.in change_master_to.sql.orig 131 | mysql -h 127.0.0.1 <<EOF 132 | $(<change_master_to.sql.orig), 133 | MASTER_HOST='mysql-0.mysql', 134 | MASTER_USER='root', 135 | MASTER_PASSWORD='', 136 | MASTER_CONNECT_RETRY=10; 137 | START SLAVE; 138 | EOF 139 | fi 140 | 141 | # Start a server to send backups when requested by peers. 142 | exec ncat --listen --keep-open --send-only --max-conns=1 3307 -c \ 143 | "xtrabackup --backup --slave-info --stream=xbstream --host=127.0.0.1 --user=root" 144 | volumeMounts: 145 | - name: data 146 | mountPath: /var/lib/mysql 147 | subPath: mysql 148 | - name: conf 149 | mountPath: /etc/mysql/conf.d 150 | resources: 151 | requests: 152 | cpu: 100m 153 | memory: 100Mi 154 | volumes: 155 | - name: conf 156 | emptyDir: {} 157 | - name: config-map 158 | configMap: 159 | name: mysql 160 | volumeClaimTemplates: 161 | - metadata: 162 | name: data 163 | spec: 164 | accessModes: ["ReadWriteOnce"] 165 | resources: 166 | requests: 167 | storage: 10Gi -------------------------------------------------------------------------------- /hollowapp/app/main/routes.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from flask import render_template, flash, redirect, url_for, request, g, \ 3 | jsonify, current_app 4 | from flask_login import current_user, login_required 5 | from flask_babel import _, get_locale 6 | from guess_language import guess_language 7 | from app import db 8 | from app.main.forms import EditProfileForm, PostForm, SearchForm, TaskForm 9 | from app.models import User, Post, Task 10 | from app.translate import translate 11 | from app.main import bp 12 | 13 | 14 | @bp.before_app_request 15 | def before_request(): 16 | if current_user.is_authenticated: 17 | current_user.last_seen = datetime.utcnow() 18 | db.session.commit() 19 | g.search_form = SearchForm() 20 | g.locale = str(get_locale()) 21 | 22 | 23 | @bp.route('/', methods=['GET', 'POST']) 24 | @bp.route('/index', methods=['GET', 'POST']) 25 | @login_required 26 | def index(): 27 | form = PostForm() 28 | if form.validate_on_submit(): 29 | language = guess_language(form.post.data) 30 | if language == 'UNKNOWN' or len(language) > 5: 31 | language = '' 32 | post = Post(body=form.post.data, author=current_user, 33 | language=language) 34 | db.session.add(post) 35 | db.session.commit() 36 | flash(_('Your post is now live!')) 37 | return redirect(url_for('main.index')) 38 | page = request.args.get('page', 1, type=int) 39 | posts = current_user.followed_posts().paginate( 40 | page, current_app.config['POSTS_PER_PAGE'], False) 41 | next_url = url_for('main.explore', page=posts.next_num) \ 42 | if posts.has_next else None 43 | prev_url = url_for('main.explore', page=posts.prev_num) \ 44 | if posts.has_prev else None 45 | return render_template('index.html', title='Home', form=form, 46 | posts=posts.items, next_url=next_url, 47 | prev_url=prev_url) 48 | 49 | 50 | @bp.route('/user/') 51 | @login_required 52 | def user(username): 53 | user = User.query.filter_by(username=username).first_or_404() 54 | page = request.args.get('page', 1, type=int) 55 | posts = user.posts.order_by(Post.timestamp.desc()).paginate( 56 | page, current_app.config['POSTS_PER_PAGE'], False) 57 | next_url = url_for('main.user', username=user.username, 58 | page=posts.next_num) if posts.has_next else None 59 | prev_url = url_for('main.user', username=user.username, 60 | page=posts.prev_num) if posts.has_prev else None 61 | return render_template('user.html', user=user, posts=posts.items, 62 | next_url=next_url, prev_url=prev_url) 63 | 64 | 65 | @bp.route('/edit_profile', methods=['GET', 'POST']) 66 | @login_required 67 | def edit_profile(): 68 | form = EditProfileForm(current_user.username) 69 | if form.validate_on_submit(): 70 | current_user.username = form.username.data 71 | current_user.about_me = form.about_me.data 72 | db.session.commit() 73 | flash(_('Your changes have been saved.')) 74 | return redirect(url_for('main.edit_profile')) 75 | elif request.method == 'GET': 76 | form.username.data = current_user.username 77 | form.about_me.data = current_user.about_me 78 | return render_template('edit_profile.html', title=_('Edit Profile'), 79 | form=form) 80 | 81 | 82 | @bp.route('/follow/') 83 | @login_required 84 | def follow(username): 85 | user = User.query.filter_by(username=username).first() 86 | if user is None: 87 | flash(_('User %(username)s not found.', username=username)) 88 | return redirect(url_for('main.index')) 89 | if user == current_user: 90 | flash(_('You cannot follow yourself!')) 91 | return redirect(url_for('main.user', username=username)) 92 | current_user.follow(user) 93 | db.session.commit() 94 | flash(_('You are following %(username)s!', username=username)) 95 | return redirect(url_for('main.user', username=username)) 96 | 97 | 98 | @bp.route('/unfollow/') 99 | @login_required 100 | def unfollow(username): 101 | user = User.query.filter_by(username=username).first() 102 | if user is None: 103 | flash(_('User %(username)s not found.', username=username)) 104 | return redirect(url_for('main.index')) 105 | if user == current_user: 106 | flash(_('You cannot unfollow yourself!')) 107 | return redirect(url_for('main.user', username=username)) 108 | current_user.unfollow(user) 109 | db.session.commit() 110 | flash(_('You are not following %(username)s.', username=username)) 111 | return redirect(url_for('main.user', username=username)) 112 | 113 | 114 | @bp.route('/translate', methods=['POST']) 115 | @login_required 116 | def translate_text(): 117 | return jsonify({'text': translate(request.form['text'], 118 | request.form['source_language'], 119 | request.form['dest_language'])}) 120 | 121 | @bp.route('/tasks', methods=['GET', 'POST']) 122 | @login_required 123 | def task(): 124 | form = TaskForm() 125 | if form.validate_on_submit(): 126 | language = guess_language(form.name.data) 127 | if language == 'UNKNOWN' or len(language) > 5: 128 | language = '' 129 | task = Task(name=form.name.data, description=form.description.data, author=current_user, 130 | language=language) 131 | db.session.add(task) 132 | db.session.commit() 133 | flash(_('Your task has been added!')) 134 | return redirect(url_for('main.task')) 135 | 136 | page = request.args.get('page', 1, type=int) 137 | tasks = current_user.owned_tasks().paginate( 138 | page, current_app.config['POSTS_PER_PAGE'], False) 139 | next_url = url_for('main.explore', page=tasks.next_num) \ 140 | if tasks.has_next else None 141 | prev_url = url_for('main.explore', page=tasks.prev_num) \ 142 | if tasks.has_prev else None 143 | return render_template('tasks.html', title='Tasks', form=form, 144 | tasks=tasks.items, next_url=next_url, 145 | prev_url=prev_url) 146 | 147 | 148 | @bp.route('/search') 149 | @login_required 150 | def search(): 151 | if not g.search_form.validate(): 152 | return redirect(url_for('main.explore')) 153 | page = request.args.get('page', 1, type=int) 154 | posts, total = Post.search(g.search_form.q.data, page, 155 | current_app.config['POSTS_PER_PAGE']) 156 | next_url = url_for('main.search', q=g.search_form.q.data, page=page + 1) \ 157 | if total > page * current_app.config['POSTS_PER_PAGE'] else None 158 | prev_url = url_for('main.search', q=g.search_form.q.data, page=page - 1) \ 159 | if page > 1 else None 160 | return render_template('search.html', title=_('Search'), posts=posts, 161 | next_url=next_url, prev_url=prev_url) 162 | -------------------------------------------------------------------------------- /hollowapp/app/translations/es/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # Spanish translations for PROJECT. 2 | # Copyright (C) 2017 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2017. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2017-11-25 18:23-0800\n" 11 | "PO-Revision-Date: 2017-09-29 23:25-0700\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: es\n" 14 | "Language-Team: es \n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.5.1\n" 20 | 21 | #: app/__init__.py:18 22 | msgid "Please log in to access this page." 23 | msgstr "Por favor ingrese para acceder a esta página." 24 | 25 | #: app/translate.py:10 26 | msgid "Error: the translation service is not configured." 27 | msgstr "Error: el servicio de traducciones no está configurado." 28 | 29 | #: app/translate.py:18 30 | msgid "Error: the translation service failed." 31 | msgstr "Error el servicio de traducciones ha fallado." 32 | 33 | #: app/auth/email.py:8 34 | msgid "[Microblog] Reset Your Password" 35 | msgstr "[Microblog] Nueva Contraseña" 36 | 37 | #: app/auth/forms.py:10 app/auth/forms.py:17 app/main/forms.py:10 38 | msgid "Username" 39 | msgstr "Nombre de usuario" 40 | 41 | #: app/auth/forms.py:11 app/auth/forms.py:19 app/auth/forms.py:42 42 | msgid "Password" 43 | msgstr "Contraseña" 44 | 45 | #: app/auth/forms.py:12 46 | msgid "Remember Me" 47 | msgstr "Recordarme" 48 | 49 | #: app/auth/forms.py:13 app/templates/auth/login.html:5 50 | msgid "Sign In" 51 | msgstr "Ingresar" 52 | 53 | #: app/auth/forms.py:18 app/auth/forms.py:37 54 | msgid "Email" 55 | msgstr "Email" 56 | 57 | #: app/auth/forms.py:21 app/auth/forms.py:44 58 | msgid "Repeat Password" 59 | msgstr "Repetir Contraseña" 60 | 61 | #: app/auth/forms.py:23 app/templates/auth/register.html:5 62 | msgid "Register" 63 | msgstr "Registrarse" 64 | 65 | #: app/auth/forms.py:28 app/main/forms.py:23 66 | msgid "Please use a different username." 67 | msgstr "Por favor use un nombre de usuario diferente." 68 | 69 | #: app/auth/forms.py:33 70 | msgid "Please use a different email address." 71 | msgstr "Por favor use una dirección de email diferente." 72 | 73 | #: app/auth/forms.py:38 app/auth/forms.py:46 74 | msgid "Request Password Reset" 75 | msgstr "Pedir una nueva contraseña" 76 | 77 | #: app/auth/routes.py:20 78 | msgid "Invalid username or password" 79 | msgstr "Nombre de usuario o contraseña inválidos" 80 | 81 | #: app/auth/routes.py:46 82 | msgid "Congratulations, you are now a registered user!" 83 | msgstr "¡Felicitaciones, ya eres un usuario registrado!" 84 | 85 | #: app/auth/routes.py:61 86 | msgid "Check your email for the instructions to reset your password" 87 | msgstr "Busca en tu email las instrucciones para crear una nueva contraseña" 88 | 89 | #: app/auth/routes.py:78 90 | msgid "Your password has been reset." 91 | msgstr "Tu contraseña ha sido cambiada." 92 | 93 | #: app/main/forms.py:11 94 | msgid "About me" 95 | msgstr "Acerca de mí" 96 | 97 | #: app/main/forms.py:13 app/main/forms.py:28 98 | msgid "Submit" 99 | msgstr "Enviar" 100 | 101 | #: app/main/forms.py:27 102 | msgid "Say something" 103 | msgstr "Dí algo" 104 | 105 | #: app/main/forms.py:32 106 | msgid "Search" 107 | msgstr "Buscar" 108 | 109 | #: app/main/routes.py:36 110 | msgid "Your post is now live!" 111 | msgstr "¡Tu artículo ha sido publicado!" 112 | 113 | #: app/main/routes.py:87 114 | msgid "Your changes have been saved." 115 | msgstr "Tus cambios han sido salvados." 116 | 117 | #: app/main/routes.py:92 app/templates/edit_profile.html:5 118 | msgid "Edit Profile" 119 | msgstr "Editar Perfil" 120 | 121 | #: app/main/routes.py:101 app/main/routes.py:117 122 | #, python-format 123 | msgid "User %(username)s not found." 124 | msgstr "El usuario %(username)s no ha sido encontrado." 125 | 126 | #: app/main/routes.py:104 127 | msgid "You cannot follow yourself!" 128 | msgstr "¡No te puedes seguir a tí mismo!" 129 | 130 | #: app/main/routes.py:108 131 | #, python-format 132 | msgid "You are following %(username)s!" 133 | msgstr "¡Ahora estás siguiendo a %(username)s!" 134 | 135 | #: app/main/routes.py:120 136 | msgid "You cannot unfollow yourself!" 137 | msgstr "¡No te puedes dejar de seguir a tí mismo!" 138 | 139 | #: app/main/routes.py:124 140 | #, python-format 141 | msgid "You are not following %(username)s." 142 | msgstr "No estás siguiendo a %(username)s." 143 | 144 | #: app/templates/_post.html:14 145 | #, python-format 146 | msgid "%(username)s said %(when)s" 147 | msgstr "%(username)s dijo %(when)s" 148 | 149 | #: app/templates/_post.html:25 150 | msgid "Translate" 151 | msgstr "Traducir" 152 | 153 | #: app/templates/base.html:4 154 | msgid "Welcome to Microblog" 155 | msgstr "Bienvenido a Microblog" 156 | 157 | #: app/templates/base.html:21 158 | msgid "Home" 159 | msgstr "Inicio" 160 | 161 | #: app/templates/base.html:22 162 | msgid "Explore" 163 | msgstr "Explorar" 164 | 165 | #: app/templates/base.html:33 166 | msgid "Login" 167 | msgstr "Ingresar" 168 | 169 | #: app/templates/base.html:35 170 | msgid "Profile" 171 | msgstr "Perfil" 172 | 173 | #: app/templates/base.html:36 174 | msgid "Logout" 175 | msgstr "Salir" 176 | 177 | #: app/templates/base.html:73 178 | msgid "Error: Could not contact server." 179 | msgstr "Error: el servidor no pudo ser contactado." 180 | 181 | #: app/templates/index.html:5 182 | #, python-format 183 | msgid "Hi, %(username)s!" 184 | msgstr "¡Hola, %(username)s!" 185 | 186 | #: app/templates/index.html:17 app/templates/user.html:31 187 | msgid "Newer posts" 188 | msgstr "Artículos siguientes" 189 | 190 | #: app/templates/index.html:22 app/templates/user.html:36 191 | msgid "Older posts" 192 | msgstr "Artículos previos" 193 | 194 | #: app/templates/search.html:4 195 | msgid "Search Results" 196 | msgstr "Resultados de Búsqueda" 197 | 198 | #: app/templates/search.html:12 199 | msgid "Previous results" 200 | msgstr "Resultados previos" 201 | 202 | #: app/templates/search.html:17 203 | msgid "Next results" 204 | msgstr "Resultados próximos" 205 | 206 | #: app/templates/user.html:8 207 | msgid "User" 208 | msgstr "Usuario" 209 | 210 | #: app/templates/user.html:11 211 | msgid "Last seen on" 212 | msgstr "Última visita" 213 | 214 | #: app/templates/user.html:13 215 | #, python-format 216 | msgid "%(count)d followers" 217 | msgstr "%(count)d seguidores" 218 | 219 | #: app/templates/user.html:13 220 | #, python-format 221 | msgid "%(count)d following" 222 | msgstr "siguiendo a %(count)d" 223 | 224 | #: app/templates/user.html:15 225 | msgid "Edit your profile" 226 | msgstr "Editar tu perfil" 227 | 228 | #: app/templates/user.html:17 229 | msgid "Follow" 230 | msgstr "Seguir" 231 | 232 | #: app/templates/user.html:19 233 | msgid "Unfollow" 234 | msgstr "Dejar de seguir" 235 | 236 | #: app/templates/auth/login.html:12 237 | msgid "New User?" 238 | msgstr "¿Usuario Nuevo?" 239 | 240 | #: app/templates/auth/login.html:12 241 | msgid "Click to Register!" 242 | msgstr "¡Haz click aquí para registrarte!" 243 | 244 | #: app/templates/auth/login.html:14 245 | msgid "Forgot Your Password?" 246 | msgstr "¿Te olvidaste tu contraseña?" 247 | 248 | #: app/templates/auth/login.html:15 249 | msgid "Click to Reset It" 250 | msgstr "Haz click aquí para pedir una nueva" 251 | 252 | #: app/templates/auth/reset_password.html:5 253 | msgid "Reset Your Password" 254 | msgstr "Nueva Contraseña" 255 | 256 | #: app/templates/auth/reset_password_request.html:5 257 | msgid "Reset Password" 258 | msgstr "Nueva Contraseña" 259 | 260 | #: app/templates/errors/404.html:4 261 | msgid "Not Found" 262 | msgstr "Página No Encontrada" 263 | 264 | #: app/templates/errors/404.html:5 app/templates/errors/500.html:6 265 | msgid "Back" 266 | msgstr "Atrás" 267 | 268 | #: app/templates/errors/500.html:4 269 | msgid "An unexpected error has occurred" 270 | msgstr "Ha ocurrido un error inesperado" 271 | 272 | #: app/templates/errors/500.html:5 273 | msgid "The administrator has been notified. Sorry for the inconvenience!" 274 | msgstr "El administrador ha sido notificado. ¡Lamentamos la inconveniencia!" 275 | 276 | -------------------------------------------------------------------------------- /hollowapp/app/static/css/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.2.2 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */@-ms-viewport{width:device-width}.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .dropdown-menu a:hover{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:hover{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} 10 | -------------------------------------------------------------------------------- /hollowapp/app/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap.js by @fat & @mdo 3 | * Copyright 2012 Twitter, Inc. 4 | * http://www.apache.org/licenses/LICENSE-2.0.txt 5 | */ 6 | !function($){"use strict";$(function(){$.support.transition=function(){var transitionEnd=function(){var name,el=document.createElement("bootstrap"),transEndEventNames={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(name in transEndEventNames)if(void 0!==el.style[name])return transEndEventNames[name]}();return transitionEnd&&{end:transitionEnd}}()})}(window.jQuery),!function($){"use strict";var dismiss='[data-dismiss="alert"]',Alert=function(el){$(el).on("click",dismiss,this.close)};Alert.prototype.close=function(e){function removeElement(){$parent.trigger("closed").remove()}var $parent,$this=$(this),selector=$this.attr("data-target");selector||(selector=$this.attr("href"),selector=selector&&selector.replace(/.*(?=#[^\s]*$)/,"")),$parent=$(selector),e&&e.preventDefault(),$parent.length||($parent=$this.hasClass("alert")?$this:$this.parent()),$parent.trigger(e=$.Event("close")),e.isDefaultPrevented()||($parent.removeClass("in"),$.support.transition&&$parent.hasClass("fade")?$parent.on($.support.transition.end,removeElement):removeElement())};var old=$.fn.alert;$.fn.alert=function(option){return this.each(function(){var $this=$(this),data=$this.data("alert");data||$this.data("alert",data=new Alert(this)),"string"==typeof option&&data[option].call($this)})},$.fn.alert.Constructor=Alert,$.fn.alert.noConflict=function(){return $.fn.alert=old,this},$(document).on("click.alert.data-api",dismiss,Alert.prototype.close)}(window.jQuery),!function($){"use strict";var Button=function(element,options){this.$element=$(element),this.options=$.extend({},$.fn.button.defaults,options)};Button.prototype.setState=function(state){var d="disabled",$el=this.$element,data=$el.data(),val=$el.is("input")?"val":"html";state+="Text",data.resetText||$el.data("resetText",$el[val]()),$el[val](data[state]||this.options[state]),setTimeout(function(){"loadingText"==state?$el.addClass(d).attr(d,d):$el.removeClass(d).removeAttr(d)},0)},Button.prototype.toggle=function(){var $parent=this.$element.closest('[data-toggle="buttons-radio"]');$parent&&$parent.find(".active").removeClass("active"),this.$element.toggleClass("active")};var old=$.fn.button;$.fn.button=function(option){return this.each(function(){var $this=$(this),data=$this.data("button"),options="object"==typeof option&&option;data||$this.data("button",data=new Button(this,options)),"toggle"==option?data.toggle():option&&data.setState(option)})},$.fn.button.defaults={loadingText:"loading..."},$.fn.button.Constructor=Button,$.fn.button.noConflict=function(){return $.fn.button=old,this},$(document).on("click.button.data-api","[data-toggle^=button]",function(e){var $btn=$(e.target);$btn.hasClass("btn")||($btn=$btn.closest(".btn")),$btn.button("toggle")})}(window.jQuery),!function($){"use strict";var Carousel=function(element,options){this.$element=$(element),this.options=options,"hover"==this.options.pause&&this.$element.on("mouseenter",$.proxy(this.pause,this)).on("mouseleave",$.proxy(this.cycle,this))};Carousel.prototype={cycle:function(e){return e||(this.paused=!1),this.options.interval&&!this.paused&&(this.interval=setInterval($.proxy(this.next,this),this.options.interval)),this},to:function(pos){var $active=this.$element.find(".item.active"),children=$active.parent().children(),activePos=children.index($active),that=this;if(!(pos>children.length-1||0>pos))return this.sliding?this.$element.one("slid",function(){that.to(pos)}):activePos==pos?this.pause().cycle():this.slide(pos>activePos?"next":"prev",$(children[pos]))},pause:function(e){return e||(this.paused=!0),this.$element.find(".next, .prev").length&&$.support.transition.end&&(this.$element.trigger($.support.transition.end),this.cycle()),clearInterval(this.interval),this.interval=null,this},next:function(){return this.sliding?void 0:this.slide("next")},prev:function(){return this.sliding?void 0:this.slide("prev")},slide:function(type,next){var e,$active=this.$element.find(".item.active"),$next=next||$active[type](),isCycling=this.interval,direction="next"==type?"left":"right",fallback="next"==type?"first":"last",that=this;if(this.sliding=!0,isCycling&&this.pause(),$next=$next.length?$next:this.$element.find(".item")[fallback](),e=$.Event("slide",{relatedTarget:$next[0]}),!$next.hasClass("active")){if($.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(e),e.isDefaultPrevented())return;$next.addClass(type),$next[0].offsetWidth,$active.addClass(direction),$next.addClass(direction),this.$element.one($.support.transition.end,function(){$next.removeClass([type,direction].join(" ")).addClass("active"),$active.removeClass(["active",direction].join(" ")),that.sliding=!1,setTimeout(function(){that.$element.trigger("slid")},0)})}else{if(this.$element.trigger(e),e.isDefaultPrevented())return;$active.removeClass("active"),$next.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return isCycling&&this.cycle(),this}}};var old=$.fn.carousel;$.fn.carousel=function(option){return this.each(function(){var $this=$(this),data=$this.data("carousel"),options=$.extend({},$.fn.carousel.defaults,"object"==typeof option&&option),action="string"==typeof option?option:options.slide;data||$this.data("carousel",data=new Carousel(this,options)),"number"==typeof option?data.to(option):action?data[action]():options.interval&&data.cycle()})},$.fn.carousel.defaults={interval:5e3,pause:"hover"},$.fn.carousel.Constructor=Carousel,$.fn.carousel.noConflict=function(){return $.fn.carousel=old,this},$(document).on("click.carousel.data-api","[data-slide]",function(e){var href,$this=$(this),$target=$($this.attr("data-target")||(href=$this.attr("href"))&&href.replace(/.*(?=#[^\s]+$)/,"")),options=$.extend({},$target.data(),$this.data());$target.carousel(options),e.preventDefault()})}(window.jQuery),!function($){"use strict";var Collapse=function(element,options){this.$element=$(element),this.options=$.extend({},$.fn.collapse.defaults,options),this.options.parent&&(this.$parent=$(this.options.parent)),this.options.toggle&&this.toggle()};Collapse.prototype={constructor:Collapse,dimension:function(){var hasWidth=this.$element.hasClass("width");return hasWidth?"width":"height"},show:function(){var dimension,scroll,actives,hasData;if(!this.transitioning){if(dimension=this.dimension(),scroll=$.camelCase(["scroll",dimension].join("-")),actives=this.$parent&&this.$parent.find("> .accordion-group > .in"),actives&&actives.length){if(hasData=actives.data("collapse"),hasData&&hasData.transitioning)return;actives.collapse("hide"),hasData||actives.data("collapse",null)}this.$element[dimension](0),this.transition("addClass",$.Event("show"),"shown"),$.support.transition&&this.$element[dimension](this.$element[0][scroll])}},hide:function(){var dimension;this.transitioning||(dimension=this.dimension(),this.reset(this.$element[dimension]()),this.transition("removeClass",$.Event("hide"),"hidden"),this.$element[dimension](0))},reset:function(size){var dimension=this.dimension();return this.$element.removeClass("collapse")[dimension](size||"auto")[0].offsetWidth,this.$element[null!==size?"addClass":"removeClass"]("collapse"),this},transition:function(method,startEvent,completeEvent){var that=this,complete=function(){"show"==startEvent.type&&that.reset(),that.transitioning=0,that.$element.trigger(completeEvent)};this.$element.trigger(startEvent),startEvent.isDefaultPrevented()||(this.transitioning=1,this.$element[method]("in"),$.support.transition&&this.$element.hasClass("collapse")?this.$element.one($.support.transition.end,complete):complete())},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var old=$.fn.collapse;$.fn.collapse=function(option){return this.each(function(){var $this=$(this),data=$this.data("collapse"),options="object"==typeof option&&option;data||$this.data("collapse",data=new Collapse(this,options)),"string"==typeof option&&data[option]()})},$.fn.collapse.defaults={toggle:!0},$.fn.collapse.Constructor=Collapse,$.fn.collapse.noConflict=function(){return $.fn.collapse=old,this},$(document).on("click.collapse.data-api","[data-toggle=collapse]",function(e){var href,$this=$(this),target=$this.attr("data-target")||e.preventDefault()||(href=$this.attr("href"))&&href.replace(/.*(?=#[^\s]+$)/,""),option=$(target).data("collapse")?"toggle":$this.data();$this[$(target).hasClass("in")?"addClass":"removeClass"]("collapsed"),$(target).collapse(option)})}(window.jQuery),!function($){"use strict";function clearMenus(){$(toggle).each(function(){getParent($(this)).removeClass("open")})}function getParent($this){var $parent,selector=$this.attr("data-target");return selector||(selector=$this.attr("href"),selector=selector&&/#/.test(selector)&&selector.replace(/.*(?=#[^\s]*$)/,"")),$parent=$(selector),$parent.length||($parent=$this.parent()),$parent}var toggle="[data-toggle=dropdown]",Dropdown=function(element){var $el=$(element).on("click.dropdown.data-api",this.toggle);$("html").on("click.dropdown.data-api",function(){$el.parent().removeClass("open")})};Dropdown.prototype={constructor:Dropdown,toggle:function(){var $parent,isActive,$this=$(this);if(!$this.is(".disabled, :disabled"))return $parent=getParent($this),isActive=$parent.hasClass("open"),clearMenus(),isActive||$parent.toggleClass("open"),$this.focus(),!1},keydown:function(e){var $this,$items,$parent,isActive,index;if(/(38|40|27)/.test(e.keyCode)&&($this=$(this),e.preventDefault(),e.stopPropagation(),!$this.is(".disabled, :disabled"))){if($parent=getParent($this),isActive=$parent.hasClass("open"),!isActive||isActive&&27==e.keyCode)return $this.click();$items=$("[role=menu] li:not(.divider):visible a",$parent),$items.length&&(index=$items.index($items.filter(":focus")),38==e.keyCode&&index>0&&index--,40==e.keyCode&&$items.length-1>index&&index++,~index||(index=0),$items.eq(index).focus())}}};var old=$.fn.dropdown;$.fn.dropdown=function(option){return this.each(function(){var $this=$(this),data=$this.data("dropdown");data||$this.data("dropdown",data=new Dropdown(this)),"string"==typeof option&&data[option].call($this)})},$.fn.dropdown.Constructor=Dropdown,$.fn.dropdown.noConflict=function(){return $.fn.dropdown=old,this},$(document).on("click.dropdown.data-api touchstart.dropdown.data-api",clearMenus).on("click.dropdown touchstart.dropdown.data-api",".dropdown form",function(e){e.stopPropagation()}).on("touchstart.dropdown.data-api",".dropdown-menu",function(e){e.stopPropagation()}).on("click.dropdown.data-api touchstart.dropdown.data-api",toggle,Dropdown.prototype.toggle).on("keydown.dropdown.data-api touchstart.dropdown.data-api",toggle+", [role=menu]",Dropdown.prototype.keydown)}(window.jQuery),!function($){"use strict";var Modal=function(element,options){this.options=options,this.$element=$(element).delegate('[data-dismiss="modal"]',"click.dismiss.modal",$.proxy(this.hide,this)),this.options.remote&&this.$element.find(".modal-body").load(this.options.remote)};Modal.prototype={constructor:Modal,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var that=this,e=$.Event("show");this.$element.trigger(e),this.isShown||e.isDefaultPrevented()||(this.isShown=!0,this.escape(),this.backdrop(function(){var transition=$.support.transition&&that.$element.hasClass("fade");that.$element.parent().length||that.$element.appendTo(document.body),that.$element.show(),transition&&that.$element[0].offsetWidth,that.$element.addClass("in").attr("aria-hidden",!1),that.enforceFocus(),transition?that.$element.one($.support.transition.end,function(){that.$element.focus().trigger("shown")}):that.$element.focus().trigger("shown")}))},hide:function(e){e&&e.preventDefault(),e=$.Event("hide"),this.$element.trigger(e),this.isShown&&!e.isDefaultPrevented()&&(this.isShown=!1,this.escape(),$(document).off("focusin.modal"),this.$element.removeClass("in").attr("aria-hidden",!0),$.support.transition&&this.$element.hasClass("fade")?this.hideWithTransition():this.hideModal())},enforceFocus:function(){var that=this;$(document).on("focusin.modal",function(e){that.$element[0]===e.target||that.$element.has(e.target).length||that.$element.focus()})},escape:function(){var that=this;this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.modal",function(e){27==e.which&&that.hide()}):this.isShown||this.$element.off("keyup.dismiss.modal")},hideWithTransition:function(){var that=this,timeout=setTimeout(function(){that.$element.off($.support.transition.end),that.hideModal()},500);this.$element.one($.support.transition.end,function(){clearTimeout(timeout),that.hideModal()})},hideModal:function(){this.$element.hide().trigger("hidden"),this.backdrop()},removeBackdrop:function(){this.$backdrop.remove(),this.$backdrop=null},backdrop:function(callback){var animate=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var doAnimate=$.support.transition&&animate;this.$backdrop=$('