feat: notify user on instance creation #14
|
@ -4,7 +4,7 @@ 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/fleet.git
|
||||
- HOSTEA_INFRA_HOSTEA_REPO_REMOTE=ssh://git@gitea:22/hostea/
|
||||
- HOSTEA_META_GITEA_INSTANCE=http://gitea:3000
|
||||
commands:
|
||||
- export HOSTEA_INFRA_HOSTEA_REPO_SSH_KEY="$(realpath ./tests/fleet-deploy-key)"
|
||||
|
|
|
@ -2,20 +2,6 @@
|
|||
<div class="footer__container">
|
||||
<div class="footer__column">
|
||||
<span class="license__conatiner">
|
||||
<a class="license__link" rel="noreferrer" href="/docs" target="_blank"
|
||||
>Docs</a
|
||||
>
|
||||
<span class="footer__column-divider--mobile-visible">|</span>
|
||||
<a
|
||||
class="license__link"
|
||||
rel="noreferrer"
|
||||
href="https://www.eff.org/issues/do-not-track/amp/"
|
||||
target="_blank"
|
||||
>No AMP</a
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
<div class="footer__column">
|
||||
<a
|
||||
href="/"
|
||||
class="footer__link"
|
||||
|
@ -23,6 +9,21 @@
|
|||
rel="noopener"
|
||||
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"
|
||||
> 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
|
||||
|
@ -30,12 +31,12 @@
|
|||
<div class="footer__column-divider">|</div>
|
||||
<a
|
||||
class="footer__link"
|
||||
href="{{ footer.source_code }}"
|
||||
href="{{ footer.source_code.link }}"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
title="Source Code"
|
||||
>
|
||||
v{{ footer.version }}-{{ footer.git_hash }}
|
||||
{{ footer.source_code.text }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -23,6 +23,7 @@ from django.http import HttpResponse
|
|||
from django.views.decorators.csrf import csrf_protect
|
||||
from django.urls import reverse
|
||||
|
||||
from dash.utils import footer_ctx
|
||||
|
||||
from .models import AccountConfirmChallenge
|
||||
from .utils import send_verification_email, ConfirmAccess
|
||||
|
@ -35,6 +36,7 @@ def login_view(request):
|
|||
def default_login_ctx():
|
||||
return {
|
||||
"title": "Login",
|
||||
"footer": footer_ctx(),
|
||||
}
|
||||
|
||||
if request.method == "GET":
|
||||
|
@ -102,6 +104,7 @@ def register_view(request):
|
|||
"title": "Register",
|
||||
"username": username,
|
||||
"email": username,
|
||||
"footer": footer_ctx(),
|
||||
}
|
||||
|
||||
if request.method == "GET":
|
||||
|
@ -213,6 +216,7 @@ def sudo(request):
|
|||
def default_login_ctx():
|
||||
return {
|
||||
"title": "Confirm Access",
|
||||
"footer": footer_ctx(),
|
||||
}
|
||||
|
||||
if request.method == "GET":
|
||||
|
|
|
@ -26,7 +26,7 @@ from django.conf import settings
|
|||
from payments import get_payment_model, RedirectNeeded, PaymentStatus
|
||||
|
||||
from accounts.tests import register_util, login_util
|
||||
from dash.tests import create_configurations, create_instance_util
|
||||
from dash.tests import create_configurations, create_instance_util, infra_custom_config
|
||||
from dash.models import Instance
|
||||
|
||||
from .models import Payment
|
||||
|
@ -43,55 +43,21 @@ class BillingTest(TestCase):
|
|||
register_util(t=self, username=self.username)
|
||||
create_configurations(t=self)
|
||||
|
||||
@override_settings(HOSTEA=infra_custom_config(test_name="test_payments"))
|
||||
def test_payments(self):
|
||||
c = Client()
|
||||
login_util(self, c, "accounts.home")
|
||||
instance_name = "test_create_instance_renders"
|
||||
instance_name = "test_payments"
|
||||
create_instance_util(
|
||||
t=self, c=c, instance_name=instance_name, config=self.instance_config[0]
|
||||
)
|
||||
|
||||
instance = Instance.objects.get(name=instance_name)
|
||||
|
||||
payment_uri = reverse("billing.invoice.generate", args=(instance_name,))
|
||||
|
||||
# generate invoice
|
||||
resp = c.get(payment_uri)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
invoice_uri = resp.headers["Location"]
|
||||
self.assertEqual("invoice/payment/" in invoice_uri, True)
|
||||
|
||||
# try to generate duplicate invoice, but should get redirected to previous invoice
|
||||
resp = c.get(payment_uri)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(invoice_uri == resp.headers["Location"], True)
|
||||
|
||||
# check if invoice details page is displaying the invoice
|
||||
# if payment is yet to be made:
|
||||
# template will show payment button
|
||||
# else:
|
||||
# template will show payment date
|
||||
resp = c.get(invoice_uri)
|
||||
self.assertEqual(str.encode(instance_name) in resp.content, True)
|
||||
self.assertEqual(
|
||||
str.encode(str(self.instance_config[0].rent)) in resp.content, True
|
||||
)
|
||||
self.assertEqual(str.encode("Paid on") in resp.content, False)
|
||||
|
||||
# check if the unpaid invoice is displayed in the pending invoice view
|
||||
resp = c.get(reverse("billing.invoice.pending"))
|
||||
self.assertEqual(str.encode(invoice_uri) in resp.content, True)
|
||||
|
||||
self.assertEqual(payment_fullfilled(instance=instance), False)
|
||||
|
||||
# simulate payment. There's probably a better way to do this
|
||||
payment = get_payment_model().objects.get(paid_by=self.user)
|
||||
payment.status = PaymentStatus.CONFIRMED
|
||||
payment.save()
|
||||
|
||||
self.assertEqual(payment_fullfilled(instance=instance), True)
|
||||
|
||||
#
|
||||
payment = get_payment_model().objects.get(paid_by=self.user)
|
||||
invoice_uri = reverse("billing.invoice.details", args=(payment.public_ref,))
|
||||
|
||||
# check if paid invoice is listed in paid invoice list view
|
||||
resp = c.get(reverse("billing.invoice.paid"))
|
||||
|
@ -111,6 +77,7 @@ class BillingTest(TestCase):
|
|||
|
||||
# try to generate an invoice for the second time on the same VM
|
||||
# shouldn't be possible since payment is already made for the duration
|
||||
payment_uri = reverse("billing.invoice.generate", args=(instance.name,))
|
||||
resp = c.get(payment_uri)
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ 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
|
||||
|
||||
|
||||
def default_ctx(title: str, username: str):
|
||||
|
@ -33,6 +34,7 @@ def default_ctx(title: str, username: str):
|
|||
return {
|
||||
"title": title,
|
||||
"username": username,
|
||||
"footer": footer_ctx(),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -12,6 +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>
|
||||
|
||||
<form
|
||||
action="{% url 'dash.instances.delete' name=instance.name %}"
|
||||
|
|
|
@ -12,12 +12,20 @@
|
|||
#
|
||||
# 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 subprocess
|
||||
import shutil
|
||||
import os
|
||||
from time import sleep
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils.http import urlencode
|
||||
from django.urls import reverse
|
||||
from django.test import TestCase, Client, override_settings
|
||||
from django.conf import settings
|
||||
from django.db.utils import IntegrityError
|
||||
from payments import get_payment_model, RedirectNeeded, PaymentStatus
|
||||
|
||||
from accounts.tests import login_util, register_util
|
||||
|
||||
|
@ -44,6 +52,28 @@ def create_configurations(t: TestCase):
|
|||
)
|
||||
|
||||
|
||||
def infra_custom_config(test_name: str):
|
||||
def create_fleet_repo(test_name: str):
|
||||
subprocess.run(
|
||||
["./integration/ci.sh", "new_fleet_repo", test_name],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
sleep(10)
|
||||
|
||||
create_fleet_repo(test_name=test_name)
|
||||
|
||||
c = settings.HOSTEA
|
||||
path = Path(f"/tmp/hostea/dashboard/{test_name}/repo")
|
||||
if path.exists():
|
||||
shutil.rmtree(path)
|
||||
c["INFRA"]["HOSTEA_REPO"]["PATH"] = str(path)
|
||||
remote_base = os.environ.get("HOSTEA_INFRA_HOSTEA_REPO_REMOTE")
|
||||
c["INFRA"]["HOSTEA_REPO"]["REMOTE"] = f"{remote_base}{test_name}.git"
|
||||
print(c["INFRA"]["HOSTEA_REPO"]["REMOTE"])
|
||||
return c
|
||||
|
||||
|
||||
def create_instance_util(
|
||||
t: TestCase, c: Client, instance_name: str, config: InstanceConfiguration
|
||||
):
|
||||
|
@ -55,6 +85,22 @@ def create_instance_util(
|
|||
resp.headers["location"],
|
||||
reverse("billing.invoice.generate", args=(instance_name,)),
|
||||
)
|
||||
# generate invoice
|
||||
payment_uri = reverse("billing.invoice.generate", args=(instance_name,))
|
||||
resp = c.get(payment_uri)
|
||||
t.assertEqual(resp.status_code, 302)
|
||||
invoice_uri = resp.headers["Location"]
|
||||
t.assertEqual("invoice/payment/" in invoice_uri, True)
|
||||
|
||||
# simulate payment. There's probably a better way to do this
|
||||
payment = get_payment_model().objects.get(
|
||||
paid_by=t.user, instance_name=instance_name
|
||||
)
|
||||
payment.status = PaymentStatus.CONFIRMED
|
||||
payment.save()
|
||||
|
||||
resp = c.get(reverse("infra.create", args=(instance_name,)))
|
||||
t.assertEqual(resp.status_code, 200)
|
||||
|
||||
|
||||
class DashHome(TestCase):
|
||||
|
@ -151,6 +197,9 @@ class CreateInstance(TestCase):
|
|||
register_util(t=self, username="createinstance_user")
|
||||
create_configurations(t=self)
|
||||
|
||||
@override_settings(
|
||||
HOSTEA=infra_custom_config(test_name="test_create_instance_renders")
|
||||
)
|
||||
def test_create_instance_renders(self):
|
||||
c = Client()
|
||||
login_util(self, c, "accounts.home")
|
||||
|
@ -236,12 +285,16 @@ class CreateInstance(TestCase):
|
|||
|
||||
resp = c.post(delete_uri)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.headers["location"], reverse("dash.home"))
|
||||
self.assertEqual(
|
||||
resp.headers["location"], reverse("infra.rm", args=(instance.name,))
|
||||
)
|
||||
resp = c.get(resp.headers["location"])
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.headers["location"], reverse("dash.instances.list"))
|
||||
self.assertEqual(
|
||||
Instance.objects.filter(
|
||||
name=instance.name,
|
||||
name=instance_name,
|
||||
owned_by=self.user,
|
||||
configuration_id=self.instance_config[0],
|
||||
).exists(),
|
||||
False,
|
||||
)
|
||||
|
|
|
@ -14,7 +14,9 @@
|
|||
# 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
|
||||
|
||||
from .models import Instance, InstanceConfiguration
|
||||
|
||||
|
@ -55,3 +57,30 @@ def create_instance(vm_name: str, configuration_name: str, user: User) -> Instan
|
|||
instance = Instance(name=vm_name, configuration_id=configuration, owned_by=user)
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
source_code = None
|
||||
|
||||
|
||||
def footer_ctx():
|
||||
global source_code
|
||||
if source_code is None:
|
||||
if "SOURCE_CODE" in settings.HOSTEA:
|
||||
source_code = {
|
||||
"text": "Source Code",
|
||||
"link": settings.HOSTEA["SOURCE_CODE"],
|
||||
}
|
||||
else:
|
||||
link = "https://gitea.hostea.org/Hostea/dashboard"
|
||||
source_code = {"text": "Source Code", "link": link}
|
||||
try:
|
||||
r = Repo(".")
|
||||
commit = r.head.commit.hexsha
|
||||
source_code["text"] = f"v-{commit.hexsha[0:8]}"
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
"source_code": source_code,
|
||||
"admin_email": settings.HOSTEA["INSTANCE_MAINTAINER_CONTACT"],
|
||||
}
|
||||
|
|
|
@ -22,9 +22,15 @@ from django.views.decorators.csrf import csrf_protect
|
|||
from django.urls import reverse
|
||||
|
||||
from accounts.decorators import confirm_access
|
||||
from infrastructure.utils import Infra
|
||||
|
||||
from .models import Instance, InstanceConfiguration
|
||||
from .utils import create_instance as create_instance_util, VmErrors, VmException
|
||||
from .utils import (
|
||||
create_instance as create_instance_util,
|
||||
VmErrors,
|
||||
VmException,
|
||||
footer_ctx,
|
||||
)
|
||||
|
||||
|
||||
def default_ctx(title: str, username: str):
|
||||
|
@ -35,6 +41,7 @@ def default_ctx(title: str, username: str):
|
|||
"title": title,
|
||||
"username": username,
|
||||
"open_instances": "open",
|
||||
"footer": footer_ctx(),
|
||||
}
|
||||
|
||||
|
||||
|
@ -113,7 +120,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)
|
||||
woodpecker = Infra.get_woodpecker_uri(instance=instance)
|
||||
|
||||
ctx["instance"] = instance
|
||||
ctx["woodpecker"] = woodpecker
|
||||
ctx["gitea_uri"] = gitea_uri
|
||||
return render(request, "dash/instances/view/index.html", context=ctx)
|
||||
|
||||
|
||||
|
@ -130,5 +142,4 @@ def delete_instance(request, name):
|
|||
ctx["instance"] = instance
|
||||
return render(request, "dash/instances/delete/index.html", context=ctx)
|
||||
|
||||
instance.delete()
|
||||
return redirect(reverse("dash.home"))
|
||||
return redirect(reverse("infra.rm", args=(instance.name,)))
|
||||
|
|
|
@ -54,6 +54,7 @@ PAYMENT_VARIANTS = {
|
|||
### Dashbaord specific configuration options
|
||||
|
||||
HOSTEA = {
|
||||
"SOURCE_CODE": "https://gitea.hostea.org/Hostea/dashboard",
|
||||
"INSTANCE_MAINTAINER_CONTACT": "contact@hostea.example.org",
|
||||
"ACCOUNTS": {
|
||||
"MAX_VERIFICATION_TOLERANCE_PERIOD": 60 * 60 * 24, # in seconds
|
||||
|
|
|
@ -53,6 +53,7 @@ PAYMENT_VARIANTS = {
|
|||
### Dashbaord specific configuration options
|
||||
|
||||
HOSTEA = {
|
||||
"SOURCE_CODE": "https://gitea.hostea.org/Hostea/dashboard",
|
||||
"INSTANCE_MAINTAINER_CONTACT": "contact@hostea.example.org",
|
||||
"ACCOUNTS": {
|
||||
"MAX_VERIFICATION_TOLERANCE_PERIOD": 60 * 60 * 24, # in seconds
|
||||
|
|
|
@ -168,6 +168,7 @@ PAYMENT_VARIANTS = {
|
|||
### Dashbaord specific configuration options
|
||||
|
||||
HOSTEA = {
|
||||
"SOURCE_CODE": "https://gitea.hostea.org/Hostea/dashboard",
|
||||
"RESTRICT_NEW_INTEGRATION_INSTALLATION": True,
|
||||
"INSTANCE_MAINTAINER_CONTACT": "contact@hostea.example.org",
|
||||
"ACCOUNTS": {
|
||||
|
|
|
@ -119,7 +119,7 @@ class Command(BaseCommand):
|
|||
vm_name = options[self.vm_name_key]
|
||||
if Instance.objects.filter(name=vm_name).exists():
|
||||
instance = Instance.objects.get(name=vm_name)
|
||||
delete_vm(instance=instance, owner=instance.owned_by.username)
|
||||
delete_vm(instance=instance)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
for i in [self.action_key, self.vm_name_key]:
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
# Generated by Django 4.0.3 on 2022-06-29 18:30
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("dash", "0006_auto_20220619_0800"),
|
||||
("infrastructure", "0005_remove_instancecreated_gitea_password"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Job",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.BigAutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("job_type", models.CharField(max_length=10, verbose_name="Job Type")),
|
||||
(
|
||||
"instance",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="dash.instance"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -12,11 +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/>.
|
||||
from django.db import models
|
||||
from enum import Enum, unique
|
||||
|
||||
from django.db import models
|
||||
from dash.models import Instance
|
||||
|
||||
|
||||
@unique
|
||||
class JobType(Enum):
|
||||
PING = "ping"
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class InstanceCreated(models.Model):
|
||||
instance = models.ForeignKey(Instance, on_delete=models.PROTECT)
|
||||
created = models.BooleanField(default=False)
|
||||
|
||||
|
||||
class Job(models.Model):
|
||||
instance = models.ForeignKey(Instance, on_delete=models.CASCADE)
|
||||
|
||||
job_type = models.CharField(
|
||||
"Job Type",
|
||||
max_length=10,
|
||||
null=False,
|
||||
)
|
||||
|
|
|
@ -14,12 +14,11 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import shutil
|
||||
import time
|
||||
import os
|
||||
import requests
|
||||
from io import StringIO
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
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
|
||||
|
@ -28,20 +27,11 @@ from git import Repo
|
|||
|
||||
from dash.models import Instance, InstanceConfiguration
|
||||
from accounts.tests import register_util, login_util
|
||||
from dash.tests import create_configurations, create_instance_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
|
||||
from .models import InstanceCreated
|
||||
|
||||
|
||||
def custom_config(test_name: str):
|
||||
c = settings.HOSTEA
|
||||
path = Path(f"/tmp/hostea/dashboard/{test_name}/repo")
|
||||
if path.exists():
|
||||
shutil.rmtree(path)
|
||||
c["INFRA"]["HOSTEA_REPO"]["PATH"] = str(path)
|
||||
return c
|
||||
from .utils import Infra, Worker
|
||||
from .models import InstanceCreated, Job, JobType
|
||||
|
||||
|
||||
class InfraUtilTest(TestCase):
|
||||
|
@ -54,7 +44,7 @@ class InfraUtilTest(TestCase):
|
|||
register_util(t=self, username=self.username)
|
||||
create_configurations(t=self)
|
||||
|
||||
@override_settings(HOSTEA=custom_config(test_name="test_path_util"))
|
||||
@override_settings(HOSTEA=infra_custom_config(test_name="test_path_util"))
|
||||
def test_path_utils(self):
|
||||
infra = Infra()
|
||||
subdomain = "foo"
|
||||
|
@ -85,7 +75,7 @@ class InfraUtilTest(TestCase):
|
|||
infra._hostscript_path(subdomain=subdomain),
|
||||
)
|
||||
|
||||
@override_settings(HOSTEA=custom_config(test_name="test_add_vm"))
|
||||
@override_settings(HOSTEA=infra_custom_config(test_name="test_add_vm"))
|
||||
def test_add_vm(self):
|
||||
infra = Infra()
|
||||
c = Client()
|
||||
|
@ -105,9 +95,12 @@ class InfraUtilTest(TestCase):
|
|||
after_add = infra.repo.head.commit.hexsha
|
||||
self.assertEqual(before_add is not after_add, True)
|
||||
|
||||
c = custom_config(test_name="test_add_vm--get-head")
|
||||
path = c["INFRA"]["HOSTEA_REPO"]["PATH"]
|
||||
# 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)
|
||||
|
||||
before_rm = infra.repo.head.commit.hexsha
|
||||
|
@ -115,10 +108,10 @@ class InfraUtilTest(TestCase):
|
|||
after_rm = infra.repo.head.commit.hexsha
|
||||
self.assertEqual(before_add is not after_add, True)
|
||||
|
||||
repo.git.pull()
|
||||
repo.git.pull(env=infra.env)
|
||||
self.assertEqual(repo.head.commit.hexsha == after_rm, True)
|
||||
|
||||
@override_settings(HOSTEA=custom_config(test_name="test_cmd"))
|
||||
@override_settings(HOSTEA=infra_custom_config(test_name="test_cmd"))
|
||||
def test_cmd(self):
|
||||
subdomain = "cmd_vm"
|
||||
infra = Infra()
|
||||
|
@ -187,3 +180,26 @@ class InfraUtilTest(TestCase):
|
|||
|
||||
# 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
|
||||
)
|
||||
|
|
|
@ -15,17 +15,60 @@
|
|||
import os
|
||||
import shutil
|
||||
import yaml
|
||||
import requests
|
||||
from pathlib import Path
|
||||
from threading import Thread, Event
|
||||
from time import sleep
|
||||
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.template.loader import render_to_string
|
||||
from django.core.mail import send_mail
|
||||
from django.conf import settings
|
||||
from git import Repo, Commit
|
||||
from git.exc import InvalidGitRepositoryError
|
||||
|
||||
from dash.models import Instance
|
||||
|
||||
from .models import InstanceCreated
|
||||
from infrastructure.models import InstanceCreated, JobType, Job
|
||||
|
||||
|
||||
class Worker(Thread):
|
||||
def __init__(self, job: Job):
|
||||
self.job = job
|
||||
super().__init__()
|
||||
|
||||
def run(self):
|
||||
gitea_uri = Infra.get_gitea_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)
|
||||
if resp.status_code == 200:
|
||||
break
|
||||
except Exception:
|
||||
return False
|
||||
sleep(10)
|
||||
|
||||
job = self.job
|
||||
self.job = None
|
||||
email = job.instance.owned_by.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
|
||||
recipient_list=[email],
|
||||
)
|
||||
job.delete()
|
||||
|
||||
|
||||
def create_vm_if_not_exists(instance: Instance) -> (str, Commit):
|
||||
|
@ -35,18 +78,20 @@ def create_vm_if_not_exists(instance: Instance) -> (str, Commit):
|
|||
infra = Infra()
|
||||
if not InstanceCreated.objects.filter(instance=instance).exists():
|
||||
(gitea_password, commit) = infra.add_vm(instance=instance)
|
||||
instance = InstanceCreated.objects.create(instance=instance, created=True)
|
||||
instance.save()
|
||||
InstanceCreated.objects.create(instance=instance, created=True)
|
||||
job = Job.objects.create(instance=instance, job_type=str(JobType.PING))
|
||||
Worker(job=job).start()
|
||||
return (gitea_password, commit)
|
||||
else:
|
||||
if str.strip(infra.get_flavor(instance=instance)) != str.strip(
|
||||
infra.translate_size(instance=instance)
|
||||
):
|
||||
# Worker.init_global()
|
||||
return infra.add_vm(instance=instance)
|
||||
return None
|
||||
|
||||
|
||||
def delete_vm(instance: Instance, owner: str):
|
||||
def delete_vm(instance: Instance):
|
||||
infra = Infra()
|
||||
infra.remove_vm(instance=instance)
|
||||
if InstanceCreated.objects.filter(instance=instance).exists():
|
||||
|
@ -97,10 +142,10 @@ class Infra:
|
|||
"""
|
||||
Get Woodpecker hostname of an instance
|
||||
"""
|
||||
return (f"{instance.name}-ci",)
|
||||
return f"{instance.name}-ci"
|
||||
|
||||
@classmethod
|
||||
def get_woodpecker_hostname(cls, instance: Instance) -> str:
|
||||
def get_woodpecker_uri(cls, instance: Instance) -> str:
|
||||
"""
|
||||
Get an instance's Gitea URI
|
||||
"""
|
||||
|
@ -179,7 +224,9 @@ class Infra:
|
|||
)
|
||||
|
||||
def _pull(self):
|
||||
self.repo.git.pull(env=self.env, rebase="true")
|
||||
self.repo.git.fetch(env=self.env)
|
||||
|
||||
# TODO: switch to using Git cmd
|
||||
|
||||
@staticmethod
|
||||
def translate_size(instance: Instance) -> str:
|
||||
|
|
|
@ -26,7 +26,7 @@ from accounts.decorators import confirm_access
|
|||
from dash.models import Instance
|
||||
from billing.utils import payment_fullfilled
|
||||
|
||||
from .utils import create_vm_if_not_exists, Infra
|
||||
from .utils import create_vm_if_not_exists, Infra, delete_vm
|
||||
|
||||
|
||||
def default_ctx(title: str, username: str):
|
||||
|
@ -84,7 +84,5 @@ def delete_instance(request, instance_name: str):
|
|||
Dashboard homepage view
|
||||
"""
|
||||
instance = get_object_or_404(Instance, name=instance_name, owned_by=request.user)
|
||||
infra = Infra()
|
||||
infra.remove_vm(instance=instance)
|
||||
# TODO: push isn't implemented yet
|
||||
return HttpResponse()
|
||||
delete_vm(instance=instance)
|
||||
return redirect(reverse("dash.instances.list"))
|
||||
|
|
|
@ -11,6 +11,7 @@ init() {
|
|||
else
|
||||
docker_compose_down || true
|
||||
docker_compose_up
|
||||
sed -i /localhost.*/d ~/.ssh/known_hosts
|
||||
setup_env
|
||||
sleep 5
|
||||
# wait_for_env
|
||||
|
@ -29,4 +30,8 @@ teardown() {
|
|||
fi
|
||||
}
|
||||
|
||||
$1
|
||||
new_fleet_repo() {
|
||||
new_fleet_repo_init $2
|
||||
}
|
||||
|
||||
$1 $@
|
||||
|
|
|
@ -120,31 +120,20 @@ support_repo_init() {
|
|||
$GITEA_HOSTEA_SUPPORT_REPO
|
||||
}
|
||||
|
||||
# register user "Hostea" on Gitea and create support repository
|
||||
fleet_repo_init() {
|
||||
python -m integration \
|
||||
gitea register \
|
||||
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
||||
$GITEA_HOSTEA_EMAIL \
|
||||
$GITEA_URL || true
|
||||
python -m integration \
|
||||
gitea login \
|
||||
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
||||
$GITEA_HOSTEA_EMAIL \
|
||||
$GITEA_URL
|
||||
new_fleet_repo_init() {
|
||||
python -m integration \
|
||||
gitea create_repo \
|
||||
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
||||
$GITEA_HOSTEA_EMAIL \
|
||||
$GITEA_URL \
|
||||
$GITEA_HOSTEA_FLEET_REPO
|
||||
$1
|
||||
|
||||
python -m integration \
|
||||
gitea add_deploy_key \
|
||||
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
||||
$GITEA_HOSTEA_EMAIL \
|
||||
$GITEA_URL \
|
||||
$GITEA_HOSTEA_FLEET_REPO \
|
||||
$1 \
|
||||
$GITEA_HOSTEA_FLEET_DEPLOY_KEY
|
||||
|
||||
tmp_dir=$(mktemp -d)
|
||||
|
@ -159,7 +148,8 @@ fleet_repo_init() {
|
|||
git init
|
||||
git add README
|
||||
git commit -m "init"
|
||||
git remote add origin $GITEA_HOSTEA_FLEET_REPO_REMOTE
|
||||
REMOTE="$GITEA_SSH_URL/$GITEA_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 push --set-upstream origin master
|
||||
popd
|
||||
|
@ -167,6 +157,24 @@ fleet_repo_init() {
|
|||
|
||||
}
|
||||
|
||||
|
||||
# register user "Hostea" on Gitea and create support repository
|
||||
fleet_repo_init() {
|
||||
python -m integration \
|
||||
gitea register \
|
||||
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
||||
$GITEA_HOSTEA_EMAIL \
|
||||
$GITEA_URL || true
|
||||
python -m integration \
|
||||
gitea login \
|
||||
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
||||
$GITEA_HOSTEA_EMAIL \
|
||||
$GITEA_URL
|
||||
|
||||
new_fleet_repo_init $GITEA_HOSTEA_FLEET_REPO
|
||||
|
||||
}
|
||||
|
||||
# Create user on Hostea to simulate a Hostea customer
|
||||
hostea_customer_simulation() {
|
||||
python -m integration \
|
||||
|
|
|
@ -37,6 +37,7 @@ pynvim==0.4.3
|
|||
pytz==2022.1
|
||||
PyYAML==6.0
|
||||
requests==2.27.1
|
||||
six==1.16.0
|
||||
smmap==5.0.0
|
||||
sqlparse==0.4.2
|
||||
stripe==3.4.0
|
||||
|
|
|
@ -34,11 +34,9 @@ h2 {
|
|||
body {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
/*
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
*/
|
||||
}
|
||||
|
||||
a:hover {
|
||||
|
@ -244,6 +242,8 @@ footer {
|
|||
display: block;
|
||||
font-size: 0.7rem;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 260px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer__container {
|
||||
|
@ -391,6 +391,8 @@ footer {
|
|||
font-size: 0.7rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-left: 260px;
|
||||
width: calc(100vw - 260px);
|
||||
}
|
||||
|
||||
.footer__container {
|
||||
|
@ -607,8 +609,6 @@ fieldset {
|
|||
background-color: #e11d21;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
.form__label {
|
||||
margin: 5px 0;
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
from django.shortcuts import render, redirect
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from dash.utils import footer_ctx
|
||||
from .utils import IssueTracker
|
||||
|
||||
|
||||
|
@ -28,6 +29,7 @@ def default_ctx(title: str, username: str):
|
|||
"username": username,
|
||||
"open_support": "open",
|
||||
"support": {"list": it.get_issue_tracker(), "new": it.open_issue()},
|
||||
"footer": footer_ctx(),
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue