├── 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 |
4 |
5 |
6 |
7 | |
8 |
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 | |
29 |
30 |
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 |
4 |
5 |
6 |
7 | |
8 |
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 | |
29 |
30 |
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 | 
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 |
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 | |
22 |
23 |
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 |
{{ message }}
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=$('').appendTo(document.body),this.$backdrop.click("static"==this.options.backdrop?$.proxy(this.$element[0].focus,this.$element[0]):$.proxy(this.hide,this)),doAnimate&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),doAnimate?this.$backdrop.one($.support.transition.end,callback):callback()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),$.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one($.support.transition.end,$.proxy(this.removeBackdrop,this)):this.removeBackdrop()):callback&&callback()}};var old=$.fn.modal;$.fn.modal=function(option){return this.each(function(){var $this=$(this),data=$this.data("modal"),options=$.extend({},$.fn.modal.defaults,$this.data(),"object"==typeof option&&option);data||$this.data("modal",data=new Modal(this,options)),"string"==typeof option?data[option]():options.show&&data.show()})},$.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},$.fn.modal.Constructor=Modal,$.fn.modal.noConflict=function(){return $.fn.modal=old,this},$(document).on("click.modal.data-api",'[data-toggle="modal"]',function(e){var $this=$(this),href=$this.attr("href"),$target=$($this.attr("data-target")||href&&href.replace(/.*(?=#[^\s]+$)/,"")),option=$target.data("modal")?"toggle":$.extend({remote:!/#/.test(href)&&href},$target.data(),$this.data());e.preventDefault(),$target.modal(option).one("hide",function(){$this.focus()})})}(window.jQuery),!function($){"use strict";var Tooltip=function(element,options){this.init("tooltip",element,options)};Tooltip.prototype={constructor:Tooltip,init:function(type,element,options){var eventIn,eventOut;this.type=type,this.$element=$(element),this.options=this.getOptions(options),this.enabled=!0,"click"==this.options.trigger?this.$element.on("click."+this.type,this.options.selector,$.proxy(this.toggle,this)):"manual"!=this.options.trigger&&(eventIn="hover"==this.options.trigger?"mouseenter":"focus",eventOut="hover"==this.options.trigger?"mouseleave":"blur",this.$element.on(eventIn+"."+this.type,this.options.selector,$.proxy(this.enter,this)),this.$element.on(eventOut+"."+this.type,this.options.selector,$.proxy(this.leave,this))),this.options.selector?this._options=$.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(options){return options=$.extend({},$.fn[this.type].defaults,options,this.$element.data()),options.delay&&"number"==typeof options.delay&&(options.delay={show:options.delay,hide:options.delay}),options},enter:function(e){var self=$(e.currentTarget)[this.type](this._options).data(this.type);return self.options.delay&&self.options.delay.show?(clearTimeout(this.timeout),self.hoverState="in",this.timeout=setTimeout(function(){"in"==self.hoverState&&self.show()},self.options.delay.show),void 0):self.show()},leave:function(e){var self=$(e.currentTarget)[this.type](this._options).data(this.type);return this.timeout&&clearTimeout(this.timeout),self.options.delay&&self.options.delay.hide?(self.hoverState="out",this.timeout=setTimeout(function(){"out"==self.hoverState&&self.hide()},self.options.delay.hide),void 0):self.hide()},show:function(){var $tip,inside,pos,actualWidth,actualHeight,placement,tp;if(this.hasContent()&&this.enabled){switch($tip=this.tip(),this.setContent(),this.options.animation&&$tip.addClass("fade"),placement="function"==typeof this.options.placement?this.options.placement.call(this,$tip[0],this.$element[0]):this.options.placement,inside=/in/.test(placement),$tip.detach().css({top:0,left:0,display:"block"}).insertAfter(this.$element),pos=this.getPosition(inside),actualWidth=$tip[0].offsetWidth,actualHeight=$tip[0].offsetHeight,inside?placement.split(" ")[1]:placement){case"bottom":tp={top:pos.top+pos.height,left:pos.left+pos.width/2-actualWidth/2};break;case"top":tp={top:pos.top-actualHeight,left:pos.left+pos.width/2-actualWidth/2};break;case"left":tp={top:pos.top+pos.height/2-actualHeight/2,left:pos.left-actualWidth};break;case"right":tp={top:pos.top+pos.height/2-actualHeight/2,left:pos.left+pos.width}}$tip.offset(tp).addClass(placement).addClass("in")}},setContent:function(){var $tip=this.tip(),title=this.getTitle();$tip.find(".tooltip-inner")[this.options.html?"html":"text"](title),$tip.removeClass("fade in top bottom left right")},hide:function(){function removeWithAnimation(){var timeout=setTimeout(function(){$tip.off($.support.transition.end).detach()},500);$tip.one($.support.transition.end,function(){clearTimeout(timeout),$tip.detach()})}var $tip=this.tip();return $tip.removeClass("in"),$.support.transition&&this.$tip.hasClass("fade")?removeWithAnimation():$tip.detach(),this},fixTitle:function(){var $e=this.$element;($e.attr("title")||"string"!=typeof $e.attr("data-original-title"))&&$e.attr("data-original-title",$e.attr("title")||"").removeAttr("title")},hasContent:function(){return this.getTitle()},getPosition:function(inside){return $.extend({},inside?{top:0,left:0}:this.$element.offset(),{width:this.$element[0].offsetWidth,height:this.$element[0].offsetHeight})},getTitle:function(){var title,$e=this.$element,o=this.options;return title=$e.attr("data-original-title")||("function"==typeof o.title?o.title.call($e[0]):o.title)},tip:function(){return this.$tip=this.$tip||$(this.options.template)},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(e){var self=$(e.currentTarget)[this.type](this._options).data(this.type);self[self.tip().hasClass("in")?"hide":"show"]()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}};var old=$.fn.tooltip;$.fn.tooltip=function(option){return this.each(function(){var $this=$(this),data=$this.data("tooltip"),options="object"==typeof option&&option;data||$this.data("tooltip",data=new Tooltip(this,options)),"string"==typeof option&&data[option]()})},$.fn.tooltip.Constructor=Tooltip,$.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'',trigger:"hover",title:"",delay:0,html:!1},$.fn.tooltip.noConflict=function(){return $.fn.tooltip=old,this}}(window.jQuery),!function($){"use strict";var Popover=function(element,options){this.init("popover",element,options)};Popover.prototype=$.extend({},$.fn.tooltip.Constructor.prototype,{constructor:Popover,setContent:function(){var $tip=this.tip(),title=this.getTitle(),content=this.getContent();$tip.find(".popover-title")[this.options.html?"html":"text"](title),$tip.find(".popover-content")[this.options.html?"html":"text"](content),$tip.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var content,$e=this.$element,o=this.options;return content=$e.attr("data-content")||("function"==typeof o.content?o.content.call($e[0]):o.content)},tip:function(){return this.$tip||(this.$tip=$(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}});var old=$.fn.popover;$.fn.popover=function(option){return this.each(function(){var $this=$(this),data=$this.data("popover"),options="object"==typeof option&&option;data||$this.data("popover",data=new Popover(this,options)),"string"==typeof option&&data[option]()})},$.fn.popover.Constructor=Popover,$.fn.popover.defaults=$.extend({},$.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:''}),$.fn.popover.noConflict=function(){return $.fn.popover=old,this}}(window.jQuery),!function($){"use strict";function ScrollSpy(element,options){var href,process=$.proxy(this.process,this),$element=$(element).is("body")?$(window):$(element);this.options=$.extend({},$.fn.scrollspy.defaults,options),this.$scrollElement=$element.on("scroll.scroll-spy.data-api",process),this.selector=(this.options.target||(href=$(element).attr("href"))&&href.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=$("body"),this.refresh(),this.process()}ScrollSpy.prototype={constructor:ScrollSpy,refresh:function(){var $targets,self=this;this.offsets=$([]),this.targets=$([]),$targets=this.$body.find(this.selector).map(function(){var $el=$(this),href=$el.data("target")||$el.attr("href"),$href=/^#\w/.test(href)&&$(href);return $href&&$href.length&&[[$href.position().top+self.$scrollElement.scrollTop(),href]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){self.offsets.push(this[0]),self.targets.push(this[1])})},process:function(){var i,scrollTop=this.$scrollElement.scrollTop()+this.options.offset,scrollHeight=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,maxScroll=scrollHeight-this.$scrollElement.height(),offsets=this.offsets,targets=this.targets,activeTarget=this.activeTarget;if(scrollTop>=maxScroll)return activeTarget!=(i=targets.last()[0])&&this.activate(i);for(i=offsets.length;i--;)activeTarget!=targets[i]&&scrollTop>=offsets[i]&&(!offsets[i+1]||offsets[i+1]>=scrollTop)&&this.activate(targets[i])},activate:function(target){var active,selector;this.activeTarget=target,$(this.selector).parent(".active").removeClass("active"),selector=this.selector+'[data-target="'+target+'"],'+this.selector+'[href="'+target+'"]',active=$(selector).parent("li").addClass("active"),active.parent(".dropdown-menu").length&&(active=active.closest("li.dropdown").addClass("active")),active.trigger("activate")}};var old=$.fn.scrollspy;$.fn.scrollspy=function(option){return this.each(function(){var $this=$(this),data=$this.data("scrollspy"),options="object"==typeof option&&option;data||$this.data("scrollspy",data=new ScrollSpy(this,options)),"string"==typeof option&&data[option]()})},$.fn.scrollspy.Constructor=ScrollSpy,$.fn.scrollspy.defaults={offset:10},$.fn.scrollspy.noConflict=function(){return $.fn.scrollspy=old,this},$(window).on("load",function(){$('[data-spy="scroll"]').each(function(){var $spy=$(this);$spy.scrollspy($spy.data())})})}(window.jQuery),!function($){"use strict";var Tab=function(element){this.element=$(element)};Tab.prototype={constructor:Tab,show:function(){var previous,$target,e,$this=this.element,$ul=$this.closest("ul:not(.dropdown-menu)"),selector=$this.attr("data-target");selector||(selector=$this.attr("href"),selector=selector&&selector.replace(/.*(?=#[^\s]*$)/,"")),$this.parent("li").hasClass("active")||(previous=$ul.find(".active:last a")[0],e=$.Event("show",{relatedTarget:previous}),$this.trigger(e),e.isDefaultPrevented()||($target=$(selector),this.activate($this.parent("li"),$ul),this.activate($target,$target.parent(),function(){$this.trigger({type:"shown",relatedTarget:previous})})))},activate:function(element,container,callback){function next(){$active.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),element.addClass("active"),transition?(element[0].offsetWidth,element.addClass("in")):element.removeClass("fade"),element.parent(".dropdown-menu")&&element.closest("li.dropdown").addClass("active"),callback&&callback()}var $active=container.find("> .active"),transition=callback&&$.support.transition&&$active.hasClass("fade");transition?$active.one($.support.transition.end,next):next(),$active.removeClass("in")}};var old=$.fn.tab;$.fn.tab=function(option){return this.each(function(){var $this=$(this),data=$this.data("tab");data||$this.data("tab",data=new Tab(this)),"string"==typeof option&&data[option]()})},$.fn.tab.Constructor=Tab,$.fn.tab.noConflict=function(){return $.fn.tab=old,this},$(document).on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(e){e.preventDefault(),$(this).tab("show")})}(window.jQuery),!function($){"use strict";var Typeahead=function(element,options){this.$element=$(element),this.options=$.extend({},$.fn.typeahead.defaults,options),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.source=this.options.source,this.$menu=$(this.options.menu),this.shown=!1,this.listen()};Typeahead.prototype={constructor:Typeahead,select:function(){var val=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(val)).change(),this.hide()},updater:function(item){return item},show:function(){var pos=$.extend({},this.$element.position(),{height:this.$element[0].offsetHeight});return this.$menu.insertAfter(this.$element).css({top:pos.top+pos.height,left:pos.left}).show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(){var items;return this.query=this.$element.val(),!this.query||this.query.length"+match+""})},render:function(items){var that=this;return items=$(items).map(function(i,item){return i=$(that.options.item).attr("data-value",item),i.find("a").html(that.highlighter(item)),i[0]}),items.first().addClass("active"),this.$menu.html(items),this},next:function(){var active=this.$menu.find(".active").removeClass("active"),next=active.next();next.length||(next=$(this.$menu.find("li")[0])),next.addClass("active")},prev:function(){var active=this.$menu.find(".active").removeClass("active"),prev=active.prev();prev.length||(prev=this.$menu.find("li").last()),prev.addClass("active")},listen:function(){this.$element.on("blur",$.proxy(this.blur,this)).on("keypress",$.proxy(this.keypress,this)).on("keyup",$.proxy(this.keyup,this)),this.eventSupported("keydown")&&this.$element.on("keydown",$.proxy(this.keydown,this)),this.$menu.on("click",$.proxy(this.click,this)).on("mouseenter","li",$.proxy(this.mouseenter,this))},eventSupported:function(eventName){var isSupported=eventName in this.$element;return isSupported||(this.$element.setAttribute(eventName,"return;"),isSupported="function"==typeof this.$element[eventName]),isSupported},move:function(e){if(this.shown){switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()}},keydown:function(e){this.suppressKeyPressRepeat=~$.inArray(e.keyCode,[40,38,9,13,27]),this.move(e)},keypress:function(e){this.suppressKeyPressRepeat||this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:case 16:case 17:case 18:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},blur:function(){var that=this;setTimeout(function(){that.hide()},150)},click:function(e){e.stopPropagation(),e.preventDefault(),this.select()},mouseenter:function(e){this.$menu.find(".active").removeClass("active"),$(e.currentTarget).addClass("active")}};var old=$.fn.typeahead;$.fn.typeahead=function(option){return this.each(function(){var $this=$(this),data=$this.data("typeahead"),options="object"==typeof option&&option;data||$this.data("typeahead",data=new Typeahead(this,options)),"string"==typeof option&&data[option]()})},$.fn.typeahead.defaults={source:[],items:8,menu:'',item:'',minLength:1},$.fn.typeahead.Constructor=Typeahead,$.fn.typeahead.noConflict=function(){return $.fn.typeahead=old,this},$(document).on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(e){var $this=$(this);$this.data("typeahead")||(e.preventDefault(),$this.typeahead($this.data()))})}(window.jQuery),!function($){"use strict";var Affix=function(element,options){this.options=$.extend({},$.fn.affix.defaults,options),this.$window=$(window).on("scroll.affix.data-api",$.proxy(this.checkPosition,this)).on("click.affix.data-api",$.proxy(function(){setTimeout($.proxy(this.checkPosition,this),1)},this)),this.$element=$(element),this.checkPosition()};Affix.prototype.checkPosition=function(){if(this.$element.is(":visible")){var affix,scrollHeight=$(document).height(),scrollTop=this.$window.scrollTop(),position=this.$element.offset(),offset=this.options.offset,offsetBottom=offset.bottom,offsetTop=offset.top,reset="affix affix-top affix-bottom";"object"!=typeof offset&&(offsetBottom=offsetTop=offset),"function"==typeof offsetTop&&(offsetTop=offset.top()),"function"==typeof offsetBottom&&(offsetBottom=offset.bottom()),affix=null!=this.unpin&&scrollTop+this.unpin<=position.top?!1:null!=offsetBottom&&position.top+this.$element.height()>=scrollHeight-offsetBottom?"bottom":null!=offsetTop&&offsetTop>=scrollTop?"top":!1,this.affixed!==affix&&(this.affixed=affix,this.unpin="bottom"==affix?position.top-scrollTop:null,this.$element.removeClass(reset).addClass("affix"+(affix?"-"+affix:"")))}};var old=$.fn.affix;$.fn.affix=function(option){return this.each(function(){var $this=$(this),data=$this.data("affix"),options="object"==typeof option&&option;data||$this.data("affix",data=new Affix(this,options)),"string"==typeof option&&data[option]()})},$.fn.affix.Constructor=Affix,$.fn.affix.defaults={offset:0},$.fn.affix.noConflict=function(){return $.fn.affix=old,this},$(window).on("load",function(){$('[data-spy="affix"]').each(function(){var $spy=$(this),data=$spy.data();data.offset=data.offset||{},data.offsetBottom&&(data.offset.bottom=data.offsetBottom),data.offsetTop&&(data.offset.top=data.offsetTop),$spy.affix(data)})})}(window.jQuery);
--------------------------------------------------------------------------------