Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@ jobs:
- name: Execute tests
env:
QUAY_IO_TOKEN: ${{ secrets.QUAY_IO_TOKEN }}
GEMFURY_API_TOKEN: ${{ secrets.GEMFURY_API_TOKEN }}

run: |
pip install -U pip wheel setuptools
pip install -U pip wheel "setuptools<82"
pip install -r devel.txt

# report to Kiwi TCMS only if we have access to secrets
Expand Down Expand Up @@ -104,7 +105,7 @@ jobs:

- name: Install Python dependencies
run: |
pip install -U pip wheel setuptools
pip install -U pip wheel "setuptools<82"
pip install -r devel.txt

- name: Login to Private Container Registry
Expand Down
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Configuration

Required settings:

- ``GEMFURY_API_TOKEN`` - string
- ``KIWI_GITHUB_PAT_FOR_CHECKING_ORGS_AND_USERNAMES`` - string
- ``KIWI_GITHUB_MARKETPLACE_SECRET`` - binary string
- ``KIWI_FASTSPRING_SECRET`` - binary string
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
httplink==0.2.0
kiwitcms-tenants>=2.8.3
mailchimp3==3.0.21
requests
Expand Down
3 changes: 2 additions & 1 deletion tcms_github_marketplace/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from django.contrib import admin
from django.http import HttpResponseForbidden, HttpResponseRedirect

from tcms_github_marketplace.models import ManualPurchase, Purchase
from tcms_github_marketplace.models import ManualPurchase, PrivateRepoToken, Purchase


class PurchaseAdmin(admin.ModelAdmin):
Expand Down Expand Up @@ -178,3 +178,4 @@ def response_add(

admin.site.register(ManualPurchase, ManualPurchaseAdmin)
admin.site.register(Purchase, PurchaseAdmin)
admin.site.register(PrivateRepoToken)
97 changes: 97 additions & 0 deletions tcms_github_marketplace/fury.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Copyright (c) 2026 Alexander Todorov <atodorov@otb.bg>
#
# Licensed under GNU Affero General Public License v3 or later (AGPLv3+)
# https://www.gnu.org/licenses/agpl-3.0.html

import requests
from requests.auth import AuthBase
from httplink import parse_link_header


class TokenAuth(AuthBase):
def __init__(self, token):
self.token = token

def __eq__(self, other):
return self.token == getattr(other, "token", None)

def __ne__(self, other):
return not self == other

def __call__(self, r):
r.headers["Authorization"] = f"Bearer {self.token}"
return r


class GemfuryAPI:
base_url = "https://api.fury.io/1"

def __init__(self, password=None):
"""
WARNING: we must be using the Full Access Token on the organization account!
"""
self.auth = TokenAuth(password)

def find_token(self, subscription_id):
json_data, link = self._request("GET", "/tokens?kind_key=pull")

while json_data:
for token in json_data:
if token.get("description") == subscription_id:
return token

if link:
page = parse_link_header(link)
# keep asking for content until there's no-more
if "next" in page:
next_url = page["next"].target
json_data, link = self._request("GET", f"/tokens{next_url}")
else:
break
else:
break

return None

def create_token(self, subscription_id):
"""
Returns:

{
'token': {
'id': 'tok_MBFav',
'kind_key': 'pull'
},
'token_value': 'actual-value'
}
"""
json_response, _ = self._request(
"POST", f"/tokens?kind_key=pull&description={subscription_id}"
)
return json_response

def delete_token(self, subscription_id):
token = self.find_token(subscription_id)

if token and token.get("id"):
token_id = token["id"]
# returns None, None
self._request("DELETE", f"/tokens/{token_id}")

def _request(self, method, path, **kwargs):
"""
https://gemfury.com/guide/api/errors/
"""
response = requests.request(
method,
f"{self.base_url}{path}",
auth=self.auth,
timeout=30,
**kwargs,
)

# Successful operation with no body
if response.status_code == 204:
return None, None

return response.json(), response.headers.get("Link")
68 changes: 33 additions & 35 deletions tcms_github_marketplace/locale/en/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-14 11:48+0000\n"
"POT-Creation-Date: 2026-03-31 11:50+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
Expand Down Expand Up @@ -51,13 +51,13 @@ msgid "Owner"
msgstr ""

#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:39
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:140
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:112
#: tcms_github_marketplace/templates/tcms_tenants/override_new.html:38
msgid "Organization"
msgstr ""

#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:52
msgid "Private repository credentials"
msgid "Private credentials"
msgstr ""

#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:57
Expand All @@ -69,73 +69,71 @@ msgid "Password"
msgstr ""

#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:68
msgid "Click 'Password' to reveal!"
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:81
msgid "Click field to reveal!"
msgstr ""

#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:70
msgid "Private containers instructions"
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:83
msgid "Instructions"
msgstr ""

#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:81
msgid "kiwitcms/gitops prefix"
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:75
msgid "Token"
msgstr ""

#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:102
msgid "Save"
msgstr ""

#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:125
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:97
msgid "You own the following tenants"
msgstr ""

#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:147
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:148
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:119
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:120
msgid "Price"
msgstr ""

#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:152
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:153
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:124
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:125
msgid "Subscription type"
msgstr ""

#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:157
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:158
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:129
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:130
#: tcms_github_marketplace/templates/tcms_tenants/override_new.html:16
msgid "Paid until"
msgstr ""

#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:164
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:136
msgid "Cancel subscription"
msgstr ""

#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:166
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:138
msgid "Cancel"
msgstr ""

#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:177
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:149
msgid "You don't own any tenants"
msgstr ""

#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:181
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:153
msgid "Subscribe via FastSpring"
msgstr ""

#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:197
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:169
msgid "Transaction history"
msgstr ""

#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:219
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:220
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:191
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:192
msgid "Sender"
msgstr ""

#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:224
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:225
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:196
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:197
msgid "Vendor"
msgstr ""

#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:229
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:230
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:201
#: tcms_github_marketplace/templates/tcms_github_marketplace/subscription.html:202
msgid "Received on"
msgstr ""

Expand All @@ -145,8 +143,8 @@ msgstr ""

#: tcms_github_marketplace/templates/tcms_tenants/include/tenant_extra_emails.html:21
msgid ""
"Kiwi TCMS will try to match recurring billing events against tenant.owner."
"email + tenant.extra_emails"
"Kiwi TCMS will try to match recurring billing events against "
"tenant.owner.email + tenant.extra_emails"
msgstr ""

#: tcms_github_marketplace/templates/tcms_tenants/include/tenant_extra_emails.html:24
Expand All @@ -169,18 +167,18 @@ msgid ""
"<a href=\"mailto:kiwitcms@mrsenko.com\">kiwitcms@mrsenko.com</a> immediately!"
msgstr ""

#: tcms_github_marketplace/utils.py:58
#: tcms_github_marketplace/utils.py:64
msgid "Kiwi TCMS Subscription Exit Poll"
msgstr ""

#: tcms_github_marketplace/views.py:617
#: tcms_github_marketplace/views.py:628
msgid "Kiwi TCMS subscription notification"
msgstr ""

#: tcms_github_marketplace/views.py:833
#: tcms_github_marketplace/views.py:846
msgid "mo"
msgstr ""

#: tcms_github_marketplace/views.py:835
#: tcms_github_marketplace/views.py:848
msgid "yr"
msgstr ""
48 changes: 48 additions & 0 deletions tcms_github_marketplace/migrations/0012_privaterepotoken.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# pylint: disable=avoid-auto-field
#
# Copyright (c) 2026 Alexander Todorov <atodorov@otb.bg>
#
# Licensed under GNU Affero General Public License v3 or later (AGPLv3+)
# https://www.gnu.org/licenses/agpl-3.0.html

import django.contrib.postgres.indexes
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("tcms_github_marketplace", "0011_quay_accounts"),
]

operations = [
migrations.CreateModel(
name="PrivateRepoToken",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("vendor", models.CharField(db_index=True, max_length=16)),
(
"subscription",
models.CharField(
blank=True, db_index=True, max_length=32, null=True
),
),
("created_at", models.DateTimeField(auto_now_add=True, db_index=True)),
("payload", models.JSONField()),
],
),
migrations.AddIndex(
model_name="privaterepotoken",
index=django.contrib.postgres.indexes.GinIndex(
fastupdate=False, fields=["payload"], name="ghmp_privaterepotoken_gin"
),
),
]
18 changes: 18 additions & 0 deletions tcms_github_marketplace/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,21 @@ def unit_count(self):
return value

return 0


class PrivateRepoToken(models.Model):
vendor = models.CharField(max_length=16, db_index=True)
subscription = models.CharField(max_length=32, db_index=True, blank=True, null=True)
created_at = models.DateTimeField(db_index=True, auto_now_add=True)
payload = models.JSONField()

class Meta:
indexes = [
GinIndex(
fastupdate=False, fields=["payload"], name="ghmp_privaterepotoken_gin"
),
]

@property
def token(self):
return self.payload["token_value"]
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,15 @@ $(document).ready(function () {
$('#docker_password').attr('type', 'password')
}
})

// toggle private repo password
$('#show-repo-token').click(function() {
var input_type = $('#repo_token').attr('type')

if (input_type === 'password') {
$('#repo_token').attr('type', 'text')
} else {
$('#repo_token').attr('type', 'password')
}
})
})
Loading
Loading