1

Working on putting a Django API into Kubernetes.

Any traffic being sent to / the ingress-nginx controller sends to the React FE. Any traffic being sent to /api is sent to the Django BE.

This is the relevant part of ingress-serivce.yaml:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ingress-service
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
  rules:
    - http:
        paths:
          - path: /?(.*)
            backend:
              serviceName: client-cluster-ip-service
              servicePort: 3000
          - path: /api/?(.*)
            backend:
              serviceName: server-cluster-ip-service
              servicePort: 5000

This is the url.py:

from django.contrib import admin from django.urls import include, path

urlpatterns = [
    path('auth/', include('authentication.urls'), name='auth'),
    path('admin/', admin.site.urls, name='admin'),
]

The client portion works just fine. The minikube IP is 192.168.99.105. Navigating to that IP loads the react front-end.

Navigating to 192.168.99.105/api/auth/test/ brings me to a `"Hello World!" response I quickly put together.

However, when I try to go to 192.168.99.105/api/admin. It automatically redirects me to /admin/login/?next=/admin/ which doesn't exist given /api is being removed. Is there anyway to prevent this behavior?

I've also just tried this:

ingress-service.yaml

- http:
    paths:
      - path: /?(.*)
        backend:
          serviceName: client-cluster-ip-service
          servicePort: 3000
      - path: /api/?(.*)
        backend:
          serviceName: server-cluster-ip-service
          servicePort: 5000
      - path: /admin/?(.*)
        backend:
          serviceName: server-cluster-ip-service
          servicePort: 5000

urls.py

urlpatterns = [
    path('auth/', include('authentication.urls'), name='auth'),
    path('/', admin.site.urls),
]

Which just produces "Not Found".

I tried to prefix also using this pattern that shows up in the documentation:

urlpatterns = [
    path('api/', include([
        path('auth/', include('authentication.urls'), name='auth'),
        path('admin/', admin.site.urls),
    ])),
]

But that just made it /api/api.

Here are the routes that are defined for admin/ in the site-packages/django/contrib/admin/sites:

# Admin-site-wide views.
urlpatterns = [
    path('', wrap(self.index), name='index'),
    path('login/', self.login, name='login'),
    path('logout/', wrap(self.logout), name='logout'),
    path('password_change/', wrap(self.password_change, cacheable=True), name='password_change'),
    path(
        'password_change/done/',
        wrap(self.password_change_done, cacheable=True),
        name='password_change_done',
    ),
    path('jsi18n/', wrap(self.i18n_javascript, cacheable=True), name='jsi18n'),
    path(
        'r/<int:content_type_id>/<path:object_id>/',
        wrap(contenttype_views.shortcut),
        name='view_on_site',
    ),
]

I guess the '' is what is causing Django to strip the /api off the URL and make it just 192.168.99.105/admin instead of 192.168.99.105/api/admin.

cjones
  • 8,384
  • 17
  • 81
  • 175
  • best practice would be to prefix the django url pattern with `/api`, https://stackoverflow.com/questions/20997863/add-a-prefix-to-url-patterns-in-django – Efrat Levitan Oct 21 '19 at 22:06
  • Most of the posts are from 2014. The one that might be relevant, doesn't work for me (https://stackoverflow.com/a/56639722/3123109). Just makes `/auth/test` and `/admin` return `"Not Found"`. Check the `ingress-service.yaml`. Any traffic for the BE is coming through already prefixed with `/api`. So if it is prefixed in `urls.py` like `path('api/auth'....` then you have to navigate to `/api/api/auth/test/` to get the response. Not sure the best way to handle this, but I don't think removing the `/api/?(.*)` is really an option since it needs to be exposed. – cjones Oct 21 '19 at 23:13
  • i agree that you shouldnt change the ingress file, we just have to figure out how to prefix the urls. im sorry that the link misled you. which django version do you use? lets find out how to add a url pattern prefix – Efrat Levitan Oct 21 '19 at 23:18
  • @EfratLevitan No worries, I appreciate the help! I'm using 2.2. I've update my question with a prefix pattern I tried, that just ended up doing `/api/api/`. I also updated with my full `ingress-service.yaml` in case something else in there might be causing it. – cjones Oct 22 '19 at 00:44

1 Answers1

2

Ok, finally figured it out. Definitely was a Django setting. Added the following to the Django settings.py:

FORCE_SCRIPT_NAME = '/api/'

Then I had to update the STATIC_URL because it was no longer serving the assets for the admin portal:

STATIC_URL = '/api/static/`

Full settings.py that I'm using, but really the only things that needed changing were adding FORCE_SCRIPT_NAME and updating STATIC_URL:

Django settings for config project.

Generated by 'django-admin startproject' using Django 2.2.6.

For more information on this file, see
https://docs.djangoproject.com/en/2.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/2.2/ref/settings/
"""

import os
from datetime import timedelta

# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# Forces Django to not strip '/api' from the URI
FORCE_SCRIPT_NAME = '/api/'

# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ['SECRET_KEY']

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ['DEBUG'] == 'True'

ALLOWED_HOSTS = [
        'localhost',
        '127.0.0.1',
        '192.168.64.7'
]


# Application definition

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    # Third-party packages
    'rest_framework',

    # Local packages
    'authentication'
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'config.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'config.wsgi.application'


# Database
# https://docs.djangoproject.com/en/2.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ['PGDATABASE'],
        'USER': os.environ['PGUSER'],
        'PASSWORD': os.environ['PGPASSWORD'],
        'HOST': os.environ['PGHOST'],
        'PORT': 1423
    }
}

# REST Framework settings
# https://www.django-rest-framework.org/
REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework_simplejwt.authentication.JWTAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ],
}

# SIMPLE_JWT Settings
# https://github.com/davesque/django-rest-framework-simplejwt
SIMPLE_JWT = {
    'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
    'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
    'ROTATE_REFRESH_TOKENS': False,
    'BLACKLIST_AFTER_ROTATION': True,

    'ALGORITHM': 'HS256',
    'SIGNING_KEY': os.environ['SECRET_KEY'],
    'VERIFYING_KEY': None,
    'AUDIENCE': None,
    'ISSUER': None,

    'AUTH_HEADER_TYPES': ('Bearer',),
    'USER_ID_FIELD': 'id',
    'USER_ID_CLAIM': 'user_id',

    'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
    'TOKEN_TYPE_CLAIM': 'token_type',

    'JTI_CLAIM': 'jti',

    'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
    'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
    'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}

# Password validation
# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization
# https://docs.djangoproject.com/en/2.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'America/Los_Angeles'
USE_I18N = True
USE_L10N = True
USE_TZ = True


# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/
STATIC_URL = '/api/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static')

Dockerfile

FROM python:3.7-slim
ENV PYTHONUNBUFFERED 1
WORKDIR /app
EXPOSE 5000
COPY requirements*.txt ./
RUN pip install -r requirements.txt
COPY . .
RUN python manage.py collectstatic
CMD ["gunicorn", "-b", ":5000", "--log-level", "info", "config.wsgi:application"]
cjones
  • 8,384
  • 17
  • 81
  • 175
  • Can you share your latest configuration? Did you keep all configuration like in the question and just added these two lines to the settings.py file? I'd be appreciated if you can help. Thanks – Cagatay Barin Jan 14 '20 at 15:37
  • @ÇağatayBarın Yeah, it was really only those two settings. I've added my full `settings.py` if that helps. – cjones Jan 14 '20 at 16:10
  • React and Django are now working well together. The only problem I have is that I can't serve static files for Django. Do you have a separate PVC for static files and how is your configuration? I'm exhausted from trying different combinations for a static root or static URL. I'd be appreciated if you can help me. Thanks man. – Cagatay Barin Jan 15 '20 at 08:42
  • On the Django side, the only static files I have are for the admin portal. I'm serving those with `gunicorn` and [`whitenoise`](http://whitenoise.evans.io/en/stable/). In my `Dockerfile` I have it `python manage.py collectstatic`. I posted my `Dockerfile` which has the the `gunicorn` and `collectstatic` stuff. The `whitenoise` settings are in the `settings.py`. – cjones Jan 16 '20 at 22:53