diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..81fe7bc --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,112 @@ +name: Deploy + +on: + push: + branches: [master] + +env: + GCP_REGION: us-central1 + AR_REPO: checkmein + IMAGE: checkmein + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + + steps: + - uses: actions/checkout@v4 + + - name: Authenticate to GCP + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} + + - name: Set up gcloud + uses: google-github-actions/setup-gcloud@v2 + + - name: Configure Docker for Artifact Registry + run: gcloud auth configure-docker ${{ env.GCP_REGION }}-docker.pkg.dev --quiet + + - name: Build and push image + run: | + IMAGE_TAG=${{ env.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/${{ env.AR_REPO }}/${{ env.IMAGE }} + docker build -t $IMAGE_TAG:${{ github.sha }} -t $IMAGE_TAG:latest . + docker push $IMAGE_TAG:${{ github.sha }} + docker push $IMAGE_TAG:latest + + deploy-staging: + needs: build + runs-on: ubuntu-latest + environment: staging # GCE_INSTANCE and GCE_ZONE set as environment variables in GitHub + permissions: + contents: read + id-token: write + + steps: + - name: Authenticate to GCP + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} + + - name: Set up gcloud + uses: google-github-actions/setup-gcloud@v2 + + - name: Deploy to staging + run: | + IMAGE_TAG=${{ env.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/${{ env.AR_REPO }}/${{ env.IMAGE }}:${{ github.sha }} + gcloud compute ssh ${{ vars.GCE_INSTANCE }} \ + --zone=${{ vars.GCE_ZONE }} \ + --tunnel-through-iap \ + --command=" + docker pull $IMAGE_TAG && \ + docker stop checkmein || true && \ + docker rm checkmein || true && \ + docker run -d --name checkmein \ + --restart always \ + -v /mnt/checkmein-data/db:/app/data \ + -v /mnt/checkmein-data/sessions:/app/sessions \ + -p 8447:8447 \ + $IMAGE_TAG + " + + deploy-prod: + needs: deploy-staging + runs-on: ubuntu-latest + environment: production # GCE_INSTANCE and GCE_ZONE set as environment variables in GitHub + # Configure required reviewers in the production environment settings + permissions: + contents: read + id-token: write + + steps: + - name: Authenticate to GCP + uses: google-github-actions/auth@v2 + with: + workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }} + + - name: Set up gcloud + uses: google-github-actions/setup-gcloud@v2 + + - name: Deploy to production + run: | + IMAGE_TAG=${{ env.GCP_REGION }}-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/${{ env.AR_REPO }}/${{ env.IMAGE }}:${{ github.sha }} + gcloud compute ssh ${{ vars.GCE_INSTANCE }} \ + --zone=${{ vars.GCE_ZONE }} \ + --tunnel-through-iap \ + --command=" + docker pull $IMAGE_TAG && \ + docker stop checkmein || true && \ + docker rm checkmein || true && \ + docker run -d --name checkmein \ + --restart always \ + -v /mnt/checkmein-data/db:/app/data \ + -v /mnt/checkmein-data/sessions:/app/sessions \ + -p 8447:8447 \ + $IMAGE_TAG + " diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..cb309e9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,26 @@ +name: Test + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + cache: pip + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Run tests + env: + PYTHONPATH: ${{ github.workspace }}:${{ github.workspace }}/tests + run: | + mkdir -p testData + pytest tests/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b0a2461 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.10-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +# These directories are mount points for the persistent disk at runtime +RUN mkdir -p data sessions + +EXPOSE 8447 + +CMD ["python", "checkMeIn.py", "container.conf"] diff --git a/container.conf b/container.conf new file mode 100644 index 0000000..5eed9cc --- /dev/null +++ b/container.conf @@ -0,0 +1,30 @@ +[global] +# Bind to all interfaces so Caddy (separate container) can reach us +server.socket_host : '0.0.0.0' +server.socket_port : 8447 +database.path : 'data/' +database.name : 'checkMeIn.db' +sync.token : 'robotsarecool' +bigquery.credentials : '' +bigquery.project : '' +bigquery.dataset : 'checkmein' + +[/] +tools.staticdir.root : os.path.abspath(os.getcwd()) +tools.sessions.on : True +tools.sessions.storage_class : cherrypy.lib.sessions.FileSession +tools.sessions.storage_path : os.path.join(os.getcwd(), 'sessions') +tools.sessions.timeout : 60 * 24 * 365 +tools.sessions.httponly : True + +[/favicon.ico] +tools.staticfile.on : True +tools.staticfile.filename : os.path.join(os.getcwd(), 'static/favicon.ico') + +[/robots.txt] +tools.staticfile.on : True +tools.staticfile.filename : os.path.join(os.getcwd(), 'static/robots.txt') + +[/static] +tools.staticdir.on : True +tools.staticdir.dir : 'static' diff --git a/tests/CPtest.py b/tests/CPtest.py index ab9045c..6a181be 100644 --- a/tests/CPtest.py +++ b/tests/CPtest.py @@ -1,3 +1,4 @@ +import contextlib from unittest.mock import patch import cherrypy @@ -5,6 +6,7 @@ from cherrypy.lib.sessions import RamSession from checkMeIn import CheckMeIn +from webBase import Cookie class CPTest(helper.CPWebCase): @@ -31,4 +33,7 @@ def patch_session(self, username='admin', barcode='100091', role=0xFF): sess_mock['username'] = username sess_mock['barcode'] = barcode sess_mock['role'] = role - return patch('cherrypy.session', sess_mock, create=True) + stack = contextlib.ExitStack() + stack.enter_context(patch('cherrypy.session', sess_mock, create=True)) + stack.enter_context(patch.object(Cookie, '_session_exists', return_value=True)) + return stack diff --git a/tests/admin_test.py b/tests/admin_test.py index fee0223..cb18fa5 100644 --- a/tests/admin_test.py +++ b/tests/admin_test.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + import CPtest @@ -61,7 +63,7 @@ def test_users(self): self.assertStatus('200 OK') def test_changeAccess(self): - with self.patch_session(): + with self.patch_session(), patch('webAdminStation.sync_roles'): self.getPage( "/admin/changeAccess?barcode=100091&admin=1&keyholder=1") self.assertStatus('303 See Other') @@ -76,8 +78,9 @@ def test_emptyBuilding(self): self.getPage("/admin/emptyBuilding") def test_addUser(self): - with self.patch_session(): + with self.patch_session(), patch('webAdminStation.sync_roles'): self.getPage("/admin/addUser?user=Fred&barcode=100093") + self.assertStatus('200 OK') def test_addUserDuplicate(self): with self.patch_session(): diff --git a/webBase.py b/webBase.py index b6f2278..9a28fc1 100644 --- a/webBase.py +++ b/webBase.py @@ -7,14 +7,14 @@ class Cookie(object): def __init__(self, name): self.name = name - def get(self, default=''): - result = default - result = cherrypy.session.get(self.name) - if not result: - self.set(default) - result = default + def _session_exists(self): + session_name = cherrypy.config.get('tools.sessions.name', 'session_id') + return session_name in cherrypy.request.cookie - return result + def get(self, default=''): + if not self._session_exists(): + return default + return cherrypy.session.get(self.name, default) def set(self, value): cherrypy.session[self.name] = value