Compare commits

...

21 Commits
0.6.3 ... 0.6.4

Author SHA1 Message Date
vabene1111
e78323d214 Update docker-publish-latest.yml 2020-04-15 15:05:36 +02:00
vabene1111
d2e866dd74 cleanup/refactor workflows 2020-04-15 14:48:18 +02:00
vabene1111
76687ad5df testing multi platform deployment 2020-04-15 12:34:58 +02:00
vabene1111
dab77e8e4f imrpoved index redirect + fixed tests 2020-04-13 23:11:33 +02:00
vabene1111
0b250c71aa actually fixed action yml 2020-04-13 22:55:50 +02:00
vabene1111
571f670db0 fixed action intendations 2020-04-13 22:54:17 +02:00
vabene1111
4e9e628162 added ability to change default page 2020-04-13 22:52:02 +02:00
vabene1111
4f49b06704 user setting default ingredient unit 2020-04-13 22:37:50 +02:00
vabene1111
8eb0c36665 fixed action invalid yaml 2020-04-13 21:53:36 +02:00
vabene1111
6f69c09aca Merge pull request #55 from hakoerber/kubernetes-manifests
Kubernetes manifests
2020-04-13 21:51:58 +02:00
vabene1111
8e6f153882 Merge pull request #56 from tourn/clickable-links
Make links in recipe clickable
2020-04-13 21:50:25 +02:00
vabene1111
07183fd40f actions testing 2020-04-13 21:39:20 +02:00
tourn
04b7f0a398 Make links in recipe clickable 2020-04-13 21:27:22 +02:00
Hannes Körber
1735fda48f Add basic kubernetes manifest
Closes #50
2020-04-13 19:39:41 +02:00
Hannes Körber
1c9ea0eda7 Use relative links in README
See https://github.blog/2013-01-31-relative-links-in-markup-files/ for
more details.
2020-04-13 19:39:39 +02:00
vabene1111
83b5b6695c Update docker-release-publish.yml 2020-04-13 18:25:49 +02:00
vabene1111
342fb3c96d Update docker-release-publish.yml 2020-04-13 18:23:19 +02:00
vabene1111
b7a18466b5 testing multi plattform builds 2020-04-13 18:17:53 +02:00
vabene1111
0cdc4d51df Merge branch 'feature/webdav-root-option' into develop 2020-04-13 17:41:02 +02:00
vabene1111
e177669514 Merge pull request #51 from pataya23/develop
added 'webdav_root' option
2020-04-13 17:38:19 +02:00
pataya23
fd294dfcdd added 'webdav_root' option
otherwise I get an error (webdav3.exceptions.RemoteResourceNotFound: Remote resource: </path> not found)
2020-04-13 13:19:03 +02:00
23 changed files with 411 additions and 32 deletions

View File

@@ -1,18 +1,18 @@
name: Deploy Docker Image
name: publish dev image docker
on:
push:
tags:
- '*'
branches:
- '*'
- '*/*'
- '!master'
jobs:
build-push:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Publish to Registry
uses: AhnSeongHyun/action-tag-docker-build-push@v1.0.0
uses: elgohr/Publish-Docker-Github-Action@2.13
with:
repo_name: vabene1111/recipes
name: vabene1111/recipes
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

View File

@@ -0,0 +1,19 @@
name: publish latest image docker
on:
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Build and publish image
uses: ilteoood/docker_buildx@master
with:
publish: true
imageName: vabene1111/recipes
tag: latest
dockerHubUser: ${{ secrets.DOCKER_USERNAME }}
dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }}

View File

@@ -0,0 +1,25 @@
name: publish tagged release docker
on:
push:
tags:
- '*'
jobs:
build:
runs-on: ubuntu-latest
name: Build image job
steps:
- name: Checkout master
uses: actions/checkout@master#
- name: Get the version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//}
- name: Build and publish image
uses: ilteoood/docker_buildx@master
with:
publish: true
imageName: vabene1111/recipes
tag: ${{ steps.get_version.outputs.VERSION }}
dockerHubUser: ${{ secrets.DOCKER_USERNAME }}
dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }}

View File

@@ -1,13 +0,0 @@
name: Publish Docker
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@2.13
with:
name: vabene1111/recipes
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

View File

@@ -25,7 +25,7 @@ The docker image (`vabene1111/recipes`) simply exposes the application on port `
### Docker-Compose
2. Choose one of the included configurations [here](https://github.com/vabene1111/recipes/tree/develop/docs/docker).
2. Choose one of the included configurations [here](docs/docker).
2. Download the environment (config) file template and fill it out `wget https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template -O .env `
3. Start the container `docker-compose up -d`
4. Create a default user by running `docker-compose exec web_recipes createsuperuser`.
@@ -46,6 +46,10 @@ While intermediate updates can be skipped when updating please make sure to **re
2. Pull the latest image using `docker-compose pull`
3. Start the container again using `docker-compose up -d`
## Kubernetes
You can find a basic kubernetes setup [here](docs/k8s/). Please see the README in the folder for more detail.
# Documentation
Most things should be straight forward but there are some more complicated things.

View File

@@ -31,10 +31,11 @@ class UserPreferenceForm(forms.ModelForm):
class Meta:
model = UserPreference
fields = ('theme', 'nav_color')
fields = ('default_unit', 'theme', 'nav_color', 'default_page')
help_texts = {
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!')
'nav_color': _('Color of the top navigation bar. Not all colors work with all themes, just try them out!'),
'default_unit': _('Default Unit to be used when inserting a new ingredient into a recipe.')
}

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.4 on 2020-04-13 20:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0031_auto_20200407_1841'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='default_unit',
field=models.CharField(default='g', max_length=32),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.0.4 on 2020-04-13 20:41
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0032_userpreference_default_unit'),
]
operations = [
migrations.AddField(
model_name='userpreference',
name='default_page',
field=models.CharField(choices=[('SEARCH', 'Search'), ('PLAN', 'Meal-Plan')], default='SEARCH', max_length=64),
),
]

View File

@@ -41,9 +41,18 @@ class UserPreference(models.Model):
COLORS = ((PRIMARY, 'Primary'), (SECONDARY, 'Secondary'), (SUCCESS, 'Success'), (INFO, 'Info'), (WARNING, 'Warning'), (DANGER, 'Danger'), (LIGHT, 'Light'), (DARK, 'Dark'))
# Default Page
SEARCH = 'SEARCH'
PLAN = 'PLAN'
BOOKS = 'BOOKS'
PAGES = ((SEARCH, _('Search')), (PLAN, _('Meal-Plan')), (BOOKS, _('Books')), )
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
theme = models.CharField(choices=THEMES, max_length=128, default=FLATLY)
nav_color = models.CharField(choices=COLORS, max_length=128, default=PRIMARY)
default_unit = models.CharField(max_length=32, default='g')
default_page = models.CharField(choices=PAGES, max_length=64, default=SEARCH)
def __str__(self):
return self.user

View File

@@ -16,9 +16,10 @@ class Nextcloud(Provider):
@staticmethod
def get_client(storage):
options = {
'webdav_hostname': storage.url + '/remote.php/dav/files/' + storage.username,
'webdav_hostname': storage.url,
'webdav_login': storage.username,
'webdav_password': storage.password
'webdav_password': storage.password,
'webdav_root': '/remote.php/dav/files/' + storage.username
}
return wc.Client(options)

View File

@@ -74,8 +74,8 @@
</button>
<div class="collapse navbar-collapse" id="navbarText">
<ul class="navbar-nav mr-auto">
<li class="nav-item {% if request.resolver_match.url_name in 'index,edit_recipe,edit_internal_recipe,edit_external_recipe,view_recipe' %}active{% endif %}">
<a class="nav-link" href="{% url 'index' %}"><i class="fas fa-book"></i> {% trans 'Cookbook' %}<span
<li class="nav-item {% if request.resolver_match.url_name in 'view_search,edit_recipe,edit_internal_recipe,edit_external_recipe,view_recipe' %}active{% endif %}">
<a class="nav-link" href="{% url 'view_search' %}"><i class="fas fa-book"></i> {% trans 'Cookbook' %}<span
class="sr-only">(current)</span></a>
</li>

View File

@@ -189,7 +189,7 @@
data.push({
ingredient__name: "{% trans 'Ingredient' %}",
amount: "100",
unit__name: "g",
unit__name: "{{ request.user.userpreference.default_unit }}",
note: "",
id: Math.floor(Math.random() * 10000000),
delete: false,

View File

@@ -194,7 +194,7 @@
<div style="font-size: large">
{% if recipe.instructions %}
{{ recipe.instructions | markdown | safe }}
{{ recipe.instructions | markdown | safe | urlize }}
{% endif %}
</div>

View File

@@ -7,10 +7,10 @@ class TestViewsGeneral(TestViews):
def test_index(self):
r = self.client.get(reverse('index'))
self.assertEqual(r.status_code, 200)
self.assertEqual(r.status_code, 302)
r = self.anonymous_client.get(reverse('index'))
self.assertEqual(r.status_code, 200)
self.assertEqual(r.status_code, 302)
def test_books(self):
url = reverse('view_books')

View File

@@ -8,6 +8,7 @@ from cookbook.helper import dal
urlpatterns = [
path('', views.index, name='index'),
path('search/', views.search, name='view_search'),
path('books/', views.books, name='view_books'),
path('plan/', views.meal_plan, name='view_plan'),
path('shopping/', views.shopping_list, name='view_shopping'),

View File

@@ -6,7 +6,9 @@ from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import PasswordChangeForm
from django.http import HttpResponseRedirect
from django.shortcuts import render, get_object_or_404
from django.urls import reverse_lazy
from django_tables2 import RequestConfig
from django.utils.translation import gettext as _
@@ -16,6 +18,21 @@ from cookbook.tables import RecipeTable
def index(request):
if not request.user.is_authenticated:
return HttpResponseRedirect(reverse_lazy('view_search'))
try:
page_map = {
UserPreference.SEARCH: reverse_lazy('view_search'),
UserPreference.PLAN: reverse_lazy('view_plan'),
UserPreference.BOOKS: reverse_lazy('view_books'),
}
return HttpResponseRedirect(page_map.get(request.user.userpreference.default_page))
except UserPreference.DoesNotExist:
return HttpResponseRedirect(reverse_lazy('view_search'))
def search(request):
if request.user.is_authenticated:
f = RecipeFilter(request.GET, queryset=Recipe.objects.all().order_by('name'))
@@ -175,6 +192,8 @@ def settings(request):
up = UserPreference(user=request.user)
up.theme = form.cleaned_data['theme']
up.nav_color = form.cleaned_data['nav_color']
up.default_unit = form.cleaned_data['default_unit']
up.default_page = form.cleaned_data['default_page']
up.save()
if 'user_name_form' in request.POST:

View File

@@ -0,0 +1,33 @@
kind: ConfigMap
apiVersion: v1
metadata:
labels:
app: recipes
name: recipes-nginx-config
data:
nginx-config: |-
events {
worker_connections 1024;
}
http {
server {
listen 80;
server_name _;
client_max_body_size 16M;
# serve static files
location /static/ {
alias /static/;
}
# serve media files
location /media/ {
alias /media/;
}
# pass requests for dynamic content to gunicorn
location / {
proxy_set_header Host $host;
proxy_pass http://localhost:8080;
}
}
}

50
docs/k8s/30-pv.yaml Normal file
View File

@@ -0,0 +1,50 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: recipes-db
labels:
app: recipes
type: local
tier: db
spec:
storageClassName: manual
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
hostPath:
path: "/data/recipes/db"
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: recipes-media
labels:
app: recipes
type: local
tier: media
spec:
storageClassName: manual
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
hostPath:
path: "/data/recipes/media"
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: recipes-static
labels:
app: recipes
type: local
tier: static
spec:
storageClassName: manual
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
hostPath:
path: "/data/recipes/static"

52
docs/k8s/30-pvc.yaml Normal file
View File

@@ -0,0 +1,52 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: recipes-db
labels:
app: recipes
spec:
selector:
matchLabels:
tier: db
storageClassName: manual
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: recipes-media
labels:
app: recipes
spec:
selector:
matchLabels:
tier: media
app: recipes
storageClassName: manual
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: recipes-static
labels:
app: recipes
spec:
selector:
matchLabels:
tier: static
app: recipes
storageClassName: manual
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi

102
docs/k8s/50-deployment.yaml Normal file
View File

@@ -0,0 +1,102 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: recipes
labels:
app: recipes
environment: production
tier: frontend
spec:
replicas: 1
strategy:
type: RollingUpdate
selector:
matchLabels:
app: recipes
environment: production
template:
metadata:
labels:
app: recipes
environment: production
spec:
containers:
- name: recipes-nginx
image: nginx:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
protocol: TCP
name: http
volumeMounts:
- mountPath: '/media'
name: media
- mountPath: '/static'
name: static
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx-config
readOnly: true
- name: recipes
image: 'vabene1111/recipes:latest'
imagePullPolicy: IfNotPresent
livenessProbe:
httpGet:
path: /
port: 8080
readinessProbe:
httpGet:
path: /
port: 8080
volumeMounts:
- mountPath: '/opt/recipes/mediafiles'
name: media
- mountPath: '/opt/recipes/staticfiles'
name: static
env:
- name: DEBUG
value: "0"
- name: ALLOWED_HOSTS
value: '*'
- name: SECRET_KEY
value: # CHANGEME
- name: DB_ENGINE
value: django.db.backends.postgresql_psycopg2
- name: POSTGRES_HOST
value: localhost
- name: POSTGRES_PORT
value: "5432"
- name: POSTGRES_USER
value: recipes
- name: POSTGRES_DB
value: recipes
- name: POSTGRES_PASSWORD
value: # CHANGEME
- name: recipes-db
image: 'postgres:latest'
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5432
volumeMounts:
- mountPath: '/var/lib/postgresql/data'
name: database
env:
- name: POSTGRES_USER
value: recipes
- name: POSTGRES_DB
value: recipes
- name: POSTGRES_PASSWORD
value: # CHANGEME
volumes:
- name: database
persistentVolumeClaim:
claimName: recipes-db
- name: media
persistentVolumeClaim:
claimName: recipes-media
- name: static
persistentVolumeClaim:
claimName: recipes-static
- name: nginx-config
configMap:
name: recipes-nginx-config

15
docs/k8s/60-service.yaml Normal file
View File

@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: recipes
labels:
app: recipes
spec:
selector:
app: recipes
environment: production
ports:
- port: 80
targetPort: http
name: http
protocol: TCP

25
docs/k8s/README.md Normal file
View File

@@ -0,0 +1,25 @@
# Kubernetes
This is a basic kubernetes setup. Please note that this does not necessarily follow Kubernetes best practices and should only used as a basis to build your own setup from!
## Important notes
State (database, static files and media files) is handled via `PersistentVolumes`.
Note that you will most likely have to change the `PersistentVolumes` in `30-pv.yaml`. The current setup is only usable for a single-node cluster because it uses local storage on the kubernetes worker nodes under `/data/recipes/`. It should just serve as an example.
Currently, the deployment in `50-deployment.yaml` just pulls the `latest` tag of all containers. In a production setup, you should set this to a fixed version!
See env variables tagged with `CHANGEME` in `50-deployment.yaml` and make sure to change those! A better setup would use kubernetes secrets but this is not implemented yet.
## Updates
These manifests are not tested against new versions.
## Apply the manifets
To apply the manifest with `kubectl`, use the following command:
```
kubectl apply -f ./docs/k8s/
```