Compare commits

...

64 Commits

Author SHA1 Message Date
Loïc Dachary eaff09efd4 Merge pull request 'do not rename past migrations' (#73) from dachary/dashboard:wip-forgejo into master
ci/woodpecker/push/woodpecker Pipeline failed Details
Reviewed-on: #73
2023-01-09 12:59:42 -05:00
Loïc Dachary d07a644552
upgrade greenlet 1.1.3.post0
ci/woodpecker/pr/woodpecker Pipeline failed Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-01-09 18:51:22 +01:00
Loïc Dachary 93c38fc1f9
Forgejo is codeberg.org/forgejo/forgejo:1.18.0-1
ci/woodpecker/pr/woodpecker Pipeline failed Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-01-09 18:39:35 +01:00
Loïc Dachary 788e025b98
do not rename past migrations
ci/woodpecker/pr/woodpecker Pipeline failed Details
it is not possible to go back in time
2023-01-09 18:24:10 +01:00
Loïc Dachary 1c165fcea3
upgrade gitea to forgejo
ci/woodpecker/pr/woodpecker Pipeline failed Details
ci/woodpecker/push/woodpecker Pipeline failed Details
Signed-off-by: Loïc Dachary <loic@dachary.org>
2022-12-23 22:34:38 +01:00
Aravinth Manivannan 2e36a186ee Merge pull request 'convert instance names to lowercase' (#70) from dachary/dashboard:wip-lowercase into master
ci/woodpecker/push/woodpecker Pipeline failed Details
Reviewed-on: https://gitea.gna.org/Hostea/dashboard/pulls/70
2022-10-04 04:39:04 -04:00
Loïc Dachary ad925cddfc
convert instance names to lowercase
ci/woodpecker/pr/woodpecker Pipeline was successful Details
Fixes: https://gitea.gna.org/Hostea/dashboard/issues/69

Signed-off-by: Loïc Dachary <loic@dachary.org>
2022-10-04 09:14:50 +02:00
Loïc Dachary 2725b9b1f6
avoid crash in CI when STRIPE is not set
Signed-off-by: Loïc Dachary <loic@dachary.org>
2022-10-04 09:14:50 +02:00
Loïc Dachary 0f1003dbe8 Merge pull request 'fix: receipt email subject' (#68) from fix-email-typo into master
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: https://gitea.gna.org/Hostea/dashboard/pulls/68
2022-10-01 12:01:08 -04:00
Aravinth Manivannan 187d22118f Merge pull request 'do not pin Gitea version' (#67) from dachary/dashboard:wip-gitea into master
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: https://gitea.gna.org/Hostea/dashboard/pulls/67
2022-10-01 07:55:12 -04:00
Hostea dashboard 9b9fb8362d
fix: receipt email subject
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline failed Details
2022-09-30 18:36:56 +05:30
Loïc Dachary 498b95848e
do not pin Gitea version
ci/woodpecker/pr/woodpecker Pipeline failed Details
So that it is upgraded when Enough is upgraded.

Signed-off-by: Loïc Dachary <loic@dachary.org>
2022-09-30 14:54:50 +02:00
Loïc Dachary b5afc49f5d Merge pull request 'notify staff when new instance is deployed' (#61) from alert-staff-new-instance into master
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: https://gitea.gna.org/Hostea/dashboard/pulls/61
2022-09-14 02:48:21 -04:00
Hostea dashboard 86fe9a2c59
feat: alert staff on new instance creation
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline failed Details
closes: https://gitea.gna.org/Hostea/dashboard/issues/52
2022-09-13 19:57:37 +05:30
Hostea dashboard 9d2a53f2e2
fix: more email project renaming 2022-09-13 19:57:26 +05:30
Loïc Dachary c7def47215 Merge pull request 'fix: don't send pre-payment notification email' (#56) from fix-rm-not-invoice into master
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: https://gitea.gna.org/Hostea/dashboard/pulls/56
2022-09-12 15:48:14 -04:00
Hostea dashboard 9f5032bd28
feat: send subscription renewal notification emails
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline failed Details
2022-09-12 21:01:24 +05:30
Hostea dashboard 669a22a004
fix: don't send pre-payment notification email
closes: https://gitea.gna.org/Hostea/dashboard/issues/49
2022-09-12 21:01:21 +05:30
Loïc Dachary 0ab82fe0b4 Merge pull request 'fix: rename Gna! in emails' (#60) from fix-rename-gna-in-emails into master
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: https://gitea.gna.org/Hostea/dashboard/pulls/60
2022-09-12 08:36:17 -04:00
Hostea dashboard 0e0b8db940
fix: test for Gna! in emails
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline failed Details
2022-09-12 17:36:24 +05:30
Hostea dashboard ac0775e075
fix: rename to Gna! in email templates 2022-09-12 17:36:04 +05:30
Hostea dashboard 51e7ea4d5e
fix: rename Gna! in emails
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/pr/woodpecker Pipeline failed Details
2022-09-12 16:36:29 +05:30
Loïc Dachary 98973301c3 Merge pull request 'fix: don't send emails to VMs that were requested but not created' (#54) from fix-dead-instance-invoices into master
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: https://gitea.gna.org/Hostea/dashboard/pulls/54
2022-09-11 19:07:41 -04:00
Hostea dashboard 3ebc518e1f
fix: don't send emails to VMs that were requested but not created
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline failed Details
SUMMARY
    dash.models.Instance is created upon request and
    infrastructure.models.InstanceCreated when the instance is created.
    Using data from InstanceCreated to send invoices should solve this
    issue.
2022-09-12 04:05:41 +05:30
Aravinth Manivannan 25db390e4d Merge pull request 'Rename hostea to gna!' (#59) from wip-rename-hostea-to-gna into master
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: https://gitea.gna.org/Hostea/dashboard/pulls/59
2022-09-11 18:25:58 -04:00
Hostea dashboard 6365b84d45
feat: run collectstatic before launching development server to update static assets
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/pr/woodpecker Pipeline failed Details
2022-09-12 03:53:19 +05:30
Hostea dashboard 41ca63244e
feat: update name and web links to Gna! 2022-09-12 03:53:19 +05:30
Hostea dashboard 0e2112e30f
feat: change hostea logo to gna! 2022-09-12 03:53:19 +05:30
Loïc Dachary 5b7cf06c7a Merge pull request 'fix: instance_names in tests must be alphanumeric and be < 20 chars' (#58) from fix-hostname-validation into master
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: https://gitea.gna.org/Hostea/dashboard/pulls/58
2022-09-11 18:13:12 -04:00
Hostea dashboard 1792713952
fix: instance_names in tests must be alphanumeric and be < 20 chars
ci/woodpecker/pr/woodpecker Pipeline failed Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
closes: https://gitea.gna.org/Hostea/dashboard/issues/57
2022-09-12 03:33:20 +05:30
Loïc Dachary 3019d9d739 Merge pull request 'fix: add hostname validation rules' (#55) from fix-hostname-validation into master
ci/woodpecker/push/woodpecker Pipeline failed Details
Reviewed-on: https://gitea.gna.org/Hostea/dashboard/pulls/55
2022-09-11 16:11:58 -04:00
Hostea dashboard bcb2e26f61
feat: test vm name validation 2022-09-04 18:04:37 +05:30
Hostea dashboard 011fb4816f
fix: validate VM names
fixes: https://gitea.gna.org/Hostea/dashboard/issues/51
2022-09-04 17:49:53 +05:30
Hostea dashboard 6c31555a52
feat: password reset workflow
ci/woodpecker/push/woodpecker Pipeline was successful Details
fixes: https://gitea.hostea.org/Hostea/support/issues/2
2022-07-10 12:42:16 +05:30
Hostea dashboard 060e9b84d4
fix: update password reset link 2022-07-10 12:40:00 +05:30
Hostea dashboard 418bb7dec0
fix: tests rely on a string from login page. Login page update reflects
ci/woodpecker/push/woodpecker Pipeline was successful Details
in tests
2022-07-10 12:39:33 +05:30
Loïc Dachary c4375a43b2 Merge pull request 'Update the page content' (#48) from dachary/dashboard:wip-site into master
ci/woodpecker/push/woodpecker Pipeline failed Details
Reviewed-on: https://gitea.hostea.org/Hostea/dashboard/pulls/48
2022-07-10 02:43:43 -04:00
Loïc Dachary 705c3a282b
copy/paste the hostea.org home page content
ci/woodpecker/pr/woodpecker Pipeline failed Details
So they are consistent with each other.

Fixes: #47

Signed-off-by: Loïc Dachary <loic@dachary.org>
2022-07-10 08:42:38 +02:00
Loïc Dachary cfd5518518
Add link to https://hostea.org
So that the user can conveniently go back to hostea.org without
editing with the URL manually.

Signed-off-by: Loïc Dachary <loic@dachary.org>
2022-07-10 08:20:24 +02:00
Aravinth Manivannan a95158f3df
fix: Gite credentials email: fix subject and let user know instance is
ci/woodpecker/push/woodpecker Pipeline was successful Details
being provisioned

fixes: https://gitea.hostea.org/Hostea/dashboard/issues/44
2022-07-09 10:54:45 +05:30
Aravinth Manivannan b12cc044da
fix: Invoice generation must not consider deleted VMs' names for
ci/woodpecker/push/woodpecker Pipeline was successful Details
checking if payment is already fulfilled

DESCRIPTION
    Invoice generation is dependent on instance_name. Deleting a VM
    doesn't delete the corresponding payments record since payment
    receipts should be preserved for accounting purposes.

    But being heavily dependent on instance_name, without taking deleted
    VMs into account produces incorrect behavior under certain
    circumstances: if a VM named 'foo' is paid for and is deleted before
    its billing cycle is competed and a new VM is created with the same
    name, either by the same user or a different user, invoice won't be
    generated for the new VM since a payment record already exists for
    that billing cycle for the VM named 'foo'.

    Marking deleted VMs' payment records unsuitable for checking if a VM
    is already paid for will result in correct behavior.

fixes: https://gitea.hostea.org/Hostea/dashboard/issues/38
2022-07-08 22:28:39 +05:30
Aravinth Manivannan cc12d1a77d
fix: hard-code CI_COMMIT_AUTHOR_* details to avoid failures in PR builds
ci/woodpecker/push/woodpecker Pipeline was successful Details
ref: https://gitea.hostea.org/Hostea/dashboard/pulls/42
2022-07-08 19:51:06 +05:30
Aravinth Manivannan cb6bce0c44 Merge pull request 'git config before push' (#43) from dachary/dashboard:wip-config-3 into master
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: https://gitea.hostea.org/Hostea/dashboard/pulls/43
2022-07-08 10:18:00 -04:00
Aravinth Manivannan bebf18946a
fix: re-enable billing app tests
ci/woodpecker/push/woodpecker Pipeline was successful Details
Commented out for debugging, forgot to enable(!!)
2022-07-08 19:31:06 +05:30
Loïc Dachary 6e84746a2c
git config before push
ci/woodpecker/pr/woodpecker Pipeline failed Details
2022-07-08 15:47:53 +02:00
Aravinth Manivannan 2c8a5909cb
fix: generate absolute URI when attaching links in invoice and payment
ci/woodpecker/push/woodpecker Pipeline was successful Details
notification

fixes: https://gitea.hostea.org/Hostea/dashboard/issues/37
2022-07-08 18:51:04 +05:30
Aravinth Manivannan ce0498b013
fix: add instance link in primary nav bar points to right page 2022-07-08 18:43:04 +05:30
Aravinth Manivannan 809322d245
feat: docs: generate_invoice management cmd
ci/woodpecker/push/woodpecker Pipeline was successful Details
fixes: https://gitea.hostea.org/Hostea/dashboard/issues/29
2022-07-08 01:51:22 +05:30
Aravinth Manivannan 2ee54a71e3
feat: management command to periodically generate invoices
ci/woodpecker/push/woodpecker Pipeline was successful Details
SUMMARY
    `python manage.py generate_invoice` generates invoices for VMs when
    it enters a new billing cycle and sends a notification email to
    VM owners.

    This command should be run as frequently as desirable. Running daily
    is recommended.

BILLING CYCLE
    By default, a billing cycle is 30 days.
2022-07-08 01:46:17 +05:30
Aravinth Manivannan 438e34f7d6
chore: refactor invoice generation into a util fn 2022-07-08 00:51:11 +05:30
Aravinth Manivannan 9c239ad78b
feat: send invoice generated notification email and payments receipt mail
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-07-07 20:51:33 +05:30
Aravinth Manivannan 147eead388 Merge pull request 'clone instead of fetch' (#19) from dachary/dashboard:wip-pull into master
ci/woodpecker/push/woodpecker Pipeline was successful Details
Reviewed-on: https://gitea.hostea.org/Hostea/dashboard/pulls/19
2022-07-07 04:42:46 -04:00
Aravinth Manivannan f2f2fadae4
feat: use whitenoise for static file in development too for uniform Behavior
ci/woodpecker/push/woodpecker Pipeline was successful Details
> it opens up the possibility for differences in behaviour between development and production environments. For this reason it’s a good idea to use WhiteNoise in development as well.

source: http://whitenoise.evans.io/en/stable/django.html#using-whitenoise-in-development
2022-07-07 13:59:06 +05:30
Aravinth Manivannan 5f6c3c459e
chore: mv common static files to common-static 2022-07-07 13:58:53 +05:30
Loïc Dachary 8d02fe107f
always clone the fleet repository
ci/woodpecker/pr/woodpecker Pipeline failed Details
It is small and not worth the trouble of dealing with fetch/pull

Signed-off-by: Loïc Dachary <loic@dachary.org>
2022-07-07 10:05:18 +02:00
Aravinth Manivannan b4183c1790
feat & fix: install and configure whitenoise to serve static files in
ci/woodpecker/push/woodpecker Pipeline failed Details
prod

fixes: https://gitea.hostea.org/Hostea/dashboard/issues/24
2022-07-07 10:59:23 +05:30
Aravinth Manivannan 22abe08f68
feat: mv static files to dash/static/dash and migrate load static template tags 2022-07-07 10:41:22 +05:30
Aravinth Manivannan 4c51eb77b0 chore: rm gitea/app.in
ci/woodpecker/push/woodpecker Pipeline was successful Details
SUMMARY
    This file was used to spawn a Gitea instance from binary for
    integration testing. That strategy was abandoned long ago but this
    file wasn't cleaned up.

fixes: https://gitea.hostea.org/Hostea/dashboard/issues/34
2022-07-05 01:59:26 -04:00
Aravinth Manivannan 9303ea59ed fix: s/EMAIL_SENDER_ADDRESS/DEFAULT_FROM_EMAIL/
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-07-04 12:42:02 -04:00
Aravinth Manivannan eb68b1e984 feat: Gitea root creds email with nicer content 2022-07-04 12:42:02 -04:00
Aravinth Manivannan 5e5ce02759 feat: instance created notification email template with nicer body 2022-07-04 12:42:02 -04:00
Aravinth Manivannan 63f4f987a9 feat: verification link email template with polished email body 2022-07-04 12:42:02 -04:00
Aravinth Manivannan 4a1c0a5cdc feat: add EMAIL_SENDER_ADDRESS to settings.py 2022-07-04 12:42:02 -04:00
Aravinth Manivannan 280807d96c
fix: rm dummy section
ci/woodpecker/pr/woodpecker Pipeline failed Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
NOTE
    The dummy section is commented out for future reference.

fixes: https://gitea.hostea.org/Hostea/dashboard/issues/27
2022-07-04 13:55:28 +05:30
129 changed files with 1601 additions and 667 deletions

View File

@ -3,5 +3,5 @@ export db=""
export STRIPE_SECRET_KEY=""
export STRIPE_PUBLIC_KEY=""
export HOSTEA_INFRA_HOSTEA_REPO_REMOTE="ssh://git@localhost:22/hostea/fleet.git"
export HOSTEA_META_GITEA_INSTANCE="http://localhost:3000"
export HOSTEA_META_FORGEJO_INSTANCE="http://localhost:3000"
export HOSTEA_INFRA_HOSTEA_REPO_SSH_KEY="$(realpath ./tests/fleet-deploy-key)"

1
.gitignore vendored
View File

@ -154,3 +154,4 @@ cython_debug/
keys
htmlcov/
tmp/
static/

View File

@ -4,8 +4,8 @@ pipeline:
environment:
- DATABSE_URL=postgres://postgres:password@database:5432/postgres
- EMAIL_URL=smtp://admin:password@smtp:10025
- HOSTEA_INFRA_HOSTEA_REPO_REMOTE=ssh://git@gitea:22/hostea/
- HOSTEA_META_GITEA_INSTANCE=http://gitea:3000
- HOSTEA_INFRA_HOSTEA_REPO_REMOTE=ssh://git@forgejo:22/hostea/
- HOSTEA_META_FORGEJO_INSTANCE=http://forgejo:3000
commands:
- export HOSTEA_INFRA_HOSTEA_REPO_SSH_KEY="$(realpath ./tests/fleet-deploy-key)"
- pip install virtualenv
@ -21,9 +21,9 @@ services:
environment:
- POSTGRES_PASSWORD=password
gitea:
image: gitea/gitea:1.16.5
container_name: hostea-dash-gitea
forgejo:
image: codeberg.org/forgejo/forgejo:1.18.0-1
container_name: hostea-dash-forgejo
smtp:
image: maildev/maildev:latest

View File

@ -1,6 +1,6 @@
FROM python
LABEL org.opencontainers.image.source https://gitea.hostea.org/Hostea/dashboard
LABEL org.opencontainers.image.source https://forgejo.hostea.org/Hostea/dashboard
RUN useradd -ms /bin/bash -u 1001 hostea
RUN apt-get update && apt-get install -y ca-certificates git

View File

@ -9,6 +9,7 @@ endef
default: ## Run app
$(call run_migrations)
. ./venv/bin/activate && yes yes | python manage.py collectstatic
. ./venv/bin/activate && python manage.py runserver
coverage: ## Generate test coverage report
@ -26,6 +27,7 @@ env: ## Install all dependencies
@-virtualenv venv
. ./venv/bin/activate && pip install -r requirements.txt
. ./venv/bin/activate && ./integration/ci.sh init
. ./venv/bin/activate && yes yes | python manage.py collectstatic
freeze: ## Freeze python dependencies
@. ./venv/bin/activate && pip freeze > requirements.txt
@ -37,13 +39,7 @@ integration-test: ## run integration tests
. ./venv/bin/activate && integration/tests.sh
lint: ## Run linter
@./venv/bin/black ./dashboard/
@./venv/bin/black ./accounts/
@./venv/bin/black ./dash/
@./venv/bin/black ./support/
@./venv/bin/black ./billing/
@./venv/bin/black ./infrastructure/
@./venv/bin/black ./integration/
@./venv/bin/black dashboard accounts dash support billing infrastructure integration
migrate: ## Run migrations
$(call run_migrations)

View File

@ -2,7 +2,7 @@
# Hostea dashboard
[![status-badge](https://woodpecker.hostea.org/api/badges/Hostea/dashboard/status.svg)](https://woodpecker.hostea.org/Hostea/dashboard)
[![status-badge](https://woodpecker.gna.org/api/badges/Hostea/dashboard/status.svg)](https://woodpecker.gna.org/Hostea/dashboard)
[![AGPL License](https://img.shields.io/badge/license-AGPL-blue.svg?style=flat-square)](http://www.gnu.org/licenses/agpl-3.0)
[![Chat](https://img.shields.io/badge/matrix-hostea:matrix.batsense.net-purple?style=flat-square)](https://matrix.to/#/#hostea:matrix.batsense.net)

View File

@ -0,0 +1,52 @@
# Generated by Django 4.0.3 on 2022-07-10 06:14
import accounts.utils
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("accounts", "0005_accountconfirmchallenge_created_at"),
]
operations = [
migrations.CreateModel(
name="PasswordResetChallenge",
fields=[
(
"public_ref",
models.CharField(
default=accounts.utils.gen_secret,
editable=False,
max_length=32,
unique=True,
verbose_name="Public referece to challenge text",
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
(
"challenge_text",
models.CharField(
default=accounts.utils.gen_secret,
editable=False,
max_length=32,
primary_key=True,
serialize=False,
unique=True,
verbose_name="Challenge text",
),
),
(
"owned_by",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
),
),
],
),
]

View File

@ -52,3 +52,36 @@ class AccountConfirmChallenge(models.Model):
def pending_url(self):
return reverse("accounts.verify.pending", args=(self.public_ref,))
class PasswordResetChallenge(models.Model):
owned_by = models.OneToOneField(User, on_delete=models.CASCADE)
public_ref = models.CharField(
"Public referece to challenge text",
unique=True,
max_length=32,
default=gen_secret,
editable=False,
)
created_at = models.DateTimeField(auto_now_add=True, blank=True)
challenge_text = models.CharField(
"Challenge text",
unique=True,
max_length=32,
default=gen_secret,
editable=False,
primary_key=True,
)
def __str__(self):
return f"{self.challenge_text}"
def verification_link(self):
"""
Get verification link
"""
return reverse("accounts.password.reset", args=(self.challenge_text,))
def pending_url(self):
return reverse("accounts.password.reset.resend", args=(self.public_ref,))

View File

@ -1,24 +1,23 @@
{% extends "common/components/base.html" %}
{% block title %}{% block title_name %} {% endblock %} | Hostea Dashbaord{% endblock %}
{% block title %}{% block title_name %} {% endblock %} | Gna! Dashboard{% endblock %}
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
{% block main %}
<main class="auth__main">
<section class="main">
<div class="title">
<h1>Free Forge Ecosystem for Free Developers</h1>
<h1><a href="https://forgejo.org">Forgejo</a> hosting and <a href="/forgejo-clinic/">service</a></h1>
<p class="welcome">
Hostea is a self-hostable libre software development suite comprising Gitea and Woodpecker CI with payments integration.
A free forge ecosystem for free developers.
</p>
<ul class="index-banner__features-list">
<li class="index-banner__features">Fully managed</li>
<li class="index-banner__features">100% Free Software</li>
<li class="index-banner__features">Fully Self-Hostable</li>
<li class="index-banner__features">Observable and reliable</li>
<li class="index-banner__features">Federation when available</li>
<li class="index-banner__features">Radically transparent</li>
<li class="index-banner__features">Horizontal community</li>
<li class="index-banner__features">Run Hostea and become a service provider!</li>
<li class="index-banner__features">Dedicated <a href="https://hosteadashboard.gna.org/register/">Forgejo hosting</a> and <a href="https://woodpecker-ci.org/">Woodpecker CI</a> from 10€/month</li>
<li class="index-banner__features">Clinic to <a href="https://gna.org/forgejo-clinic/">heal sick Forgejo</a> instances</li>
<li class="index-banner__features">100% <a href="https://www.gnu.org/philosophy/free-sw.html">Free Software</a></li>
<li class="index-banner__features">Radically <a href="https://forum.gna.org/t/about-governance-and-decisions-in-hostea/55">Transparent</a></li>
<li class="index-banner__features">Run by a <a href="https://forum.gna.org/t/about-governance-and-decisions-in-hostea/55">horizontal collective</a></li>
<li class="index-banner__features">25% of the income <a href="https://forum.gna.org/t/decision-revenue-sharing-model/92">dedicated to sustain Free Software dependencies</a></li>
<li class="index-banner__features">Committed to <a href="https://forgefriends.org/blog/2022/06/30/2022-06-state-forge-federation/">further forge federation</a></li>
</ul>
</div>
</section>

View File

@ -34,12 +34,12 @@
/>
</label>
<div class="form__action-container">
<a href="/forgot-password">Forgot password?</a>
<a href="{% url 'accounts.password.reset.new' %}">Forgot password?</a>
<button class="form__submit" type="submit">Login</button>
</div>
</form>
<p class="form__alt-action">
New to Hostea?
New to Gna!?
<a href="{% url 'accounts.register' %}">Create an account</a>
</p>
{% endblock %}

View File

@ -0,0 +1,28 @@
{% extends "common/components/base.html" %}
{% block title %} Reset Password| Gna! Dashboard{% endblock %}
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
{% block main %}
<div class="dialogue-box__container">
<h2>Reset password</h2>
<form
action="{% url 'accounts.password.reset.new' %}"
method="POST"
class="form"
accept-charset="utf-8"
>
{% include "common/components/error.html" %} {% csrf_token %}
<label class="form__label" for="email">
Email
<input
class="form__input"
name="email"
id="email"
type="email"
/>
</label>
<div class="form__action-container">
<button class="form__submit" type="submit">Send Password Reset Link</button>
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "common/components/base.html" %}
{% block title %} Reset Password | Gna! Dashboard{% endblock %}
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
{% block main %}
<div class="dialogue-box__container">
<h2>Reset password</h2>
<p>Verification link is sent to email address: {{email}}</p>
<form
action="{% url 'accounts.password.reset.resend' public_ref=public_ref %}"
method="POST"
class="form"
accept-charset="utf-8"
>
{% include "common/components/error.html" %} {% csrf_token %}
<div class="form__action-container">
<button class="form__submit" type="submit">Click here to resend email</button>
</div>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,40 @@
{% extends "common/components/base.html" %}
{% block title %} Reset Password | Gna! Dashboard{% endblock %}
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
{% block main %}
<div class="dialogue-box__container">
<h2>Reset Password</h2>
<form
action="{% url 'accounts.password.reset' challenge=challenge %}"
method="POST"
class="form"
accept-charset="utf-8"
>
{% include "common/components/error.html" %} {% csrf_token %}
<label class="form__label" for="password">
password
<input
class="form__input"
name="password"
required
id="password"
type="password"
/>
</label>
<label class="form__label" for="confirm_password">
Re-enter Password
<input
class="form__input"
name="confirm_password"
required
id="confirm_password"
type="password"
/>
</label>
<div class="form__action-container">
<button class="form__submit" type="submit">Reset Password</button>
</div>
</form>
</div>
{% endblock %}

View File

@ -1,5 +1,5 @@
{% extends "common/components/base.html" %}
{% block title %} Confirm Access | Hostea Dashbaord{% endblock %}
{% block title %} Confirm Access | Gna! Dashboard{% endblock %}
{% block nav %} {% include "dash/common/components/primary-nav.html" %} {% endblock %}
{% block main %}

View File

@ -1,5 +1,5 @@
{% extends "common/components/base.html" %}
{% block title %} Confirm Account | Hostea Dashbaord{% endblock %}
{% block title %} Confirm Account | Gna! Dashboard{% endblock %}
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
{% block main %}
<div class="dialogue-box__container">

View File

@ -1,5 +1,5 @@
{% extends "common/components/base.html" %}
{% block title %} Confirm Account | Hostea Dashbaord{% endblock %}
{% block title %} Confirm Account | Gna! Dashboard{% endblock %}
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
{% block main %}
<div class="dialogue-box__container">

View File

@ -0,0 +1,14 @@
Hello {{ username }},
You have a new password!
Your password for signing in to Gna! was recently changed. If you made this change, then we're all set.
If you did not make this change, please reset your password to secure your account.
{% url 'accounts.password.reset.new' %}
Either way, feel free to reach out with any questions you might have. We're here to help.
Cheers,
Gna! team

View File

@ -0,0 +1,9 @@
Hello {{ email }},
Please click on the link below to reset your password:
{{ link }}
If you don't recognise this activity, please delete this mail.
Cheers,
Gna! team

View File

@ -0,0 +1,9 @@
Hello {{ username }},
Please click on the link below to verify your email.
{{ link }}
If you don't recognise this activity, please delete this mail.
Cheers,
Gna! team

View File

@ -10,21 +10,11 @@
title="RSS"
>Home</a>
<span class="footer__column-divider--mobile-visible">|</span>
<a class="license__link" rel="noreferrer" href="https://hostea.org/about" target="_blank"
<a class="license__link" rel="noreferrer" href="https://gna.org/about" target="_blank"
>&nbsp; About</a
>
</span>
</div>
<div class="footer__column">
<a
class="license__link"
rel="noreferrer"
href="https://www.eff.org/issues/do-not-track/amp/"
target="_blank"
>No AMP</a
>
<div class="footer__column-divider">|</div>
<a href="mailto:{{ footer.admin_email }}" class="footer__link"
>Contact Instance Maintainer</a
>

View File

@ -3,11 +3,8 @@
<input type="checkbox" class="nav__toggle" id="nav__toggle" />
<div class="nav__header">
<a class="nav__logo-container" href="/">
<img src="{% static 'img/android-icon-48x48.png' %}"
alt="Hostea temporary logo"/>
<p class="nav__home-btn">
ostea
</p>
<img class="nav__logo-img" src="{% static 'img/logo.png' %}"
alt="Gna! logo"/>
</a>
<label class="nav__hamburger-menu" for="nav__toggle">
<span class="nav__hamburger-inner"></span>

View File

@ -33,7 +33,7 @@ from django.conf import settings
from oauth2_provider.models import get_application_model
from .models import AccountConfirmChallenge
from .models import AccountConfirmChallenge, PasswordResetChallenge
from .management.commands.rm_unverified_users import (
Command as CleanUnverifiedUsersCommand,
)
@ -77,7 +77,9 @@ class LoginTest(TestCase):
Tests if login template renders
"""
resp = self.client.get(reverse("accounts.login"))
self.assertEqual(b"Free Forge Ecosystem" in resp.content, True)
self.assertEqual(
b"A free forge ecosystem for free developers" in resp.content, True
)
def test_login_works(self):
"""
@ -158,6 +160,71 @@ class LoginTest(TestCase):
self.assertEqual(resp.headers["location"], reverse("dash.instances.list"))
class ResetPasswordTest(TestCase):
def setUp(self):
self.username = "reset_password_user"
register_util(t=self, username=self.username)
def reset_password(self):
c = Client()
payload = {
"email": self.email,
}
resp = c.get(reverse("accounts.password.reset.new"))
self.assertEqual(resp.status_code == 200)
resp = c.post(reverse("accounts.password.reset.new"), payload)
self.assertEqual(resp.status_code == 302)
challenge = PasswordResetChallenge.objects.filter(owned_by=self.user)
self.assertEqual(resp.headers["location"] == challenge.pending_url(), True)
password_reset_mail = mail.outbox.pop()
self.assertEqual("reset your password" in password_reset_mail, True)
self.assertEqual(challenge.verification_link() in password_reset_mail, True)
resp = c.get(self.challenge.verification_link())
self.assertEqual(resp.status_code == 200)
new_password = "newpasdasdf234234"
# passwords don't match
payload = {
"password": new_password,
"confirm_password": self.password,
}
resp = c.post(self.challenge.verification_link(), payload)
self.assertEqual(resp.status_code == 400)
# change password
payload["confirm_password"] = new_password
resp = c.post(self.challenge.verification_link(), payload)
self.assertEqual(resp.status_code == 302)
self.assertEqual(resp.headers["location"], reverse("accounts.login"))
# verify password changed notification email was sent
password_updated_email = mail.outbox.pop()
self.assertEqual(
"Your password for signing in to Hostea was recently changed. If you made this change, then we're all set."
in password_updated_email,
True,
)
self.assertEqual(reverse("accounts.reset.new") in password_updated_email, True)
# trying to login with old password
payload = {
"login": self.username,
"password": self.password,
}
resp = self.client.post(reverse("accounts.login"), payload)
self.assertEqual(resp.status_code, 401)
self.assertEqual(b"Login Failed" in resp.content, True)
payload["password"] = new_password
resp = c.post(reverse("accounts.login"), payload)
self.assertEqual(resp.status_code, 302)
self.assertEqual(resp.headers["location"], reverse("accounts.home"))
class RegistrationTest(TestCase):
def setUp(self):
self.username = "register_user"
@ -169,7 +236,9 @@ class RegistrationTest(TestCase):
Tests if register template renders
"""
resp = self.client.get(reverse("accounts.register"))
self.assertEqual(b"Free Forge Ecosystem" in resp.content, True)
self.assertEqual(
b"A free forge ecosystem for free developers." in resp.content, True
)
def test_register_works(self):
"""

View File

@ -25,6 +25,9 @@ from .views import (
resend_verification_email_view,
verification_pending_view,
sudo,
password_reset_send_verificaiton_link,
password_resend_verification_link_pending,
reset_password,
)
urlpatterns = [
@ -44,5 +47,20 @@ urlpatterns = [
),
path("accounts/verify/<str:challenge>/", verify_account, name="accounts.verify"),
path("accounts/sudo/", sudo, name="accounts.sudo"),
path(
"accounts/password/reset/challenge/",
password_reset_send_verificaiton_link,
name="accounts.password.reset.new",
),
path(
"accounts/password/reset/<str:challenge>/",
reset_password,
name="accounts.password.reset",
),
path(
"accounts/password/reset/challenge/<str:public_ref>/",
password_resend_verification_link_pending,
name="accounts.password.reset.resend",
),
path("", protected_view, name="accounts.home"),
]

View File

@ -17,6 +17,7 @@ from datetime import datetime, timezone
from django.utils.crypto import get_random_string
from django.core.mail import send_mail
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.http import urlencode
from django.conf import settings
@ -29,17 +30,78 @@ def gen_secret() -> str:
return get_random_string(32)
def send_password_changed_email(request):
ctx = {
"username": request.user.username,
}
body = render_to_string(
"accounts/emails/password-changed.txt",
context=ctx,
)
email = request.user.email
sender = settings.DEFAULT_FROM_EMAIL
send_mail(
subject="[Gna!] Password changed",
message=body,
from_email=f"No reply Gna!<{sender}>",
recipient_list=[email],
)
def send_password_reset_email(request, challenge):
verification_link = (
f"{request.scheme}://{request.get_host()}{challenge.verification_link()}"
)
ctx = {
"link": verification_link,
"email": challenge.owned_by.email,
}
body = render_to_string(
"accounts/emails/password-reset-link.txt",
context=ctx,
)
email = challenge.owned_by.email
sender = settings.DEFAULT_FROM_EMAIL
send_mail(
subject="[Gna!] Password reset link",
message=body,
from_email=f"No reply Gna!<{sender}>",
recipient_list=[email],
)
def send_verification_email(request, challenge):
verification_link = (
f"{request.scheme}://{request.get_host()}{challenge.verification_link()}"
)
ctx = {
"link": verification_link,
"username": challenge.owned_by.username,
}
body = render_to_string(
"accounts/emails/verification-link.txt",
context=ctx,
)
email = challenge.owned_by.email
sender = settings.DEFAULT_FROM_EMAIL
send_mail(
subject="[Hostea] Please confirm your email address",
message=f"Please confirm your email address {email}.\n {verification_link}",
from_email="No reply Hostea<no-reply@exampl.org>", # TODO read from settings.py
subject="[Gna!] Please confirm your email address",
message=body,
from_email=f"No reply Gna!<{sender}>",
recipient_list=[email],
)

View File

@ -25,8 +25,8 @@ from django.urls import reverse
from dash.utils import footer_ctx
from .models import AccountConfirmChallenge
from .utils import send_verification_email, ConfirmAccess
from .models import AccountConfirmChallenge, PasswordResetChallenge
from .utils import send_verification_email, ConfirmAccess, send_password_reset_email
from .decorators import redirect_if_authenticated
@ -242,3 +242,107 @@ def sudo(request):
ConfirmAccess.set(request=request)
return redirect(request.POST["next"])
@redirect_if_authenticated
@csrf_protect
def password_reset_send_verificaiton_link(request):
def default_ctx():
return {
"title": "Reset Password",
"footer": footer_ctx(),
}
if request.method == "GET":
ctx = default_ctx()
return render(request, "accounts/auth/password-reset-form.html", ctx)
challenge = None
email = request.POST["email"]
User = get_user_model()
user = get_object_or_404(User, email=email)
if not PasswordResetChallenge.objects.filter(owned_by=user).exists():
challenge = PasswordResetChallenge(owned_by=user)
challenge.save()
send_password_reset_email(request, challenge=challenge)
else:
challenge = PasswordResetChallenge.objects.get(owned_by=user)
return redirect(challenge.pending_url())
@redirect_if_authenticated
@csrf_protect
def password_resend_verification_link_pending(request, public_ref):
challenge = get_object_or_404(PasswordResetChallenge, public_ref=public_ref)
if request.method == "GET":
ctx = {
"email": challenge.owned_by.email,
"public_ref": challenge.public_ref,
}
return render(
request,
"accounts/auth/password-reset-resend-verification.html",
context=ctx,
)
send_password_reset_email(request, challenge=challenge)
ctx = {
"email": challenge.owned_by.email,
"public_ref": challenge.public_ref,
}
return render(
request, "accounts/auth/password-reset-resend-verification.html", context=ctx
)
@csrf_protect
def reset_password(request, challenge):
def default_ctx(challenge):
return {
"title": "Reset Password",
"footer": footer_ctx(),
"challenge": challenge,
}
challenge = get_object_or_404(PasswordResetChallenge, challenge_text=challenge)
if request.method == "GET":
ctx = default_ctx(challenge=challenge)
return render(request, "accounts/auth/password-reset.html", ctx)
confirm_password = request.POST["confirm_password"]
password = request.POST["password"]
if password != confirm_password:
ctx = default_ctx(challenge=challenge)
ctx["error"] = {
"title": "Reset Password Failed",
"reason": "Passwords don't match, please verify input",
}
return render(
request, "accounts/auth/password-reset.html", status=400, context=ctx
)
user = challenge.owned_by
try:
validate_password(password, user=user)
except ValidationError as err:
ctx = default_ctx(challenge=challenge)
reason = ""
for r in err:
reason += r + " "
ctx["error"] = {"title": "Reset Password Failed", "reason": reason}
return render(
request, "accounts/auth/password-reset.html", status=400, context=ctx
)
user.set_password(password)
user.save()
challenge.delete()
send_password_reset_email(request)
return redirect("accounts.login")

View File

@ -0,0 +1,69 @@
# Copyright © 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.core.management.base import BaseCommand
from django.core.exceptions import ValidationError
from django.conf import settings
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.contrib.auth import get_user_model
from oauth2_provider.models import get_application_model
from oauth2_provider.generators import generate_client_id, generate_client_secret
from accounts.utils import gen_secret
from dash.models import Instance
from infrastructure.models import InstanceCreated
from billing.utils import generate_invoice, payment_fullfilled, get_invoice_link
Application = get_application_model()
class Command(BaseCommand):
help = "Generate invoices, should be run from cronjob scheduled for daily execution"
def handle(self, *args, **options):
instances = Instance.objects.all()
if instances:
for paid_instance in InstanceCreated.objects.all():
self.stdout.write(f"Found instance: {paid_instance.instance}")
if not payment_fullfilled(instance=paid_instance.instance):
self.stdout.write(
f"Payment not fulfilled for instance: {paid_instance.instance}"
)
payment = generate_invoice(instance=paid_instance.instance)
owner = paid_instance.instance.owned_by
ctx = {
"username": owner.username,
"payment": payment,
"link": get_invoice_link(payment=payment),
}
body = render_to_string(
"billing/emails/renew-subscription.txt",
context=ctx,
)
email = owner.email
sender = settings.DEFAULT_FROM_EMAIL
send_mail(
subject="[Gna!] Payment receipt for your Gna! VM",
message=body,
from_email=f"No reply Gna!<{sender}>", # TODO read from settings.py
recipient_list=[email],
)
else:
self.stdout.write("No instances available")

View File

@ -0,0 +1,18 @@
# Generated by Django 4.0.3 on 2022-07-08 13:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("billing", "0004_payment_billing_pay_paid_by_77f57c_idx"),
]
operations = [
migrations.AddField(
model_name="payment",
name="vm_deleted",
field=models.BooleanField(default=False),
),
]

View File

@ -67,6 +67,8 @@ class Payment(BasePayment):
date = models.DateTimeField(auto_now_add=True, blank=True)
objects = PaymentModelManager()
vm_deleted = models.BooleanField(default=False, null=False)
def get_failure_url(self) -> str:
url = urlparse(settings.PAYMENT_HOST)
return urlunparse(

View File

@ -0,0 +1,14 @@
Hello {{ username }}!
An invoice is generated for your Gna! VM {{ payment.instance_name }}.
- Configuration: {{payment.instance_configuration_id.name}}
- Invoice generated on: {{payment.date.month}}/{{payment.date.day}}/{{payment.date.year}}
- Total Amount: {{payment.total}} {{payment.currency|upper}}
To pay, please click the link below:
{{ link }}
Cheers,
Gna! team

View File

@ -0,0 +1,22 @@
Hello {{ username }}!
This is a receipt for your latest Gna! payment.
-----------------------------------------------------
Gna! Receipt - {{payment.date.month}}/{{payment.date.day}}/{{payment.date.year}}
- Instance Name: {{ payment.instance_name }}
- Configuration: {{payment.instance_configuration_id.name}}
- Total Amount: {{payment.total}} {{payment.currency|upper}}
To view the receipt online, please see the following link:
{{link}}
We appreciate your business!
Cheers,
Gna! team

View File

@ -0,0 +1,17 @@
Hello {{ username }}!
Your Gna! VM subscription is due for renewal. Please click the link
below to renew your subscription:
{{link}}
-----------------------------------------------------
- Instance Name: {{ payment.instance_name }}
- Configuration: {{payment.instance_configuration_id.name}}
- Total Amount: {{payment.total}} {{payment.currency|upper}}
We appreciate your business!
Cheers,
Gna! team

View File

@ -16,11 +16,13 @@ import time
from io import StringIO
from django.contrib.auth import get_user_model
from django.core import mail
from django.core.management import call_command
from django.urls import reverse
from django.test import TestCase, Client, override_settings
from django.utils.http import urlencode
from django.contrib.auth import authenticate
from django.core.management import call_command
from django.conf import settings
from payments import get_payment_model, RedirectNeeded, PaymentStatus
@ -47,7 +49,7 @@ class BillingTest(TestCase):
def test_payments(self):
c = Client()
login_util(self, c, "accounts.home")
instance_name = "test_payments"
instance_name = "tpayments"
create_instance_util(
t=self, c=c, instance_name=instance_name, config=self.instance_config[0]
)
@ -89,7 +91,135 @@ class BillingTest(TestCase):
reverse("infra.create", args=(payment.instance_name,)),
)
# create_instance_util creates an instance and pays for it. An email is
# sent when the invoice is generated and one after payment is made
#
# So we are first checking for the last email that was sent(receipt)
# and then the Forgejo instance credentials notification followed by the
# invoice generation email.
receipt_mail = mail.outbox.pop()
print(receipt_mail.body)
self.assertEqual(
all(
[
receipt_mail.to[0] == self.email,
"This is a receipt for your latest Gna! payment"
in receipt_mail.body,
]
),
True,
)
instance_notificaiton = mail.outbox.pop()
self.assertEqual(
all(
[
instance_notificaiton.to[0] == self.email,
"Congratulations on your new Gna! instance!"
in instance_notificaiton.body,
]
),
True,
)
## payment failure page; no real functionality but user is redirected here
# by stripe if payment is successful
resp = c.get(reverse("billing.invoice.fail", args=(payment.public_ref,)))
self.assertEqual(b"failed" in resp.content, True)
class GenerateInvoiceCommand(TestCase):
"""
Test command: manage.py generate_invoice
"""
def setUp(self):
self.username = "test_generate_invoice_cmd_user"
register_util(t=self, username=self.username)
create_configurations(t=self)
@override_settings(
HOSTEA=infra_custom_config(
test_name="test_dont_send_invoices_to_not_created_vms"
)
)
def test_dont_send_invoices_to_not_created_vms(self):
c = Client()
login_util(self, c, "accounts.home")
instance_name = "tnoinvonocrevm"
payload = {"name": instance_name, "configuration": self.instance_config[0].name}
resp = c.post(reverse("dash.instances.new"), payload)
self.assertEqual(resp.status_code, 302)
self.assertEqual(
resp.headers["location"],
reverse("billing.invoice.generate", args=(instance_name,)),
)
stdout = StringIO()
stderr = StringIO()
instance = Instance.objects.get(name=instance_name)
self.assertEqual(payment_fullfilled(instance=instance), False)
prev_len = len(mail.outbox)
# username exists
call_command(
"generate_invoice",
stdout=stdout,
stderr=stderr,
)
out = stdout.getvalue()
print(out)
self.assertEqual(instance_name not in out, True)
@override_settings(
HOSTEA=infra_custom_config(test_name="test_generate_invoice_cmd")
)
def test_cmd(self):
c = Client()
login_util(self, c, "accounts.home")
instance_name = "tgeninvmd"
create_instance_util(
t=self, c=c, instance_name=instance_name, config=self.instance_config[0]
)
stdout = StringIO()
stderr = StringIO()
instance = Instance.objects.get(name=instance_name)
self.assertEqual(payment_fullfilled(instance=instance), True)
prev_len = len(mail.outbox)
# username exists
call_command(
"generate_invoice",
stdout=stdout,
stderr=stderr,
)
out = stdout.getvalue()
print(out)
self.assertEqual(instance_name in out, True)
self.assertEqual(prev_len, len(mail.outbox))
# delete payment and re-generate with command
get_payment_model().objects.get(instance_name=instance_name).delete()
stdout = StringIO()
stderr = StringIO()
call_command(
"generate_invoice",
stdout=stdout,
stderr=stderr,
)
out = stdout.getvalue()
print("out")
print(out)
self.assertEqual(instance_name in out, True)
self.assertEqual(f"Payment not fulfilled for instance: {instance}" in out, True)
self.assertEqual(prev_len + 1, len(mail.outbox))

View File

@ -12,22 +12,90 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from enum import Enum, unique
from urllib.parse import urlparse, urlunparse
from datetime import datetime, timedelta, timezone
from payments import get_payment_model, RedirectNeeded, PaymentStatus
from django.core.mail import send_mail
from django.urls import reverse
from django.template.loader import render_to_string
from django.contrib.auth.models import User
from django.conf import settings
from django.shortcuts import get_object_or_404
from dash.models import Instance
Payment = get_payment_model()
def payment_fullfilled(instance: Instance) -> bool:
Payment = get_payment_model()
def __get_delta():
now = datetime.now(tz=timezone.utc)
delta = now - timedelta(seconds=(60 * 60 * 24 * 30)) # one month
return delta
def get_invoice_link(payment: Payment):
invoice_link = reverse("billing.invoice.details", args=(payment.public_ref,))
parsed = urlparse(settings.PAYMENT_HOST)
return urlunparse((parsed.scheme, parsed.netloc, invoice_link, "", "", ""))
def payment_fullfilled(instance: Instance) -> bool:
delta = __get_delta()
payment = None
for p in Payment.objects.filter(date__gt=(delta), instance_name=instance.name):
for p in Payment.objects.filter(
date__gt=(delta), instance_name=instance.name, vm_deleted=False
):
if p.status == PaymentStatus.CONFIRMED:
return True
return False
@unique
class GenerateInvoiceErrorCode(Enum):
ALREADY_PAID = "already paid"
DUPLICATE_PAYMENT = "DUPLICATE PAYMENT"
def __str__(self) -> str:
return self.name
class GenerateInvoiceException(Exception):
error: str
code: GenerateInvoiceErrorCode
def __init__(self, code: GenerateInvoiceErrorCode):
self.error = str(code)
self.code = code
def __str__(self):
return self.error
def generate_invoice(instance: Instance) -> Payment:
delta = __get_delta()
payment = None
for p in Payment.objects.filter(
date__gt=(delta), instance_name=instance.name, vm_deleted=False
):
if p.status == PaymentStatus.CONFIRMED:
raise GenerateInvoiceException(code=GenerateInvoiceErrorCode.ALREADY_PAID)
if any([p.status == PaymentStatus.INPUT, p.status == PaymentStatus.WAITING]):
if payment is None:
payment = p
else:
print(f"Duplicate payment {p}, deleting in favor of {payment}")
p.delete()
if payment is None:
print("Payment not found, generating new payment")
payment = Payment.objects.create(
variant="stripe", # this is the variant from PAYMENT_VARIANTS
instance=instance,
)
return payment

View File

@ -16,16 +16,26 @@ from datetime import datetime, timedelta, timezone
from django.shortcuts import get_object_or_404, redirect, render
from django.template.response import TemplateResponse
from django.core.mail import send_mail
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.template.loader import render_to_string
from django.urls import reverse
from django.db.models import Q
from django.conf import settings
from payments import get_payment_model, RedirectNeeded, PaymentStatus
from dash.models import Instance
from django.db.models import Q
from infrastructure.utils import create_vm_if_not_exists
from dash.utils import footer_ctx
from .utils import (
generate_invoice as generate_invoice_util,
GenerateInvoiceErrorCode,
GenerateInvoiceException,
get_invoice_link,
)
def default_ctx(title: str, username: str):
"""
@ -66,29 +76,13 @@ def paid_invoices(request):
def generate_invoice(request, instance_name: str):
instance = get_object_or_404(Instance, name=instance_name, owned_by=request.user)
Payment = get_payment_model()
now = datetime.now(tz=timezone.utc)
delta = now - timedelta(seconds=(60 * 60 * 24 * 30)) # one month
payment = None
for p in Payment.objects.filter(date__gt=(delta), instance_name=instance_name):
if p.status == PaymentStatus.CONFIRMED:
return HttpResponse("BAD REQUEST: Already paid", status=400)
elif any([p.status == PaymentStatus.INPUT, p.status == PaymentStatus.WAITING]):
if payment is None:
payment = p
else:
print(f"Duplicate payment {p}, deleting in favor of {payment}")
p.delete()
if payment is None:
print("Payment not found, generating new payment")
payment = Payment.objects.create(
variant="stripe", # this is the variant from PAYMENT_VARIANTS
instance=instance,
)
try:
payment = generate_invoice_util(instance=instance)
return redirect(reverse("billing.invoice.details", args=(payment.public_ref,)))
except GenerateInvoiceException as e:
if e.code == GenerateInvoiceErrorCode.ALREADY_PAID:
return HttpResponse("BAD REQUEST: Already paid", status=400)
return redirect(reverse("dash.home"))
@login_required
@ -118,6 +112,28 @@ def payment_success(request, payment_public_id):
payment = get_object_or_404(
get_payment_model(), public_ref=payment_public_id, paid_by=request.user
)
ctx = {
"username": request.user.username,
"payment": payment,
"link": get_invoice_link(payment=payment),
}
body = render_to_string(
"billing/emails/payment-receipt.txt",
context=ctx,
)
email = request.user.email
sender = settings.DEFAULT_FROM_EMAIL
send_mail(
subject="[Gna!] Payment receipt for your Gna! VM",
message=body,
from_email=f"No reply Gna!<{sender}>", # TODO read from settings.py
recipient_list=[email],
)
return redirect(reverse("infra.create", args=(payment.instance_name,)))

View File

@ -127,6 +127,10 @@ header {
text-decoration: underline;
}
.nav__logo-img {
height: 48px;
}
.nav__toggle {
display: none;
}

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 955 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
common-static/img/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="{% static 'css/main.css' %}" />
<title>{{ title }}| Hostea Dashbaord</title>
<title>{{ title }}| Gna! Dashboard</title>
{% include "common/components/meta.html" %}
</head>
<body>

View File

@ -2,12 +2,8 @@
<nav class="nav__container">
<input type="checkbox" class="nav__toggle" id="nav__toggle" />
<div class="nav__header">
<a class="nav__logo-container" href="/">
<img
src="{% static 'img/android-icon-48x48.png' %}"
alt="Hostea temporary logo"
/>
<p class="nav__home-btn">ostea</p>
<a class="nav__logo-container" href="https://gna.org">
<img alt="Gna! logo" class="nav__logo-img" src="{% static 'img/logo.png' %}" />
</a>
<label class="nav__hamburger-menu" for="nav__toggle">
<span class="nav__hamburger-inner"></span>
@ -18,17 +14,26 @@
<div class="nav__link-group">
<div class="nav__link-container">
<a class="nav__link" rel="noreferrer" href="{% url 'accounts.login' %}"
<a
class="nav__link"
rel="noreferrer"
href="{% url 'dash.instances.new' %}"
>Add Instance</a
>
</div>
<div class="nav__link-container">
<a class="nav__link" rel="noreferrer" href="{% url 'support.home' %}"
<a
class="nav__link"
rel="noreferrer"
href="{% url 'support.home' %}"
>Support</a
>
</div>
<div class="nav__link-container">
<a class="nav__link" rel="noreferrer" href="{% url 'accounts.logout' %}"
<a
class="nav__link"
rel="noreferrer"
href="{% url 'accounts.logout' %}"
>Logout</a
>
</div>

View File

@ -45,8 +45,10 @@
</details>
</div>
<!--
<div class="secondary-nav__options">
<a href="/foo" class="secondary-nav__option-link">Manage Account</a>
</div>
-->
</nav>
</aside>

View File

@ -12,7 +12,7 @@
</ul>
<p>Created On: {{ instance.created_at }}</p>
<p><a href="{{gitea_uri}}">Gitea Instance</a>|<a href="{{woodpecker}}">Woodpecker CI</a></p>
<p><a href="{{forgejo_uri}}">Forgejo Instance</a>|<a href="{{woodpecker}}">Woodpecker CI</a></p>
<form
action="{% url 'dash.instances.delete' name=instance.name %}"

View File

@ -30,6 +30,7 @@ from payments import get_payment_model, RedirectNeeded, PaymentStatus
from accounts.tests import login_util, register_util
from .models import InstanceConfiguration, Instance
from .utils import create_instance, sanitize_vm_name, VmErrors, VmException
def create_configurations(t: TestCase):
@ -170,7 +171,7 @@ class InstancesConfig(TestCase):
"""
Expects InstancesConfig titled "s1-2", "s1-4" and "s1-8"
ref: https://gitea.hostea.org/Hostea/july-mvp/issues/10#issuecomment-639
ref: https://forgejo.gna.org/Hostea/july-mvp/issues/10#issuecomment-639
"""
self.assertEqual(
InstanceConfiguration.objects.filter(
@ -197,6 +198,30 @@ class CreateInstance(TestCase):
register_util(t=self, username="createinstance_user")
create_configurations(t=self)
def test_sanitize_vm_name(self):
self.assertEqual(sanitize_vm_name(vm_name="LOWERname"), "lowername")
with self.assertRaises(VmException):
sanitize_vm_name(vm_name="12345452131324234234234234")
with self.assertRaises(VmException):
sanitize_vm_name(vm_name="122342$#34234")
@override_settings(
HOSTEA=infra_custom_config(test_name="test_create_instance_util")
)
def test_create_instance_util(self):
configuration = self.instance_config[0].name
with self.assertRaises(VmException):
create_instance(
vm_name="12345452131324234234234234",
configuration_name=configuration,
user=self.user,
)
@override_settings(
HOSTEA=infra_custom_config(test_name="test_create_instance_renders")
)
@ -214,14 +239,14 @@ class CreateInstance(TestCase):
self.assertEqual(str.encode(test) in resp.content, True)
# create instance
instance_name = "testirenrs"
payload = {
"name": "test_create_instance_renders",
"name": instance_name,
"configuration": self.instance_config[0].name,
}
self.assertEqual(Instance.objects.filter(name=payload["name"]).exists(), False)
instance_name = "test_create_instance_renders"
create_instance_util(
t=self, c=c, instance_name=instance_name, config=self.instance_config[0]
)

View File

@ -14,7 +14,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from enum import Enum, unique
from git import Repo
from django.contrib.auth.models import User
from django.conf import settings
@ -24,6 +23,8 @@ from .models import Instance, InstanceConfiguration
@unique
class VmErrors(Enum):
NAME_EXISTS = "Instance name exists, please try again with a different name"
ILLEGAL_NAME = "Only alphanumeric characters are allowed in instance name"
NAME_TOO_LONG = "Instance name must be less than 20 characters"
NO_CONFIG = "Configuration doesn't exist, please try again."
def __str__(self) -> str:
@ -42,13 +43,30 @@ class VmException(Exception):
return self.error
def sanitize_vm_name(vm_name: str) -> str:
"""
Sanity checks and normalization of the vm name
"""
vm_name = vm_name.lower()
if len(vm_name) > 20:
raise VmException(code=VmErrors.NAME_TOO_LONG)
if not str.isalnum(vm_name):
raise VmException(code=VmErrors.ILLEGAL_NAME)
if Instance.objects.filter(name=vm_name).exists():
raise VmException(code=VmErrors.NAME_EXISTS)
return vm_name
def create_instance(vm_name: str, configuration_name: str, user: User) -> Instance:
"""
Create instance view
"""
if Instance.objects.filter(name=vm_name).exists():
raise VmException(code=VmErrors.NAME_EXISTS)
vm_name = sanitize_vm_name(vm_name)
if not InstanceConfiguration.objects.filter(name=configuration_name).exists():
raise VmException(code=VmErrors.NO_CONFIG)
@ -71,7 +89,7 @@ def footer_ctx():
"link": settings.HOSTEA["SOURCE_CODE"],
}
else:
link = "https://gitea.hostea.org/Hostea/dashboard"
link = "https://forgejo.gna.org/Hostea/dashboard"
source_code = {"text": "Source Code", "link": link}
try:
r = Repo(".")

View File

@ -89,10 +89,7 @@ def create_instance(request):
return redirect(reverse("billing.invoice.generate", args=(instance.name,)))
except VmException as e:
ctx = get_ctx()
if e.code == VmErrors.NAME_EXISTS:
reason = ("Instance name exists, please try again with a different name",)
elif e.code == VmErrors.NO_CONFIG:
reason = "Configuration doesn't exist, please try again."
reason = e.code.value
ctx["error"] = {
"title": "Can't create instance",
@ -120,12 +117,12 @@ def view_instance(request, name: str):
instance = get_object_or_404(Instance, owned_by=user, name=name)
ctx = default_ctx(title=PAGE_TITLE, username=user.username)
instance.configuration = instance.configuration_id
gitea_uri = Infra.get_gitea_uri(instance=instance)
forgejo_uri = Infra.get_forgejo_uri(instance=instance)
woodpecker = Infra.get_woodpecker_uri(instance=instance)
ctx["instance"] = instance
ctx["woodpecker"] = woodpecker
ctx["gitea_uri"] = gitea_uri
ctx["forgejo_uri"] = forgejo_uri
return render(request, "dash/instances/view/index.html", context=ctx)

View File

@ -44,8 +44,8 @@ PAYMENT_VARIANTS = {
"stripe": (
"payments.stripe.StripeProvider", # please don't change this
{
"secret_key": env.get_value("STRIPE_SECRET_KEY"),
"public_key": env.get_value("STRIPE_PUBLIC_KEY"),
"secret_key": env.get_value("STRIPE_SECRET_KEY", default="UNSET"),
"public_key": env.get_value("STRIPE_PUBLIC_KEY", default="UNSET"),
},
)
}
@ -54,19 +54,19 @@ PAYMENT_VARIANTS = {
### Dashbaord specific configuration options
HOSTEA = {
"SOURCE_CODE": "https://gitea.hostea.org/Hostea/dashboard",
"INSTANCE_MAINTAINER_CONTACT": "contact@hostea.example.org",
"SOURCE_CODE": "https://forgejo.gna.org/Hostea/dashboard",
"INSTANCE_MAINTAINER_CONTACT": "contact@gna.example.org",
"ACCOUNTS": {
"MAX_VERIFICATION_TOLERANCE_PERIOD": 60 * 60 * 24, # in seconds
"SUDO_TTL": 60 * 5,
},
"META": {
"GITEA_INSTANCE": env.get_value(
"HOSTEA_META_GITEA_INSTANCE"
), # meta Gitea insatnce
"GITEA_ORG_NAME": "Hostea", # Organisation name on Hostea meta instance
"FORGEJO_INSTANCE": env.get_value(
"HOSTEA_META_FORGEJO_INSTANCE"
), # meta Forgejo insatnce
"FORGEJO_ORG_NAME": "Hostea", # Organisation name on Hostea meta instance
# Repository dedicated for handling support
# ref: https://gitea.hostea.org/Hostea/july-mvp/issues/17
# ref: https://forgejo.gna.org/Hostea/july-mvp/issues/17
"SUPPORT_REPOSITORY": "support",
},
"INFRA": {
@ -78,13 +78,14 @@ HOSTEA = {
# SSH key that can push to the Git repository remote mentioned above
"SSH_KEY": env.get_value("HOSTEA_INFRA_HOSTEA_REPO_SSH_KEY"),
},
"HOSTEA_DOMAIN": "hostea.org", # domain at which Hostea VMs will be spun up
"HOSTEA_DOMAIN": "gna.org", # domain at which Hostea VMs will be spun up
},
}
# Please see EMAIL_* configuration options:
# https://docs.djangoproject.com/en/4.1/ref/settings/#email-host
EMAIL_CONFIG = env.email("EMAIL_URL", default="smtp://admin:password@localhost:10025")
DEFAULT_FROM_EMAIL = "no-reply@gna.org"
vars().update(EMAIL_CONFIG)

View File

@ -53,17 +53,17 @@ PAYMENT_VARIANTS = {
### Dashbaord specific configuration options
HOSTEA = {
"SOURCE_CODE": "https://gitea.hostea.org/Hostea/dashboard",
"INSTANCE_MAINTAINER_CONTACT": "contact@hostea.example.org",
"SOURCE_CODE": "https://forgejo.gna.org/Hostea/dashboard",
"INSTANCE_MAINTAINER_CONTACT": "contact@gna.example.org",
"ACCOUNTS": {
"MAX_VERIFICATION_TOLERANCE_PERIOD": 60 * 60 * 24, # in seconds
"SUDO_TTL": 60 * 5,
},
"META": {
"GITEA_INSTANCE": "https://gitea.hostea.org", # meta Gitea insatnce
"GITEA_ORG_NAME": "Hostea", # Organisation name on Hostea meta instance
"FORGEJO_INSTANCE": "https://forgejo.gna.org", # meta Forgejo insatnce
"FORGEJO_ORG_NAME": "Hostea", # Organisation name on Hostea meta instance
# Repository dedicated for handling support
# ref: https://gitea.hostea.org/Hostea/july-mvp/issues/17
# ref: https://forgejo.gna.org/Hostea/july-mvp/issues/17
"SUPPORT_REPOSITORY": "support",
},
"INFRA": {
@ -87,3 +87,4 @@ EMAIL_USE_SSL = False
EMAIL_PORT = 10025
EMAIL_HOST_USER = "admin"
EMAIL_HOST_PASSWORD = "password"
DEFAULT_FROM_EMAIL = "no-reply@gna.org"

View File

@ -24,13 +24,13 @@ SECRET_KEY = "django-insecure-44zt@)$td7_yh(01q^hrce%h(311n!djn%%#s1b7$cvfy!pf7y
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
"whitenoise.runserver_nostatic",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
@ -48,6 +48,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"oauth2_provider.middleware.OAuth2TokenMiddleware",
"django.middleware.common.CommonMiddleware",
@ -129,11 +130,13 @@ USE_TZ = True
# https://docs.djangoproject.com/en/4.0/howto/static-files/
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "static"
STATICFILES_DIRS = [
BASE_DIR / "static",
BASE_DIR / "common-static",
]
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field
@ -168,18 +171,18 @@ PAYMENT_VARIANTS = {
### Dashbaord specific configuration options
HOSTEA = {
"SOURCE_CODE": "https://gitea.hostea.org/Hostea/dashboard",
"SOURCE_CODE": "https://forgejo.gna.org/Hostea/dashboard",
"RESTRICT_NEW_INTEGRATION_INSTALLATION": True,
"INSTANCE_MAINTAINER_CONTACT": "contact@hostea.example.org",
"INSTANCE_MAINTAINER_CONTACT": "contact@gna.example.org",
"ACCOUNTS": {
"MAX_VERIFICATION_TOLERANCE_PERIOD": 60 * 60 * 24, # in seconds
"SUDO_TTL": 60 * 5,
},
"META": {
"GITEA_INSTANCE": "http://localhost:3000", # meta Gitea insatnce
"GITEA_ORG_NAME": "Hostea", # Organisation name on Hostea meta instance
"FORGEJO_INSTANCE": "http://localhost:3000", # meta Forgejo insatnce
"FORGEJO_ORG_NAME": "Hostea", # Organisation name on Hostea meta instance
# Repository dedicated for handling support
# ref: https://gitea.hostea.org/Hostea/july-mvp/issues/17
# ref: https://forgejo.gna.org/Hostea/july-mvp/issues/17
"SUPPORT_REPOSITORY": "support",
},
"INFRA": {
@ -191,7 +194,7 @@ HOSTEA = {
# SSH key that can push to the Git repository remote mentioned above
"SSH_KEY": "/srv/hostea/deploy",
},
"HOSTEA_DOMAIN": "vm.hostea.org", # domain at which Hostea VMs will be spun up
"HOSTEA_DOMAIN": "vm.gna.org", # domain at which Hostea VMs will be spun up
},
}
@ -203,6 +206,7 @@ EMAIL_USE_SSL = False
EMAIL_PORT = 10025
EMAIL_HOST_USER = "admin"
EMAIL_HOST_PASSWORD = "password"
DEFAULT_FROM_EMAIL: "no-reply@gna.org"
try:
from dashboard.local_settings import *

View File

@ -1,23 +1,23 @@
version: "3"
#networks:
# hostea-dash-gitea:
# hostea-dash-forgejo:
# external: false
# hostea-dash-smtp:
# external: false
services:
gitea:
image: gitea/gitea:1.16.5
container_name: hostea-dash-gitea
forgejo:
image: codeberg.org/forgejo/forgejo:1.18.0-1
container_name: hostea-dash-forgejo
network_mode: host
environment:
- USER_UID=1000
- USER_GID=1000
restart: always
#networks:
# - hostea-dash-gitea
# - hostea-dash-forgejo
volumes:
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro

View File

@ -15,7 +15,7 @@
1. Clone the project
```bash
git clone https://gitea.hostea.org/Hostea/dashboard.git && cd dashboard
git clone https://forgejo.gna.org/Hostea/dashboard.git && cd dashboard
```
2. Create `virtualenv` and activate environment

View File

@ -9,6 +9,18 @@ configuration file to override [dashboard/settings.py](../dashboard/settings.py)
Please copy local_settings.example.py to local_settings.py and make
changes to the newly copied file.
## Static files
In order to serve static files, please run the following command before
running the Dashbaord server:
```bash
yes yes | python manage.py collectstatic
```
This command will gather all static assets from all the modules in
Dashbaord and place them in `static/` in the base directory.
## Cron jobs
Run cron job at an interval of your choosing with the following comamnd:
@ -32,8 +44,8 @@ hence the current redundancy in configuration and cronjob duration.
## Support Platform Integration
Hostea Dashbaord delegates support to Hostea's meta Gitea instance, as
discussed [here](https://gitea.hostea.org/Hostea/july-mvp/issues/17).
Hostea Dashbaord delegates support to Hostea's meta Forgejo instance, as
discussed [here](https://forgejo.gna.org/Hostea/july-mvp/issues/17).
To configure support platform integration , please set the following
attributes in `settings.py`:
@ -43,10 +55,10 @@ HOSTEA = {
# <--snip--->
"META": {
# <--snip--->
"GITEA_INSTANCE": "https://gitea.hostea.org", # meta Gitea insatnce
"GITEA_ORG_NAME": "Hostea", # Organisation name on Hostea meta instance
"FORGEJO_INSTANCE": "https://forgejo.gna.org", # meta Forgejo insatnce
"FORGEJO_ORG_NAME": "Hostea", # Organisation name on Hostea meta instance
# Repository dedicated for handling support
# ref: https://gitea.hostea.org/Hostea/july-mvp/issues/17
# ref: https://forgejo.gna.org/Hostea/july-mvp/issues/17
"SUPPORT_REPOSITORY": "support",
},
}

View File

@ -26,3 +26,15 @@ python manage.py vm delete <VM-name>
This command is not idempotent. The command throws an error when a
a VM with the given name doesn't exist.
### 3. Generate Invoices: periodically generate invoices for VMs
```bash
python manage.py generate_invoice
```
Generates invoices for VMs which enter a new billing cycle and sends
notification email to VM owners.
This command can be run as many times as desirable but running at least
once in a day is advisable.

View File

@ -1,75 +0,0 @@
APP_NAME = Gitea: Git with a cup of tea
RUN_USER = atm
RUN_MODE = prod
[database]
DB_TYPE = sqlite3
HOST = 127.0.0.1:3306
NAME = gitea
USER = gitea
PASSWD =
SCHEMA =
SSL_MODE = disable
CHARSET = utf8
PATH = ./tmp/gitea/db/gitea.db
LOG_SQL = false
[repository]
ROOT = ./tmp/gitea/repos/
[server]
SSH_DOMAIN = localhost
DOMAIN = localhost
HTTP_PORT = 3000
ROOT_URL = http://localhost:3000/
DISABLE_SSH = false
SSH_PORT = 2222
LFS_START_SERVER = true
LFS_JWT_SECRET = MilbUZw4BbeFsnOWBDGzYrgBINrkJIcoOPivE9IPNAQ
OFFLINE_MODE = false
[lfs]
PATH = ./tmp/gitea/lfs/
[mailer]
ENABLED = false
[service]
REGISTER_EMAIL_CONFIRM = false
ENABLE_NOTIFY_MAIL = false
DISABLE_REGISTRATION = false
ALLOW_ONLY_EXTERNAL_REGISTRATION = false
ENABLE_CAPTCHA = false
REQUIRE_SIGNIN_VIEW = false
DEFAULT_KEEP_EMAIL_PRIVATE = false
DEFAULT_ALLOW_CREATE_ORGANIZATION = true
DEFAULT_ENABLE_TIMETRACKING = true
NO_REPLY_ADDRESS = noreply.localhost
[picture]
DISABLE_GRAVATAR = false
ENABLE_FEDERATED_AVATAR = true
[openid]
ENABLE_OPENID_SIGNIN = true
ENABLE_OPENID_SIGNUP = true
[session]
PROVIDER = file
[log]
MODE = console
LEVEL = debug
ROOT_PATH = ./tmp/gitea/log
ROUTER = console
[repository.pull-request]
DEFAULT_MERGE_STYLE = merge
[repository.signing]
DEFAULT_TRUST_MODEL = committer
[security]
INSTALL_LOCK = true
INTERNAL_TOKEN = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE2NTQ2MDY2ODV9.WbIw4n8M_MXy594pqgmEMD3NUtTpL8hcUC_uhoSc5ec
PASSWORD_HASH_ALGO = pbkdf2

View File

@ -93,9 +93,9 @@ class Command(BaseCommand):
instance = create_instance(
vm_name=vm_name, configuration_name=size, user=user
)
(gitea_password, _commit) = create_vm_if_not_exists(instance)
(forgejo_password, _commit) = create_vm_if_not_exists(instance)
print("Instance created")
print(f"Gitea admin password: {gitea_password}")
print(f"Forgejo admin password: {forgejo_password}")
except VmException as e:
if e.code == VmErrors.NAME_EXISTS:
instance = Instance.objects.get(name=vm_name)
@ -104,9 +104,9 @@ class Command(BaseCommand):
name=size
)
instance.save()
(gitea_password, _commit) = create_vm_if_not_exists(instance)
(forgejo_password, _commit) = create_vm_if_not_exists(instance)
print("Instance created")
print(f"Gitea admin password: {gitea_password}")
print(f"Forgejo admin password: {forgejo_password}")
else:
self.stderr.write(self.style.ERROR(f"error: {str(e)}"))

View File

@ -0,0 +1,18 @@
Hello {{ username }},
Congratulations on your new Gna! instance!
Your Gna! instance is being prepared, you will receive an email
notification when it is ready.
You can use the following credentials to log into an admin account on
your new Gna! Forgejo instance. Great powers come with great
responsibilities, so use the admin credentials wisely. When in doubt,
consult the Forgejo docs or contact support!
- username : root
- password: {{ forgejo_password }}
- Forgejo {{ forgejo_uri }}
Cheers,
Gna! team

View File

@ -0,0 +1,11 @@
Hello {{ username }}!,
The deployment job has run to completion and your Gna! instance is now online!
Credentials to admin account was sent in an earlier email, please contact
support if didn't receive it.
Forgejo: {{ forgejo_uri }}
Woodpecker CI: {{ woodpecker_uri }}
Cheers,
Gna! team

View File

@ -0,0 +1,10 @@
Hello {{ username }}!,
A customer has purchased a new instance. Please find the details below:
Forgejo: {{ forgejo_uri }}
Woodpecker CI: {{ woodpecker_uri }}
Cheers,
Gna! team

View File

@ -1,11 +1,11 @@
{% extends 'dash/common/base.html' %} {% block dash %}
<h1>{{ title }}</h1>
<h2>Gitea Admin Credentials</h2>
<h2>Forgejo Admin Credentials</h2>
<ul>
<li><b>Username:</b> root</li>
<li><b>Password:</b> {{ gitea_password }}</li>
<li><b>Password:</b> {{ forgejo_password }}</li>
</ul>
{% endblock %}

View File

@ -1,2 +1,2 @@
enough --domain $domain host create {{subdomain}}-host
enough --domain $domain service create --host {{subdomain}}-host gitea
enough --domain $domain service create --host {{subdomain}}-host forgejo

View File

@ -2,49 +2,49 @@
#
#######################################
#
# Public hostname of the Gitea instance
# Public hostname of the Forgejo instance
#
#
gitea_host: "{{ subdomain }}.{{ '{' }}{{ '{' }} domain {{ '}' }}{{ '}' }}"
forgejo_host: "{{ subdomain }}.{{ '{' }}{{ '{' }} domain {{ '}' }}{{ '}' }}"
#
#######################################
#
# Mailer from
#
#
gitea_mailer_from: "noreply@{{ '{' }}{{ '{' }} domain {{ '}' }}{{ '}' }}"
forgejo_mailer_from: "noreply@{{ '{' }}{{ '{' }} domain {{ '}' }}{{ '}' }}"
#
#######################################
#
# SSH port of the Gitea instance
# SSH port of the Forgejo instance
#
#
gitea_ssh_port: "22"
forgejo_ssh_port: "22"
#
#######################################
#
# Gitea version
# Forgejo version
#
#
gitea_version: "1.16.8"
#forgejo_version: "1.18.0-1"
#
#######################################
#
# Admin user name
#
gitea_user: root
forgejo_user: root
#
#######################################
#
# Admin user password
#
gitea_password: "{{ gitea_password }}"
forgejo_password: "{{ forgejo_password }}"
#
#######################################
#
# Admin user email
#
gitea_email: "{{ gitea_email }}"
forgejo_email: "{{ forgejo_email }}"
#
#######################################
#
@ -60,9 +60,9 @@ woodpecker_host: "{{ '{' }}{{ '{' }} woodpecker_hostname {{ '}' }}{{ '}' }}.{{ '
#
#######################################
#
# Gitea users with admin rights on woodpecker
# Forgejo users with admin rights on woodpecker
#
woodpecker_admins: "{{ '{' }}{{ '{' }} gitea_user {{ '}' }}{{ '}' }}"
woodpecker_admins: "{{ '{' }}{{ '{' }} forgejo_user {{ '}' }}{{ '}' }}"
#
#######################################
#

View File

@ -1,4 +1,4 @@
gitea-service-group:
forgejo-service-group:
hosts:
{{ subdomain }}-host:
ansible_port: 2222

View File

@ -12,25 +12,21 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import shutil
import time
from io import StringIO
from urllib.parse import urlunparse
from pathlib import Path
import requests
from django.test import TestCase, Client, override_settings
from django.conf import settings
from django.core.management import call_command
from git import Repo
from django.core import mail
from dash.models import Instance, InstanceConfiguration
from accounts.tests import register_util, login_util
from dash.tests import create_configurations, create_instance_util, infra_custom_config
from infrastructure.management.commands.vm import translate_sizes
from .utils import Infra, Worker
from billing.utils import payment_fullfilled
from .utils import Infra, Worker, create_vm_if_not_exists, delete_vm
from .models import InstanceCreated, Job, JobType
@ -42,164 +38,209 @@ class InfraUtilTest(TestCase):
def setUp(self):
self.username = "infrautil_user"
register_util(t=self, username=self.username)
self.user.is_staff = True
self.user.save()
create_configurations(t=self)
@override_settings(HOSTEA=infra_custom_config(test_name="test_path_util"))
def test_path_utils(self):
infra = Infra()
subdomain = "foo"
base = infra.repo_path
# @override_settings(HOSTEA=infra_custom_config(test_name="test_path_util"))
# def test_path_utils(self):
# infra = Infra()
# subdomain = "foo"
# base = infra.repo_path
#
# self.assertEqual(
# base.joinpath(f"inventory/host_vars/{subdomain}-host/"),
# infra._host_vars_dir(subdomain=subdomain),
# )
#
# self.assertEqual(
# base.joinpath(f"inventory/host_vars/{subdomain}-host/forgejo.yml"),
# infra._forgejo_path(subdomain=subdomain),
# )
#
# self.assertEqual(
# base.joinpath(f"inventory/host_vars/{subdomain}-host/provision.yml"),
# infra._provision_path(subdomain=subdomain),
# )
#
# self.assertEqual(
# base.joinpath(f"inventory/{subdomain}-backup.yml"),
# infra._backup_path(subdomain=subdomain),
# )
#
# self.assertEqual(
# base.joinpath(f"hosts-scripts/{subdomain}-host.sh"),
# infra._hostscript_path(subdomain=subdomain),
# )
#
# @override_settings(HOSTEA=infra_custom_config(test_name="test_add_vm"))
# def test_add_vm(self):
# c = Client()
# login_util(self, c, "accounts.home")
# subdomain = "add_vm"
#
# create_instance_util(
# t=self, c=c, instance_name=subdomain, config=self.instance_config[0]
# )
#
# instance = Instance.objects.get(name=subdomain)
#
# infra = Infra()
# before_add = infra._sha()
# (password, after_add) = infra.add_vm(instance=instance)
# self.assertNotEqual(before_add, after_add)
#
# before_rm = after_add
# after_rm = infra.remove_vm(instance=instance)
# self.assertNotEqual(before_rm, after_rm)
#
# @override_settings(HOSTEA=infra_custom_config(test_name="test_cmd"))
# def test_cmd(self):
# subdomain = "cmd_vm"
# infra = Infra()
# c = Client()
# login_util(self, c, "accounts.home")
#
# self.assertEqual(Instance.objects.filter(name=subdomain).exists(), False)
# # username exists
# call_command(
# "vm", "create", subdomain, f"--owner={self.username}", "--flavor=medium"
# )
#
# instance = Instance.objects.get(name=subdomain)
#
# self.assertEqual(infra.get_flavor(instance=instance), "openstack_flavor_medium")
#
# self.assertEqual(instance.owned_by, self.user)
# self.assertEqual(
# instance.configuration_id, InstanceConfiguration.objects.get(name="s1-4")
# )
#
# instance_created = InstanceCreated.objects.get(instance=instance)
# self.assertEqual(instance_created.instance, instance)
#
# self.assertEqual(instance_created.created, True)
#
# # run create vm command again with same configuration to crudely check idempotency
# call_command(
# "vm", "create", subdomain, f"--owner={self.username}", "--flavor=medium"
# )
#
# # run create vm command again with different configuration but same name
# # to crudely check idempotency
# call_command(
# "vm", "create", subdomain, f"--owner={self.username}", "--flavor=large"
# )
# instance.refresh_from_db()
# # verify new size is updated in DB
# self.assertEqual(
# str.strip(instance.configuration_id.name)
# == str.strip(translate_sizes("large")),
# True,
# )
#
# # verify new size is updated in repository
# self.assertEqual(
# str.strip(infra.translate_size(instance=instance))
# == str.strip(infra.get_flavor(instance=instance)),
# True,
# )
#
# call_command("vm", "delete", subdomain)
#
# self.assertEqual(Instance.objects.filter(name=subdomain).exists(), False)
# host_vars_dir = infra._host_vars_dir(subdomain)
# self.assertEqual(host_vars_dir.exists(), False)
#
# # run delete VM command to crudely check idempotency
# call_command("vm", "delete", subdomain)
#
# def test_worker(self):
# subdomain = "forgejo" # yes, forgejo.hostea.org exists. will use it till I
# # figure out how to use requests_mock within django
# c = Client()
# login_util(self, c, "accounts.home")
# create_instance_util(
# t=self, c=c, instance_name=subdomain, config=self.instance_config[0]
# )
#
# instance = Instance.objects.get(name=subdomain)
# job = Job.objects.create(instance=instance, job_type=JobType.PING)
# forgejo_uri = Infra.get_forgejo_uri(instance=instance)
# print(f"mocking {forgejo_uri}")
#
# w = Worker(job=job)
# w.start()
# time.sleep(15)
# self.assertEqual(w.is_alive(), False)
# w.join()
# self.assertEqual(
# Job.objects.filter(instance=instance, job_type=JobType.PING).exists(), True
# )
#
@override_settings(HOSTEA=infra_custom_config(test_name="test_vm_delete_payments"))
def test_vm_delete_payments(self):
"""
Test if the dashboard generates invoices for a VM crated with a name
matching a VM name that was deleted that existed.
self.assertEqual(
base.joinpath(f"inventory/host_vars/{subdomain}-host/"),
infra._host_vars_dir(subdomain=subdomain),
)
self.assertEqual(
base.joinpath(f"inventory/host_vars/{subdomain}-host/gitea.yml"),
infra._gitea_path(subdomain=subdomain),
)
self.assertEqual(
base.joinpath(f"inventory/host_vars/{subdomain}-host/provision.yml"),
infra._provision_path(subdomain=subdomain),
)
self.assertEqual(
base.joinpath(f"inventory/{subdomain}-backup.yml"),
infra._backup_path(subdomain=subdomain),
)
self.assertEqual(
base.joinpath(f"hosts-scripts/{subdomain}-host.sh"),
infra._hostscript_path(subdomain=subdomain),
)
@override_settings(HOSTEA=infra_custom_config(test_name="test_add_vm"))
def test_add_vm(self):
infra = Infra()
ref: https://forgejo.hostea.org/Hostea/dashboard/issues/38#issuecomment-1162
"""
c = Client()
conf = settings.HOSTEA["INFRA"]["HOSTEA_REPO"]
login_util(self, c, "accounts.home")
subdomain = "add_vm"
instance_name = "trmpayments"
base = infra.repo_path
infra = Infra()
create_instance_util(
t=self, c=c, instance_name=subdomain, config=self.instance_config[0]
t=self, c=c, instance_name=instance_name, config=self.instance_config[0]
)
before_add = infra.repo.head.commit.hexsha
instance = Instance.objects.get(name=subdomain)
woodpecker_agent_secret = infra.add_vm(instance=instance)
after_add = infra.repo.head.commit.hexsha
self.assertEqual(before_add is not after_add, True)
instance = Instance.objects.get(name=instance_name)
self.assertEqual(payment_fullfilled(instance=instance), True)
create_vm_if_not_exists(instance=instance)
# c = infra_custom_config(test_name="test_add_vm--get-head")
path = Path("/tmp/hostea/dashboard/check-test_add_vm")
if path.exists():
shutil.rmtree(path)
repo = Repo.clone_from(conf["REMOTE"], path, env=infra.env)
repo.git.pull(env=infra.env)
self.assertEqual(repo.head.commit.hexsha == after_add, True)
# delete VM
delete_vm(instance=instance)
self.assertEqual(Instance.objects.filter(name=instance_name).exists(), False)
before_rm = infra.repo.head.commit.hexsha
infra.remove_vm(instance=instance)
after_rm = infra.repo.head.commit.hexsha
self.assertEqual(before_add is not after_add, True)
repo.git.pull(env=infra.env)
self.assertEqual(repo.head.commit.hexsha == after_rm, True)
@override_settings(HOSTEA=infra_custom_config(test_name="test_cmd"))
def test_cmd(self):
subdomain = "cmd_vm"
infra = Infra()
c = Client()
conf = settings.HOSTEA["INFRA"]["HOSTEA_REPO"]
login_util(self, c, "accounts.home")
base = infra.repo_path
# re-create VM with management command as it bypasses payments. We
# usually use create_instance_util but it will pay for the instance too
call_command(
"vm", "create", instance_name, f"--owner={self.username}", "--flavor=medium"
)
# verify VM is created
self.assertEqual(Instance.objects.filter(name=instance_name).exists(), True)
# verify payment is unfulfilled
instance = Instance.objects.get(name=instance_name)
self.assertEqual(payment_fullfilled(instance=instance), False)
# generate invoice
stdout = StringIO()
stderr = StringIO()
self.assertEqual(Instance.objects.filter(name=subdomain).exists(), False)
# username exists
call_command(
"vm", "create", subdomain, f"--owner={self.username}", "--flavor=medium"
"generate_invoice",
stdout=stdout,
stderr=stderr,
)
out = stdout.getvalue()
print("out")
print(out)
self.assertEqual(instance_name in out, True)
self.assertEqual(f"Payment not fulfilled for instance: {instance}" in out, True)
instance = Instance.objects.get(name=subdomain)
self.assertEqual(infra.get_flavor(instance=instance), "openstack_flavor_medium")
self.assertEqual(instance.owned_by, self.user)
staff_notification = None
for m in mail.outbox:
if "New instance alert" in m.subject:
staff_notification = m
break
self.assertEqual(staff_notification.to[0], self.email)
self.assertEqual(
instance.configuration_id, InstanceConfiguration.objects.get(name="s1-4")
"[Gna!] New instance alert" in staff_notification.subject, True
)
instance_created = InstanceCreated.objects.get(instance=instance)
self.assertEqual(instance_created.instance, instance)
self.assertEqual(instance_created.created, True)
# run create vm command again with same configuration to crudely check idempotency
call_command(
"vm", "create", subdomain, f"--owner={self.username}", "--flavor=medium"
)
# run create vm command again with different configuration but same name
# to crudely check idempotency
old_size = instance.configuration_id
call_command(
"vm", "create", subdomain, f"--owner={self.username}", "--flavor=large"
)
instance.refresh_from_db()
# verify new size is updated in DB
self.assertEqual(
str.strip(instance.configuration_id.name)
== str.strip(translate_sizes("large")),
"A customer has purchased a new instance. Please find the details below:"
in staff_notification.body,
True,
)
# verify new size is updated in repository
self.assertEqual(
str.strip(infra.translate_size(instance=instance))
== str.strip(infra.get_flavor(instance=instance)),
True,
)
call_command("vm", "delete", subdomain)
out = stdout.getvalue()
self.assertEqual(Instance.objects.filter(name=subdomain).exists(), False)
host_vars_dir = infra._host_vars_dir(subdomain)
self.assertEqual(host_vars_dir.exists(), False)
# run delete VM command to crudely check idempotency
call_command("vm", "delete", subdomain)
def test_worker(self):
subdomain = "gitea" # yes, gitea.hostea.org exists. will use it till I
# figure out how to use requests_mock within django
c = Client()
login_util(self, c, "accounts.home")
create_instance_util(
t=self, c=c, instance_name=subdomain, config=self.instance_config[0]
)
instance = Instance.objects.get(name=subdomain)
job = Job.objects.create(instance=instance, job_type=JobType.PING)
gitea_uri = Infra.get_gitea_uri(instance=instance)
print(f"mocking {gitea_uri}")
w = Worker(job=job)
w.start()
time.sleep(15)
self.assertEqual(w.is_alive(), False)
w.join()
self.assertEqual(
Job.objects.filter(instance=instance, job_type=JobType.PING).exists(), True
)

View File

@ -12,25 +12,30 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import os
import sh
import shutil
import yaml
import requests
from pathlib import Path
from threading import Thread, Event
from threading import Thread
from time import sleep
from django.utils.crypto import get_random_string
from django.template.loader import render_to_string
from django.contrib.auth import get_user_model
from django.core.mail import send_mail
from django.conf import settings
from git import Repo, Commit
from git.exc import InvalidGitRepositoryError
from payments import get_payment_model
from dash.models import Instance
from infrastructure.models import InstanceCreated, JobType, Job
logging.basicConfig()
logger = logging.getLogger(__name__)
class Worker(Thread):
def __init__(self, job: Job):
@ -38,12 +43,12 @@ class Worker(Thread):
super().__init__()
def run(self):
gitea_uri = Infra.get_gitea_uri(instance=self.job.instance)
forgejo_uri = Infra.get_forgejo_uri(instance=self.job.instance)
woodpecker = Infra.get_woodpecker_uri(instance=self.job.instance)
while True:
try:
print(f"[ping] Trying to reach {gitea_uri}")
resp = requests.get(gitea_uri)
print(f"[ping] Trying to reach {forgejo_uri}")
resp = requests.get(forgejo_uri)
if resp.status_code == 200:
break
except Exception:
@ -53,46 +58,86 @@ class Worker(Thread):
job = self.job
self.job = None
email = job.instance.owned_by.email
ctx = {
"forgejo_uri": forgejo_uri,
"woodpecker_uri": woodpecker,
"username": job.instance.owned_by.username,
}
body = render_to_string(
"infrastructure/emails/instance-created.txt",
context=ctx,
)
sender = settings.DEFAULT_FROM_EMAIL
send_mail(
subject="[Hostea] Your Hostea instance is now online!",
message=f"""
Hello,
The deployment job has run to completion and your Hostea instance is now online!
Credentials to admin account was sent in an earlier email, please contact
support if didn't receive it.
Gitea: {gitea_uri}
Woodpecker CI: {woodpecker}
""",
from_email="No reply Hostea<no-reply@exampl.org>", # TODO read from settings.py
subject="[Gna!] Your Gna! instance is now online!",
message=body,
from_email=f"No reply Gna!<{sender}>", # TODO read from settings.py
recipient_list=[email],
)
job.delete()
def create_vm_if_not_exists(instance: Instance) -> (str, Commit):
def create_vm_if_not_exists(instance: Instance) -> (str, str):
"""
Create VM utility. Gitea password is returned
Create VM utility. Forgejo password is returned
"""
def notify_staff(instance: Instance):
infra = Infra()
User = get_user_model()
forgejo_uri = Infra.get_forgejo_uri(instance=instance)
woodpecker = Infra.get_woodpecker_uri(instance=instance)
for staff in User.objects.filter(is_staff=True):
ctx = {
"forgejo_uri": forgejo_uri,
"woodpecker_uri": woodpecker,
"username": staff.username,
}
body = render_to_string(
"infrastructure/emails/staff-new-instance-alert.txt",
context=ctx,
)
sender = settings.DEFAULT_FROM_EMAIL
send_mail(
subject="[Gna!] New instance alert",
message=body,
from_email=f"No reply Gna!<{sender}>", # TODO read from settings.py
recipient_list=[staff.email],
)
infra = Infra()
if not InstanceCreated.objects.filter(instance=instance).exists():
(gitea_password, commit) = infra.add_vm(instance=instance)
(forgejo_password, commit) = infra.add_vm(instance=instance)
InstanceCreated.objects.create(instance=instance, created=True)
notify_staff(instance=instance)
job = Job.objects.create(instance=instance, job_type=str(JobType.PING))
Worker(job=job).start()
return (gitea_password, commit)
return (forgejo_password, commit)
else:
if str.strip(infra.get_flavor(instance=instance)) != str.strip(
infra.translate_size(instance=instance)
):
# Worker.init_global()
notify_staff(instance=instance)
return infra.add_vm(instance=instance)
return None
def delete_vm(instance: Instance):
infra = Infra()
Payment = get_payment_model()
for payment in Payment.objects.filter(
paid_by=instance.owned_by, instance_name=instance.name
):
payment.vm_deleted = True
payment.save()
infra.remove_vm(instance=instance)
if InstanceCreated.objects.filter(instance=instance).exists():
InstanceCreated.objects.get(instance=instance).delete()
@ -107,15 +152,17 @@ class Infra:
def __init__(self):
conf = settings.HOSTEA["INFRA"]["HOSTEA_REPO"]
self.repo_path = Path(conf["PATH"])
if not self.repo_path.exists():
os.makedirs(self.repo_path)
self._clone()
def _clone(self):
conf = settings.HOSTEA["INFRA"]["HOSTEA_REPO"]
ssh_cmd = f"/usr/bin/ssh -oStrictHostKeyChecking=no -i {conf['SSH_KEY']}"
self.env = {"GIT_SSH_COMMAND": ssh_cmd}
try:
self.repo = Repo(path=self.repo_path)
except InvalidGitRepositoryError:
self.repo = Repo.clone_from(conf["REMOTE"], self.repo_path, env=self.env)
self.git = sh.git.bake(_env={"GIT_SSH_COMMAND": ssh_cmd})
conf = settings.HOSTEA["INFRA"]["HOSTEA_REPO"]
if os.path.exists(self.repo_path):
shutil.rmtree(self.repo_path)
self.git.clone(conf["REMOTE"], self.repo_path)
self.git = self.git.bake("-C", self.repo_path)
def _host_vars_dir(self, subdomain: str) -> Path:
"""
@ -130,9 +177,9 @@ class Infra:
return self._host_vars_dir(subdomain=subdomain).joinpath("provision.yml")
@classmethod
def get_gitea_uri(cls, instance: Instance) -> str:
def get_forgejo_uri(cls, instance: Instance) -> str:
"""
Get an instance's Gitea URI
Get an instance's Forgejo URI
"""
base = settings.HOSTEA["INFRA"]["HOSTEA_DOMAIN"]
return f"https://{instance.name}.{base}"
@ -147,7 +194,7 @@ class Infra:
@classmethod
def get_woodpecker_uri(cls, instance: Instance) -> str:
"""
Get an instance's Gitea URI
Get an instance's Forgejo URI
"""
base = settings.HOSTEA["INFRA"]["HOSTEA_DOMAIN"]
return f"https://{cls._gen_woodpecker_hostname(instance=instance)}.{base}"
@ -164,12 +211,12 @@ class Infra:
return config["openstack_flavor"].split("{{ ")[1].split(" }}")[0]
return None
def _gitea_path(self, subdomain: str) -> Path:
def _forgejo_path(self, subdomain: str) -> Path:
"""
utility method: get gitea file for a subdomain
utility method: get forgejo file for a subdomain
"""
return self._host_vars_dir(subdomain=subdomain).joinpath("gitea.yml")
return self._host_vars_dir(subdomain=subdomain).joinpath("forgejo.yml")
def _backup_path(self, subdomain: str) -> Path:
"""
@ -203,30 +250,21 @@ class Infra:
f.write(content)
f.write("\n")
def _add_files(self, subdomain: str):
"""
Add all relevant files of a VM
"""
self.repo.git.add(str(self._host_vars_dir(subdomain=subdomain)))
self.repo.git.add(str(self._backup_path(subdomain=subdomain)))
self.repo.git.add(str(self._service_path(subdomain=subdomain)))
self.repo.git.add(str(self._hostscript_path(subdomain=subdomain)))
def _push(self, message):
self.git.add(".")
self.git.config("user.email", settings.HOSTEA["INSTANCE_MAINTAINER_CONTACT"])
self.git.config("user.name", "Hostea dashboard")
try:
self.git.commit("-m", f"dashboard: {message}")
except sh.ErrorReturnCode_1:
logger.debug("no change")
else:
self.git.push("origin", "master")
return self._sha()
def _commit(self, action: str, subdomain: str) -> Commit:
"""
Commit changes to a VM configuration
"""
self._add_files(subdomain=subdomain)
return self.repo.git.commit(
message=f"{action} VM {subdomain}",
author="Dashboard Bot <bot@dashboard.hostea.org>",
)
def _pull(self):
self.repo.git.fetch(env=self.env)
# TODO: switch to using Git cmd
def _sha(self):
sha = self.git("rev-parse", "origin/master")
return str(sha).strip()
@staticmethod
def translate_size(instance: Instance) -> str:
@ -242,13 +280,12 @@ class Infra:
return "openstack_flavor_large"
return instance.configuration_id.name
def add_vm(self, instance: Instance) -> (str, Commit):
def add_vm(self, instance: Instance) -> (str, str):
"""
Add new VM to infrastructure repository
The gitea user password is returned
The forgejo user password is returned
"""
self._pull()
subdomain = instance.name
host_vars_dir = self._host_vars_dir(subdomain)
@ -261,22 +298,22 @@ class Infra:
os.makedirs(hostscript_path)
woodpecker_agent_secret = get_random_string(64)
gitea_password = get_random_string(20)
forgejo_password = get_random_string(20)
ctx = {
"woodpecker_agent_secret": woodpecker_agent_secret,
"woodpecker_hostname": self._gen_woodpecker_hostname(instance=instance),
"woodpecker_admins": f"{instance.owned_by.username}",
"gitea_email": instance.owned_by.email,
"gitea_password": gitea_password,
"forgejo_email": instance.owned_by.email,
"forgejo_password": forgejo_password,
"subdomain": subdomain,
}
gitea = self._gitea_path(subdomain)
with open(gitea, "w+", encoding="utf-8") as f:
forgejo = self._forgejo_path(subdomain)
with open(forgejo, "w+", encoding="utf-8") as f:
f.write(
render_to_string(
"infrastructure/yml/gitea.yml",
"infrastructure/yml/forgejo.yml",
context=ctx,
)
)
@ -319,29 +356,26 @@ class Infra:
),
)
commit = self._commit(action="add", subdomain=subdomain)
self.repo.git.push(env=self.env)
return (gitea_password, commit)
commit = self._push(f"add vm {subdomain}")
return (forgejo_password, commit)
def remove_vm(self, instance: Instance):
"""
Remove a VM from infrastructure repository
"""
self._pull()
subdomain = instance.name
try:
host_vars_dir = self._host_vars_dir(subdomain)
if os.path.exists(host_vars_dir):
shutil.rmtree(host_vars_dir)
backup = self._backup_path(subdomain)
if os.path.exists(backup):
os.remove(backup)
service = self._service_path(subdomain)
if os.path.exists(service):
os.remove(service)
except FileNotFoundError:
pass
hostscript = self._hostscript_path(subdomain)
with open(hostscript, "w+", encoding="utf-8") as f:
@ -354,5 +388,4 @@ class Infra:
context={"subdomain": subdomain},
),
)
self._commit(action="rm", subdomain=subdomain)
self.repo.git.push(env=self.env)
return self._push(f"rm vm {subdomain}")

View File

@ -18,8 +18,10 @@ from django.contrib.auth import authenticate, login, logout
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.template.loader import render_to_string
from django.views.decorators.csrf import csrf_protect
from django.core.mail import send_mail
from django.conf import settings
from django.urls import reverse
from accounts.decorators import confirm_access
@ -51,28 +53,28 @@ def create_instance(request, instance_name: str):
res = create_vm_if_not_exists(instance=instance)
if res is not None:
(gitea_password, commit) = res
(forgejo_password, commit) = res
ctx = {
"username": request.user.username,
"forgejo_password": forgejo_password,
"forgejo_uri": Infra.get_forgejo_uri(instance=instance),
}
body = render_to_string(
"infrastructure/emails/forgejo-creds.txt",
context=ctx,
)
sender = settings.DEFAULT_FROM_EMAIL
send_mail(
subject="[Hostea] Gitea admin credentials",
message=f"""
Congratulations on your new Gitea instance!\n
You can use the following credentials to log into an admin account on
your new Gitea instance. Great powers come with great responsibilities,
so use the admin credentials wisely. When in doubt, consult the Gitea
docs or contact support!\n
- username : root
- password: {gitea_password}
""",
from_email="No reply Hostea<no-reply@exampl.org>", # TODO read from settings.py
subject="[Gna!] Forgejo admin credentials",
message=body,
from_email=f"No reply Gna!<{sender}>", # TODO read from settings.py
recipient_list=[request.user.email],
)
ctx = {
"gitea_password": gitea_password,
"gitea_uri": Infra.get_gitea_uri(instance=instance),
}
return render(request, "infrastructure/html/create.html", ctx)
return HttpResponse()

View File

@ -16,7 +16,7 @@ init() {
sleep 5
# wait_for_env
fi
gitea_root
forgejo_root
support_repo_init
fleet_repo_init
}

View File

@ -3,10 +3,10 @@ import argparse
from requests import Session
def gitea_from_args(args, c: Session):
from .gitea import Gitea
def forgejo_from_args(args, c: Session):
from .forgejo import Forgejo
return Gitea(
return Forgejo(
host=args.host,
username=args.username,
password=args.password,
@ -15,7 +15,7 @@ def gitea_from_args(args, c: Session):
)
class Gitea:
class Forgejo:
def __init__(self, parser, c: Session):
self.c = c
self.parser = parser
@ -29,56 +29,56 @@ class Gitea:
def __add_credentials_parser(self, parser):
group = parser.add_argument_group("credentials", "User credentials")
group.add_argument("username", type=str, help="Gitea user's username")
group.add_argument("password", type=str, help="Gitea user's password")
group.add_argument("email", type=str, help="Gitea user's email")
group.add_argument("host", type=str, help="URI at which Gitea is running")
group.add_argument("username", type=str, help="Forgejo user's username")
group.add_argument("password", type=str, help="Forgejo user's password")
group.add_argument("email", type=str, help="Forgejo user's email")
group.add_argument("host", type=str, help="URI at which Forgejo is running")
def install(self):
def run(args, c: Session):
gitea = gitea_from_args(args, c=c)
gitea.install()
forgejo = forgejo_from_args(args, c=c)
forgejo.install()
self.install_parser = self.subparser.add_parser(
name="install", description="Install Gitea", help="Install Gitea"
name="install", description="Install Forgejo", help="Install Forgejo"
)
self.__add_credentials_parser(self.install_parser)
self.install_parser.set_defaults(func=run)
def register(self):
def run(args, c: Session):
gitea = gitea_from_args(args, c=c)
gitea.register()
forgejo = forgejo_from_args(args, c=c)
forgejo.register()
self.register_parser = self.subparser.add_parser(
name="register",
description="Gitea user registration",
help="Register a user on Gitea",
description="Forgejo user registration",
help="Register a user on Forgejo",
)
self.__add_credentials_parser(self.register_parser)
self.register_parser.set_defaults(func=run)
def login(self):
def run(args, c: Session):
gitea = gitea_from_args(args, c=c)
gitea.login()
forgejo = forgejo_from_args(args, c=c)
forgejo.login()
self.login_parser = self.subparser.add_parser(
name="login", description="Gitea user login", help="Login on Gitea"
name="login", description="Forgejo user login", help="Login on Forgejo"
)
self.__add_credentials_parser(self.login_parser)
self.login_parser.set_defaults(func=run)
def create_repository(self):
def run(args, c: Session):
gitea = gitea_from_args(args, c=c)
gitea.login()
gitea.create_repository(name=args.repo_name)
forgejo = forgejo_from_args(args, c=c)
forgejo.login()
forgejo.create_repository(name=args.repo_name)
self.create_repository_parser = self.subparser.add_parser(
name="create_repo",
description="Create repository on Gitea",
help="Create repository on Gitea",
description="Create repository on Forgejo",
help="Create repository on Forgejo",
)
self.__add_credentials_parser(self.create_repository_parser)
self.create_repository_parser.set_defaults(func=run)
@ -88,10 +88,10 @@ class Gitea:
def install_sso(self):
def run(args, c: Session):
gitea = gitea_from_args(args, c=c)
gitea.login()
forgejo = forgejo_from_args(args, c=c)
forgejo.login()
print(f"CLIENT ID: {args.client_id}")
gitea.install_sso(
forgejo.install_sso(
sso_name=args.sso_name,
client_id=args.client_id,
client_secret=args.client_secret,
@ -100,8 +100,8 @@ class Gitea:
self.install_sso_parser = self.subparser.add_parser(
name="install_sso",
description="Install SSO on Gitea",
help="Install SSO on Gitea",
description="Install SSO on Forgejo",
help="Install SSO on Forgejo",
)
self.__add_credentials_parser(self.install_sso_parser)
self.install_sso_parser.add_argument(
@ -123,14 +123,14 @@ class Gitea:
def add_deploy_key(self):
def run(args, c: Session):
gitea = gitea_from_args(args, c=c)
gitea.login()
gitea.add_deploy_key(repo=args.repo_name, key=args.key_file)
forgejo = forgejo_from_args(args, c=c)
forgejo.login()
forgejo.add_deploy_key(repo=args.repo_name, key=args.key_file)
self.add_deploy_key_parser = self.subparser.add_parser(
name="add_deploy_key",
description="Create repository on Gitea",
help="Add deploy key to a repository on Gitea",
description="Create repository on Forgejo",
help="Add deploy key to a repository on Forgejo",
)
self.__add_credentials_parser(self.add_deploy_key_parser)
self.add_deploy_key_parser.add_argument(
@ -203,21 +203,21 @@ class Hostea:
def support(self):
def run(args, c: Session):
from .gitea import GiteaSSO
from .forgejo import ForgejoSSO
dash = dash_from_args(args, c=c)
dash.login()
gitea = GiteaSSO(
forgejo = ForgejoSSO(
username=dash.username,
email=dash.email,
gitea_host=args.gitea_host,
hostea_org=args.gitea_hostea_org,
forgejo_host=args.forgejo_host,
hostea_org=args.forgejo_hostea_org,
support_repo=args.support_repo,
c=c,
)
dash.new_ticket(gitea.new_issues_uri)
gitea.new_issue()
dash.new_ticket(forgejo.new_issues_uri)
forgejo.new_issue()
self.support_parser = self.subparser.add_parser(
name="support",
@ -226,12 +226,12 @@ class Hostea:
)
self.__add_credentials_parser(self.support_parser)
self.support_parser.add_argument(
"gitea_host", type=str, help="URI at which Gitea is running"
"forgejo_host", type=str, help="URI at which Forgejo is running"
)
self.support_parser.add_argument(
"gitea_hostea_org",
"forgejo_hostea_org",
type=str,
help="Hostea namespace(username/org) on Gitea, where support repository is hosted",
help="Hostea namespace(username/org) on Forgejo, where support repository is hosted",
)
self.support_parser.add_argument(
"support_repo", type=str, help="support repository name"
@ -245,28 +245,28 @@ class Cli:
c = Session()
self.c = c
self.parser = argparse.ArgumentParser(
description="Install and Bootstrap Gitea and Hostea Dashboard"
description="Install and Bootstrap Forgejo and Hostea Dashboard"
)
self.subparser = self.parser.add_subparsers()
self.check_env()
self.gitea()
self.forgejo()
self.hostea()
def __add_credentials_parser(self, parser):
group = parser.add_argument_group("credentials", "User credentials")
group.add_argument("username", type=str, help="Gitea user's username")
group.add_argument("password", type=str, help="Gitea user's password")
group.add_argument("email", type=str, help="Gitea user's email")
group.add_argument("username", type=str, help="Forgejo user's username")
group.add_argument("password", type=str, help="Forgejo user's password")
group.add_argument("email", type=str, help="Forgejo user's email")
def check_env(self):
def run(args, c: Session):
from .gitea import Gitea
from .forgejo import Forgejo
from .hostea import Hostea
Hostea.check_online(
dashboard_host=args.hostea_host, maildev_host=args.maildev_host
)
Gitea.check_online(host=args.gitea_host)
Forgejo.check_online(host=args.forgejo_host)
self.check_env_parser = self.subparser.add_parser(
name="check_env",
@ -275,7 +275,7 @@ class Cli:
)
self.check_env_parser.add_argument(
"gitea_host", type=str, help="URI at which Gitea is running"
"forgejo_host", type=str, help="URI at which Forgejo is running"
)
self.check_env_parser.add_argument(
@ -295,13 +295,13 @@ class Cli:
)
Hostea(parser=self.hostea, c=self.c)
def gitea(self):
self.gitea = self.subparser.add_parser(
name="gitea",
description="Gitea",
help="Gitea-related functionality",
def forgejo(self):
self.forgejo = self.subparser.add_parser(
name="forgejo",
description="Forgejo",
help="Forgejo-related functionality",
)
Gitea(parser=self.gitea, c=self.c)
Forgejo(parser=self.forgejo, c=self.c)
def parse(self):
return self.parser.parse_args()

View File

@ -13,7 +13,7 @@ class ParseCSRF(HTMLParser):
# return cls(name="csrfmiddlewaretoken")
#
# @classmethod
# def gitea_parser(cls) -> "ParseCSRF":
# def forgejo_parser(cls) -> "ParseCSRF":
# return cls(name="_csrf")
#
def handle_starttag(self, tag: str, attrs: (str, str)):

View File

@ -10,15 +10,15 @@ import requests
from .csrf import ParseCSRF
# GITEA_USER = "root"
# GITEA_EMAIL = "root@example.com"
# GITEA_PASSWORD = "foobarpassword"
# FORGEJO_USER = "root"
# FORGEJO_EMAIL = "root@example.com"
# FORGEJO_PASSWORD = "foobarpassword"
# HOST = "http://localhost:8080"
#
# REPOS = []
class Gitea:
class Forgejo:
def __init__(self, host: str, username: str, password: str, email: str, c: Session):
self.host = host
self.username = username
@ -48,7 +48,7 @@ class Gitea:
@staticmethod
def check_online(host: str):
"""
Check if Gitea instance is online
Check if Forgejo instance is online
"""
count = 0
parsed = urlparse(host)
@ -67,7 +67,7 @@ class Gitea:
def install(self):
"""
Install Gitea, first form that a user sees when a new instance is
Install Forgejo, first form that a user sees when a new instance is
deployed
"""
# cwd = os.environ.get("PWD")
@ -77,12 +77,12 @@ class Gitea:
"db_host": "localhost:3306",
"db_user": "root",
"db_passwd": "",
"db_name": "gitea",
"db_name": "forgejo",
"ssl_mode": "disable",
"db_schema": "",
"charset": "utf8",
"db_path": "/data/gitea/gitea.db",
"app_name": "Gitea:+Git+with+a+cup+of+tea",
"app_name": "Forgejo:+Beyond+Coding+We+Forge",
"repo_root_path": "/data/git/repositories",
"lfs_root_path": "/data/git/lfs",
"run_user": "git",
@ -316,26 +316,26 @@ class ParseSSOLogin(HTMLParser):
return
class GiteaSSO:
class ForgejoSSO:
def __init__(
self,
username: str,
email: str,
gitea_host: str,
forgejo_host: str,
hostea_org: str,
support_repo: str,
c: Session,
):
self.c = c
self.username = username
self.gitea_host = gitea_host
self.forgejo_host = forgejo_host
self.hostea_org = hostea_org
self.support_repo = support_repo
self.email = email
self.__csrf_key = "_csrf"
url = urlparse(self.gitea_host)
url = urlparse(self.forgejo_host)
repo = f"{self.hostea_org}/{self.support_repo}"
issues = f"{repo}/issues"
new_issues = f"{issues}/new"
@ -371,8 +371,8 @@ class GiteaSSO:
parser = ParseSSOLogin()
parser.feed(resp.text)
url = urlparse(self.gitea_host)
## SSO URL in Gitea login page
url = urlparse(self.forgejo_host)
## SSO URL in Forgejo login page
sso = urlunparse((url.scheme, url.netloc, parser.url, "", "", ""))
# redirects are enabled to for a cleaner implementation. Commented out

View File

@ -11,12 +11,12 @@ is_ci(){
if is_ci
then
MAILDEV_URL="http://smtp:1080"
GITEA_URL="http://gitea:3000"
GITEA_SSH_URL="ssh://git@gitea:22"
FORGEJO_URL="http://forgejo:3000"
FORGEJO_SSH_URL="ssh://git@forgejo:22"
else
MAILDEV_URL="http://localhost:1080"
GITEA_URL="http://localhost:3000"
GITEA_SSH_URL="ssh://git@localhost:22"
FORGEJO_URL="http://localhost:3000"
FORGEJO_SSH_URL="ssh://git@localhost:22"
fi
readonly DASHBOARD_URL="http://localhost:8000"
@ -27,22 +27,22 @@ readonly DASHBOARD_OIDC_DISCOVERY_URL="$DASHBOARD_URL/o/.well-known/openid-confi
readonly DASHBOARD_ADMIN_USERNAME=root
readonly DASHBOARD_ADMIN_PASSWORD=supercomplicatedpassword
readonly DASHBOARD_ADMIN_EMAIL="$DASHBOARD_ADMIN_USERNAME@dash.example.org"
readonly DASHBOARD_OIDC_APP_NAME=hostea-gitea
readonly DASHBOARD_OIDC_APP_NAME=hostea-forgejo
readonly GITEA_ROOT_USERNAME=root
readonly GITEA_ROOT_EMAIL="$GITEA_ROOT_USERNAME@example.org"
readonly GITEA_ROOT_PASSOWRD=supercomplicatedpassword
readonly GITEA_HOSTEA_SSO_NAME=hostea-sso
readonly GITEA_OIDC_CALLBACK="$GITEA_URL/user/oauth2/$GITEA_HOSTEA_SSO_NAME/callback"
readonly GITEA_HOSTEA_FLEET_DEPLOY_KEY="$(realpath tests/fleet-deploy-key.pub)"
readonly GITEA_HOSTEA_FLEET_DEPLOY_KEY_PRIVATE="$(realpath tests/fleet-deploy-key)"
readonly FORGEJO_ROOT_USERNAME=root
readonly FORGEJO_ROOT_EMAIL="$FORGEJO_ROOT_USERNAME@example.org"
readonly FORGEJO_ROOT_PASSOWRD=supercomplicatedpassword
readonly FORGEJO_HOSTEA_SSO_NAME=hostea-sso
readonly FORGEJO_OIDC_CALLBACK="$FORGEJO_URL/user/oauth2/$FORGEJO_HOSTEA_SSO_NAME/callback"
readonly FORGEJO_HOSTEA_FLEET_DEPLOY_KEY="$(realpath tests/fleet-deploy-key.pub)"
readonly FORGEJO_HOSTEA_FLEET_DEPLOY_KEY_PRIVATE="$(realpath tests/fleet-deploy-key)"
readonly GITEA_HOSTEA_USERNAME=hostea
readonly GITEA_HOSTEA_PASSWORD=supercomplicatedpassword
readonly GITEA_HOSTEA_EMAIL="$GITEA_HOSTEA_USERNAME@example.org"
readonly GITEA_HOSTEA_SUPPORT_REPO="support"
readonly GITEA_HOSTEA_FLEET_REPO="fleet"
readonly GITEA_HOSTEA_FLEET_REPO_REMOTE="$GITEA_SSH_URL/$GITEA_HOSTEA_USERNAME/$GITEA_HOSTEA_FLEET_REPO.git"
readonly FORGEJO_HOSTEA_USERNAME=hostea
readonly FORGEJO_HOSTEA_PASSWORD=supercomplicatedpassword
readonly FORGEJO_HOSTEA_EMAIL="$FORGEJO_HOSTEA_USERNAME@example.org"
readonly FORGEJO_HOSTEA_SUPPORT_REPO="support"
readonly FORGEJO_HOSTEA_FLEET_REPO="fleet"
readonly FORGEJO_HOSTEA_FLEET_REPO_REMOTE="$FORGEJO_SSH_URL/$FORGEJO_HOSTEA_USERNAME/$FORGEJO_HOSTEA_FLEET_REPO.git"
readonly HOSTEA_CUSTOMER_USERNAME=batman
readonly HOSTEA_CUSTOMER_PASSWORD=supercomplicatedpassword
@ -53,7 +53,7 @@ OIDC_CLIENT_SECRET=""
wait_for_env() {
python -m integration \
check_env $GITEA_URL $DASHBOARD_URL $MAILDEV_URL
check_env $FORGEJO_URL $DASHBOARD_URL $MAILDEV_URL
}
# create OIDC app on Hostea Dashboard
@ -67,90 +67,90 @@ oidc_dashboard_init() {
resp=$(python manage.py create_oidc \
$DASHBOARD_OIDC_APP_NAME $DASHBOARD_ADMIN_USERNAME \
$GITEA_OIDC_CALLBACK)
$FORGEJO_OIDC_CALLBACK)
OIDC_CLIENT_ID=$(echo $resp | cut -d ":" -f 2 | cut -d " " -f 2)
OIDC_CLIENT_SECRET=$(echo $resp | cut -d ":" -f 3 | cut -d " " -f 2)
}
# register root user on Gitea to simulate Hoste admin and integrate SSO
gitea_root(){
# register root user on Forgejo to simulate Hoste admin and integrate SSO
forgejo_root(){
python -m integration \
gitea install \
$GITEA_ROOT_USERNAME $GITEA_ROOT_PASSOWRD \
$GITEA_ROOT_EMAIL \
$GITEA_URL
forgejo install \
$FORGEJO_ROOT_USERNAME $FORGEJO_ROOT_PASSOWRD \
$FORGEJO_ROOT_EMAIL \
$FORGEJO_URL
python -m integration \
gitea register \
$GITEA_ROOT_USERNAME $GITEA_ROOT_PASSOWRD \
$GITEA_ROOT_EMAIL \
$GITEA_URL
forgejo register \
$FORGEJO_ROOT_USERNAME $FORGEJO_ROOT_PASSOWRD \
$FORGEJO_ROOT_EMAIL \
$FORGEJO_URL
python -m integration \
gitea login \
$GITEA_ROOT_USERNAME $GITEA_ROOT_PASSOWRD \
$GITEA_ROOT_EMAIL \
$GITEA_URL
forgejo login \
$FORGEJO_ROOT_USERNAME $FORGEJO_ROOT_PASSOWRD \
$FORGEJO_ROOT_EMAIL \
$FORGEJO_URL
# python -m integration \
# gitea install_sso \
# $GITEA_ROOT_USERNAME $GITEA_ROOT_PASSOWRD \
# $GITEA_ROOT_EMAIL \
# $GITEA_URL \
# $GITEA_HOSTEA_SSO_NAME \
# forgejo install_sso \
# $FORGEJO_ROOT_USERNAME $FORGEJO_ROOT_PASSOWRD \
# $FORGEJO_ROOT_EMAIL \
# $FORGEJO_URL \
# $FORGEJO_HOSTEA_SSO_NAME \
# $OIDC_CLIENT_ID $OIDC_CLIENT_SECRET \
# $DASHBOARD_OIDC_DISCOVERY_URL
}
# register user "Hostea" on Gitea and create support repository
# register user "Hostea" on Forgejo and create support repository
support_repo_init() {
python -m integration \
gitea register \
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
$GITEA_HOSTEA_EMAIL \
$GITEA_URL
forgejo register \
$FORGEJO_HOSTEA_USERNAME $FORGEJO_HOSTEA_PASSWORD \
$FORGEJO_HOSTEA_EMAIL \
$FORGEJO_URL
python -m integration \
gitea login \
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
$GITEA_HOSTEA_EMAIL \
$GITEA_URL
forgejo login \
$FORGEJO_HOSTEA_USERNAME $FORGEJO_HOSTEA_PASSWORD \
$FORGEJO_HOSTEA_EMAIL \
$FORGEJO_URL
python -m integration \
gitea create_repo \
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
$GITEA_HOSTEA_EMAIL \
$GITEA_URL \
$GITEA_HOSTEA_SUPPORT_REPO
forgejo create_repo \
$FORGEJO_HOSTEA_USERNAME $FORGEJO_HOSTEA_PASSWORD \
$FORGEJO_HOSTEA_EMAIL \
$FORGEJO_URL \
$FORGEJO_HOSTEA_SUPPORT_REPO
}
new_fleet_repo_init() {
python -m integration \
gitea create_repo \
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
$GITEA_HOSTEA_EMAIL \
$GITEA_URL \
forgejo create_repo \
$FORGEJO_HOSTEA_USERNAME $FORGEJO_HOSTEA_PASSWORD \
$FORGEJO_HOSTEA_EMAIL \
$FORGEJO_URL \
$1
python -m integration \
gitea add_deploy_key \
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
$GITEA_HOSTEA_EMAIL \
$GITEA_URL \
forgejo add_deploy_key \
$FORGEJO_HOSTEA_USERNAME $FORGEJO_HOSTEA_PASSWORD \
$FORGEJO_HOSTEA_EMAIL \
$FORGEJO_URL \
$1 \
$GITEA_HOSTEA_FLEET_DEPLOY_KEY
$FORGEJO_HOSTEA_FLEET_DEPLOY_KEY
tmp_dir=$(mktemp -d)
pushd $tmp_dir
echo "init" >> README
git init
if is_ci
then
git config --global user.email "${CI_COMMIT_AUTHOR_EMAIL}"
git config --global user.name "${CI_COMMIT_AUTHOR}"
chmod 600 $GITEA_HOSTEA_FLEET_DEPLOY_KEY_PRIVATE
git config user.email "hostea-dashobard-test@example.org"
git config user.name "hoste-dashobard-test"
chmod 600 $FORGEJO_HOSTEA_FLEET_DEPLOY_KEY_PRIVATE
fi
git init
git add README
git commit -m "init"
REMOTE="$GITEA_SSH_URL/$GITEA_HOSTEA_USERNAME/$1.git"
REMOTE="$FORGEJO_SSH_URL/$FORGEJO_HOSTEA_USERNAME/$1.git"
git remote add origin $REMOTE
GIT_SSH_COMMAND="/usr/bin/ssh -oStrictHostKeyChecking=no -i $GITEA_HOSTEA_FLEET_DEPLOY_KEY_PRIVATE" \
GIT_SSH_COMMAND="/usr/bin/ssh -oStrictHostKeyChecking=no -i $FORGEJO_HOSTEA_FLEET_DEPLOY_KEY_PRIVATE" \
git push --set-upstream origin master
popd
rm -rf $tmp_dir
@ -158,20 +158,20 @@ new_fleet_repo_init() {
}
# register user "Hostea" on Gitea and create support repository
# register user "Hostea" on Forgejo and create support repository
fleet_repo_init() {
python -m integration \
gitea register \
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
$GITEA_HOSTEA_EMAIL \
$GITEA_URL || true
forgejo register \
$FORGEJO_HOSTEA_USERNAME $FORGEJO_HOSTEA_PASSWORD \
$FORGEJO_HOSTEA_EMAIL \
$FORGEJO_URL || true
python -m integration \
gitea login \
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
$GITEA_HOSTEA_EMAIL \
$GITEA_URL
forgejo login \
$FORGEJO_HOSTEA_USERNAME $FORGEJO_HOSTEA_PASSWORD \
$FORGEJO_HOSTEA_EMAIL \
$FORGEJO_URL
new_fleet_repo_init $GITEA_HOSTEA_FLEET_REPO
new_fleet_repo_init $FORGEJO_HOSTEA_FLEET_REPO
}
@ -192,8 +192,8 @@ hostea_customer_simulation() {
$HOSTEA_CUSTOMER_USERNAME $HOSTEA_CUSTOMER_PASSWORD \
$HOSTEA_CUSTOMER_EMAIL \
$DASHBOARD_URL \
$GITEA_URL \
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_SUPPORT_REPO
$FORGEJO_URL \
$FORGEJO_HOSTEA_USERNAME $FORGEJO_HOSTEA_SUPPORT_REPO
}
setup_env() {

View File

@ -6,7 +6,7 @@ import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dashboard.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard.settings")
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
@ -18,5 +18,5 @@ def main():
execute_from_command_line(sys.argv)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -1,6 +1,7 @@
asgiref==3.5.0
astroid==2.9.3
black==22.1.0
Brotli==1.0.9
certifi==2022.5.18.1
cffi==1.15.0
charset-normalizer==2.0.12
@ -14,10 +15,9 @@ django-oauth-toolkit==2.0.0
django-payments==1.0.0
django-phonenumber-field==6.3.0
djangorestframework==3.13.1
gitdb==4.0.9
GitPython==3.1.27
greenlet==1.1.2
greenlet==1.1.3.post0
idna==3.3
install==1.3.5
isort==5.10.1
jedi==0.18.1
jwcrypto==1.3.1
@ -38,6 +38,7 @@ pytz==2022.1
PyYAML==6.0
requests==2.27.1
six==1.16.0
sh==1.14.2
smmap==5.0.0
sqlparse==0.4.2
stripe==3.4.0
@ -45,4 +46,5 @@ tblib==1.7.0
toml==0.10.2
tomli==2.0.1
urllib3==1.26.9
whitenoise==6.2.0
wrapt==1.13.3

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Some files were not shown because too many files have changed in this diff Show More