Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ZO-4519: Deploy nightwatch to k8s #595

Merged
merged 5 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
commit-message:
prefix: "MAINT:"

- package-ecosystem: "docker"
directory: "/smoketest"
schedule:
interval: "weekly"
commit-message:
prefix: "MAINT:"
- package-ecosystem: "pip"
directory: "/smoketest"
schedule:
interval: "weekly"
commit-message:
prefix: "MAINT:"
37 changes: 37 additions & 0 deletions .github/workflows/nightwatch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Build and deploy nightwatch tests

on:
push:
branches:
- main
paths:
- '.github/workflows/nightwatch.yaml'
- 'smoketest/**'
pull_request:
paths:
- '.github/workflows/nightwatch.yaml'
- 'smoketest/**'

jobs:
build:
uses: zeitonline/gh-action-workflows/.github/workflows/[email protected]
secrets: inherit
with:
versions: smoketest/k8s/base/versions
# copy&paste from k8s/base and k8s/staging manifest;
# the json/shell quoting is atrocious.
args: |
--override-type=strategic --overrides="{\"spec\": {
\"serviceAccount\": \"baseproject\",
\"containers\": [{
\"name\": \"nightwatch-test-$TAG\",
\"env\": [
{\"name\": \"HTTPS_PROXY\", \"value\": \"http://static-ip-proxy.ops.zeit.de:3128\"},
{\"name\": \"VIVI_XMLRPC_PASSWORD\", \"valueFrom\": {\"secretKeyRef\": {
\"name\": \"principals\",
\"key\": \"vivi_zeit.cms.principals_system.nightwatch\"
}}}
]
}] }}"

# deploy happens via flux (on `main` branch)
44 changes: 44 additions & 0 deletions bin/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/bash

DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

function vault_read() {
local path=$1
local field=$2

if [[ -z "$VAULT_TOKEN" ]]; then
VAULT_TOKEN=$(<"$HOME/.vault-token")
fi
curl --silent -H "X-Vault-Token: $VAULT_TOKEN" \
"${VAULT_ADDR%/}/v1/zon/v1/${path}" | \
sed -e "s+^.*\"${field}\":\"\([^\"]*\).*$+\1+"
}


COMMAND=$1
case $COMMAND in
smoke)
set -e
shift
if [[ "$1" != -* ]]; then
environment=$1
shift
else
environment="staging"
fi

cd "$DIR/../smoketest"

image=$(awk -F': ' '/^ newName:/ { print $2 }' \
< k8s/base/kustomization.yaml)
docker buildx build --output type=docker --quiet --tag $image .
docker run --rm -it \
-e VIVI_XMLRPC_PASSWORD=$(vault_read vivi/$environment/nightwatch password) \
$image \
--nightwatch-environment=$environment "$@"
;;
*)
echo "Unrecognized command: $COMMAND"
exit 1
;;
esac
1 change: 1 addition & 0 deletions smoketest/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
k8s/**/*
9 changes: 9 additions & 0 deletions smoketest/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# See https://github.com/ZeitOnline/gh-action-workflows/blob/main/.github/workflows/nightwatch-build.yaml
FROM python:3.12.1-slim as nightwatch
WORKDIR /app
RUN pip --no-cache-dir install pipenv
COPY Pipfile Pipfile.lock ./
RUN pipenv sync
COPY *.py ./
# See https://github.com/ZeitOnline/kustomize/blob/main/components/nightwatch/deployment.yaml
ENTRYPOINT ["pipenv", "run", "pytest", "--tb=native"]
11 changes: 11 additions & 0 deletions smoketest/Pipfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
"webdavclient3" = "*"
"zeit.nightwatch" = ">=1.3.2"

[requires]
python_version = "3"
549 changes: 549 additions & 0 deletions smoketest/Pipfile.lock

Large diffs are not rendered by default.

77 changes: 77 additions & 0 deletions smoketest/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from io import BytesIO
from urllib.parse import urlparse
import os
import xmlrpc.client

import pytest
import webdav3.client


XMLRPC_AUTH = 'nightwatch:' + os.environ['VIVI_XMLRPC_PASSWORD']
CONFIG_STAGING = {
'browser': {'baseurl': 'https://www.staging.zeit.de'},
'vivi': {
'dav_url': 'http://cms-backend.staging.zeit.de:9000',
'xmlrpc_url': f'https://{XMLRPC_AUTH}@vivi-frontend.staging.zeit.de:9090/',
},
'elasticsearch': 'https://tms-es.staging.zon.zeit.de/zeit_content/_search',
}


CONFIG_PRODUCTION = {
'browser': {'baseurl': 'https://www.zeit.de'},
'vivi': {
'dav_url': 'http://cms-backend.zeit.de:9000',
'xmlrpc_url': f'https://{XMLRPC_AUTH}@vivi-frontend.zeit.de:9090/',
},
'elasticsearch': 'https://tms-es.zon.zeit.de/zeit_content/_search',
}


@pytest.fixture(scope='session')
def nightwatch_config(nightwatch_environment):
config = globals()['CONFIG_%s' % nightwatch_environment.upper()]
return dict(config, environment=nightwatch_environment)


@pytest.fixture(scope='session')
def config(nightwatch_config): # shorter spelling for our tests
return nightwatch_config


def pytest_configure(config):
config.option.prometheus_job_name = 'vivi-deployment-%s' % config.option.nightwatch_environment
if config.option.prometheus_extra_labels is None:
config.option.prometheus_extra_labels = []
config.option.prometheus_extra_labels.append('project=vivi-deployment')


class ViviClient:
def __init__(self, dav_url, xmlrpc_url):
self.dav = webdav3.client.Client({'webdav_hostname': dav_url})
self.xmlrpc = xmlrpc.client.ServerProxy(xmlrpc_url)

def set_property(self, unique_id, ns, name, value):
path = '/cms/work' + urlparse(unique_id).path
if not ns.startswith('http'):
ns = 'http://namespaces.zeit.de/CMS/%s' % ns
self.dav.set_property(path, {'namespace': ns, 'name': name, 'value': value})

def put(self, unique_id, body):
path = '/cms/work' + urlparse(unique_id).path
self.dav.upload_to(BytesIO(body.encode('utf-8')), path)

def refresh_dav_cache(self, unique_id):
if unique_id.startswith('/'):
unique_id = 'http://xml.zeit.de' + unique_id
self.xmlrpc.invalidate(unique_id)

def publish(self, unique_id):
if unique_id.startswith('/'):
unique_id = 'http://xml.zeit.de' + unique_id
self.xmlrpc.publish(unique_id)


@pytest.fixture(scope='session')
def vivi(config):
return ViviClient(**config['vivi'])
25 changes: 25 additions & 0 deletions smoketest/k8s/base/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

components:
- github.com/ZeitOnline/kustomize/components/nightwatch?ref=1.3
- versions

patches:
- target:
kind: Deployment
name: nightwatch
patch: |
- op: add
path: /spec/template/spec/containers/0/env
value:
- name: VIVI_XMLRPC_PASSWORD
valueFrom:
secretKeyRef:
name: principals
key: vivi_zeit.cms.principals_system.nightwatch

# See https://github.com/ZeitOnline/gh-action-workflows/blob/main/.github/workflows/nightwatch-build.yaml
images:
- name: nightwatch
newName: europe-west3-docker.pkg.dev/zeitonline-engineering/docker-zon/vivi-nightwatch
6 changes: 6 additions & 0 deletions smoketest/k8s/base/versions/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component

images:
- name: nightwatch
newTag: "nothing"
15 changes: 15 additions & 0 deletions smoketest/k8s/production/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- ../base

patches:
- target:
kind: Deployment
name: nightwatch
patch: |-
- op: replace
path: /spec/template/spec/containers/0/args
value:
- "--nightwatch-environment=production"
16 changes: 16 additions & 0 deletions smoketest/k8s/staging/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- ../base

patches:
- target:
kind: Deployment
name: nightwatch
patch: |-
- op: add
path: /spec/template/spec/containers/0/env/-
value:
name: HTTPS_PROXY
value: http://static-ip-proxy.ops.zeit.de:3128
60 changes: 60 additions & 0 deletions smoketest/test_publisher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from datetime import datetime, timezone
from time import sleep

import pytest


def test_publisher_invalidates_fastly(vivi, http):
article = '/data/nightwatch-publish.txt'

expected = datetime.now().isoformat()

vivi.put(article, expected)
vivi.publish(article)

# vivi runs the publisher asynchronously from the API call.
timeout = 60
for i in range(timeout):
sleep(1)
r = http(article)
current = r.text.strip()
if current == expected:
break
else:
pytest.fail('Expected %s, got %s after %s seconds' % (expected, current, timeout))


@pytest.mark.parametrize(
'content',
[
'/wirtschaft/2010-01/automarkt-usa-deutschland-smart',
'/2010/01/index',
],
)
def test_publisher_updates_metadata(vivi, http, config, content):
before = datetime.now(timezone.utc)
sleep(1)

vivi.publish(content)

# vivi runs the publisher asynchronously from the API call.
timeout = 60
for i in range(timeout):
sleep(1)
r = http(
config['elasticsearch'],
json={
'query': {'bool': {'filter': [{'term': {'url': content}}]}},
'_source': ['payload.workflow.date_last_published'],
},
)
try:
hit = r.json()['hits']['hits'][0]['_source']
current = hit['payload']['workflow']['date_last_published']
current = datetime.fromisoformat(current)
except Exception:
current = datetime.min
if current > before:
break
else:
pytest.fail('%s did not increase after %s seconds' % (current, timeout))