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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

### Changed

- Provisioning load tests generate an ephemeral admin DID per run instead of
requiring an operator-supplied DID.
- VTA-only sessions are marked `running` only after the VTA `/health` readiness
probe reports a Ready replica.
- Full-stack DID hosting, mediator and VTC deployments now use their HTTP
Expand Down
39 changes: 6 additions & 33 deletions cmd/gen-keypair/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,20 @@ import (
"encoding/base64"
"fmt"
"log"
"math/big"
)

// base58btc alphabet used by multibase
const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"

func base58Encode(b []byte) string {
n := new(big.Int).SetBytes(b)
base := big.NewInt(58)
zero := big.NewInt(0)
mod := new(big.Int)

var result []byte
for n.Cmp(zero) > 0 {
n.DivMod(n, base, mod)
result = append(result, alphabet[mod.Int64()])
}
// leading zero bytes → '1'
for _, byt := range b {
if byt != 0 {
break
}
result = append(result, alphabet[0])
}
// reverse
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
result[i], result[j] = result[j], result[i]
}
return string(result)
}
"github.com/ic3software/vtafarm-api/internal/didkey"
)

func main() {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
log.Fatal(err)
}

// did:key encoding: multibase base58btc of (multicodec ed25519-pub prefix + pubkey)
// ed25519-pub multicodec varint: 0xed 0x01
prefixed := append([]byte{0xed, 0x01}, pub...)
didKey := "did:key:z" + base58Encode(prefixed)
didKey, err := didkey.FromPublicKey(pub)
if err != nil {
log.Fatal(err)
}

// Store only the 32-byte seed (private key), not the full 64-byte Go representation
privB64 := base64.StdEncoding.EncodeToString(priv.Seed())
Expand Down
11 changes: 7 additions & 4 deletions docs/vta-provisioning-load-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ orchestrator path used by the user portal.

## Run lifecycle

`POST /api/v1/admin/load-tests` accepts a count (1–50), one VTA image, and one
`did:key` admin DID. Members use the existing `platform` system account and are
named `load-<run-id>-NNN`. Up to ten member records are created in parallel;
each recorded session then runs independently in the ordinary orchestrator.
`POST /api/v1/admin/load-tests` accepts a count (1–50) and one VTA image. The API
generates one ephemeral `did:key` for the run, discards its private key, and
supplies the DID to every member so the ordinary pipeline continues through
deployment without operator input. Members use the existing `platform` system
account and are named `load-<run-id>-NNN`. Up to ten member records are created
in parallel; each recorded session then runs independently in the ordinary
orchestrator.

Only one run may own active resources at a time. A partial run remains active
until it is deleted so its successfully created members cannot be forgotten.
Expand Down
9 changes: 4 additions & 5 deletions internal/apidocs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1560,8 +1560,8 @@ paths:
description: |
Records a run and asynchronously creates 1–50 ordinary VTA-only
sessions against the platform stack. Names are generated as
`load-<run-id>-NNN`; one admin DID is supplied to every session so each
pipeline automatically continues through deployment.
`load-<run-id>-NNN`; the API generates one ephemeral admin DID for the
run so every member pipeline automatically continues through deployment.
tags: [Admin]
security:
- CookieAuthAdmin: []
Expand All @@ -1573,14 +1573,13 @@ paths:
type: object
properties:
count: { type: integer, minimum: 1, maximum: 50 }
admin_did: { type: string, example: "did:key:z6Mk..." }
vta_image: { type: string }
required: [count, admin_did, vta_image]
required: [count, vta_image]
responses:
"202":
description: Run recorded and member creation started
"400":
description: Invalid count, DID, or image
description: Invalid count or image
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
Expand Down
55 changes: 55 additions & 0 deletions internal/didkey/didkey.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package didkey

import (
"crypto/ed25519"
"crypto/rand"
"errors"
"math/big"
)

const base58BTCAlphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"

// Generate creates a valid did:key and deliberately discards its private key.
func Generate() (string, error) {
publicKey, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return "", err
}
return FromPublicKey(publicKey)
}

// FromPublicKey encodes an Ed25519 public key as a did:key identifier.
func FromPublicKey(publicKey ed25519.PublicKey) (string, error) {
if len(publicKey) != ed25519.PublicKeySize {
return "", errors.New("ed25519 public key must be 32 bytes")
}

// ed25519-pub multicodec varint: 0xed 0x01.
prefixed := make([]byte, 2+len(publicKey))
prefixed[0], prefixed[1] = 0xed, 0x01
copy(prefixed[2:], publicKey)
return "did:key:z" + base58Encode(prefixed), nil
}

func base58Encode(value []byte) string {
n := new(big.Int).SetBytes(value)
base := big.NewInt(58)
zero := big.NewInt(0)
mod := new(big.Int)

result := make([]byte, 0, len(value)*2)
for n.Cmp(zero) > 0 {
n.DivMod(n, base, mod)
result = append(result, base58BTCAlphabet[mod.Int64()])
}
for _, b := range value {
if b != 0 {
break
}
result = append(result, base58BTCAlphabet[0])
}
for i, j := 0, len(result)-1; i < j; i, j = i+1, j-1 {
result[i], result[j] = result[j], result[i]
}
return string(result)
}
23 changes: 23 additions & 0 deletions internal/didkey/didkey_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package didkey

import (
"crypto/ed25519"
"strings"
"testing"
)

func TestGenerate(t *testing.T) {
did, err := Generate()
if err != nil {
t.Fatalf("Generate() error = %v", err)
}
if !strings.HasPrefix(did, "did:key:z6Mk") {
t.Fatalf("Generate() = %q, want an Ed25519 did:key", did)
}
}

func TestFromPublicKeyRejectsInvalidLength(t *testing.T) {
if _, err := FromPublicKey(ed25519.PublicKey{1, 2, 3}); err == nil {
t.Fatal("FromPublicKey() accepted an invalid public key")
}
}
17 changes: 9 additions & 8 deletions internal/handler/admin_load_testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/gin-gonic/gin"

"github.com/ic3software/vtafarm-api/internal/capacity"
"github.com/ic3software/vtafarm-api/internal/didkey"
"github.com/ic3software/vtafarm-api/internal/k8s"
"github.com/ic3software/vtafarm-api/internal/middleware"
"github.com/ic3software/vtafarm-api/internal/model"
Expand All @@ -26,7 +27,6 @@ const (

type createLoadTestRequest struct {
Count int `json:"count"`
AdminDid string `json:"admin_did"`
VtaImage string `json:"vta_image"`
}

Expand Down Expand Up @@ -75,16 +75,11 @@ func (h *SetupHandler) AdminCreateLoadTest(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
req.AdminDid = strings.TrimSpace(req.AdminDid)
req.VtaImage = strings.TrimSpace(req.VtaImage)
if req.Count < 1 || req.Count > maxLoadTestSessions {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("count must be between 1 and %d", maxLoadTestSessions)})
return
}
if !didKeyRe.MatchString(req.AdminDid) {
c.JSON(http.StatusBadRequest, gin.H{"error": "admin_did must be a did:key value produced by pnm setup"})
return
}
if req.VtaImage == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "vta_image is required"})
return
Expand Down Expand Up @@ -119,6 +114,11 @@ func (h *SetupHandler) AdminCreateLoadTest(c *gin.Context) {
return
}
}
adminDid, err := didkey.Generate()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate load-test admin DID"})
return
}

adminID := c.MustGet(middleware.ContextUserID).(uint)
run := model.LoadTestRun{
Expand All @@ -145,13 +145,14 @@ func (h *SetupHandler) AdminCreateLoadTest(c *gin.Context) {
return
}

go h.startLoadTest(run.ID, provider.UserID, req, infra, provider)
go h.startLoadTest(run.ID, provider.UserID, req, adminDid, infra, provider)
c.JSON(http.StatusAccepted, gin.H{"id": run.ID, "status": run.Status})
}

func (h *SetupHandler) startLoadTest(
runID, userID uint,
request createLoadTestRequest,
adminDid string,
infra sharedInfra,
provider *model.SetupSession,
) {
Expand All @@ -172,7 +173,7 @@ func (h *SetupHandler) startLoadTest(
Mode: model.ModeVtaOnly,
VtaName: name,
VtaImage: request.VtaImage,
AdminDid: request.AdminDid,
AdminDid: adminDid,
}, infra, provider, &runID)
cancel()
results <- createResult{err: err}
Expand Down