feat(license): add lkctl for issuing licences by hand
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
// Command lkctl issues and inspects Vantage licences by hand.
|
||||
//
|
||||
// lkctl keypair
|
||||
// lkctl issue --instance-id=<uuid> --instance-name="Acme" --tier=professional --term=1y
|
||||
// lkctl inspect <blob-or-file>
|
||||
//
|
||||
// issue reads the signing key from LICENSE_SIGNING_KEY.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/hyperboloide/lk"
|
||||
"github.com/mrhid6/vantage/shared/license"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
usage()
|
||||
}
|
||||
switch os.Args[1] {
|
||||
case "keypair":
|
||||
keypair()
|
||||
case "issue":
|
||||
issue(os.Args[2:])
|
||||
case "inspect":
|
||||
inspect(os.Args[2:])
|
||||
default:
|
||||
usage()
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintln(os.Stderr, "usage: lkctl keypair | issue | inspect")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func keypair() {
|
||||
priv, err := lk.NewPrivateKey()
|
||||
if err != nil {
|
||||
fatal("generate key: %v", err)
|
||||
}
|
||||
privStr, err := priv.ToB32String()
|
||||
if err != nil {
|
||||
fatal("encode private key: %v", err)
|
||||
}
|
||||
// PublicKey.ToB32String returns one value, unlike its private counterpart.
|
||||
pubStr := priv.GetPublicKey().ToB32String()
|
||||
|
||||
fmt.Println("PRIVATE KEY (store in a password manager and in the admin service's")
|
||||
fmt.Println("LICENSE_SIGNING_KEY; back it up in two places, it cannot be recovered):")
|
||||
fmt.Println()
|
||||
fmt.Println(privStr)
|
||||
fmt.Println()
|
||||
fmt.Println("PUBLIC KEY (paste into trustedPublicKeys in shared/license/keys.go):")
|
||||
fmt.Println()
|
||||
fmt.Println(pubStr)
|
||||
}
|
||||
|
||||
func issue(args []string) {
|
||||
fs := flag.NewFlagSet("issue", flag.ExitOnError)
|
||||
instanceID := fs.String("instance-id", "", "instance UUID the licence is bound to (required)")
|
||||
instanceName := fs.String("instance-name", "", "display name")
|
||||
accountID := fs.String("account-id", "", "admin-side account id, optional")
|
||||
tier := fs.String("tier", "", "free | professional | self_hosted (required)")
|
||||
term := fs.String("term", "1y", "1m or 1y")
|
||||
expires := fs.String("expires", "", "explicit RFC3339 expiry, overrides --term")
|
||||
out := fs.String("out", "", "write the blob to this file instead of stdout")
|
||||
fs.Parse(args)
|
||||
|
||||
if *instanceID == "" || *tier == "" {
|
||||
fatal("--instance-id and --tier are required")
|
||||
}
|
||||
|
||||
plan, ok := license.PlanFor(*tier)
|
||||
if !ok {
|
||||
fatal("unknown tier %q", *tier)
|
||||
}
|
||||
|
||||
key := os.Getenv("LICENSE_SIGNING_KEY")
|
||||
if key == "" {
|
||||
fatal("LICENSE_SIGNING_KEY is not set")
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
var exp time.Time
|
||||
switch {
|
||||
case *expires != "":
|
||||
t, err := time.Parse(time.RFC3339, *expires)
|
||||
if err != nil {
|
||||
fatal("parse --expires: %v", err)
|
||||
}
|
||||
exp = t.UTC()
|
||||
case *term == "1m":
|
||||
exp = now.AddDate(0, 1, 0)
|
||||
case *term == "1y":
|
||||
exp = now.AddDate(1, 0, 0)
|
||||
default:
|
||||
fatal("--term must be 1m or 1y")
|
||||
}
|
||||
|
||||
// Self Hosted is sold annually only, so the window in which a cancelled
|
||||
// licence keeps working is bounded at a year.
|
||||
if plan.Tier == license.TierSelfHosted && *term == "1m" && *expires == "" {
|
||||
fatal("self_hosted is annual only; use --term=1y or an explicit --expires")
|
||||
}
|
||||
|
||||
name := *instanceName
|
||||
if name == "" {
|
||||
name = *instanceID
|
||||
}
|
||||
|
||||
l := license.License{
|
||||
ID: uuid.NewString(),
|
||||
InstanceID: *instanceID,
|
||||
AccountID: *accountID,
|
||||
InstanceName: name,
|
||||
Tier: plan.Tier,
|
||||
Deployment: plan.Deployment,
|
||||
IssuedAt: now,
|
||||
ExpiresAt: exp,
|
||||
Limits: plan.Limits,
|
||||
Features: plan.Features,
|
||||
}
|
||||
|
||||
blob, err := license.Sign(l, key)
|
||||
if err != nil {
|
||||
fatal("%v", err)
|
||||
}
|
||||
|
||||
if *out != "" {
|
||||
if err := os.WriteFile(*out, []byte(blob+"\n"), 0o600); err != nil {
|
||||
fatal("write %s: %v", *out, err)
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "wrote %s (tier=%s deployment=%s expires=%s)\n",
|
||||
*out, l.Tier, l.Deployment, l.ExpiresAt.Format(time.RFC3339))
|
||||
return
|
||||
}
|
||||
fmt.Println(blob)
|
||||
}
|
||||
|
||||
func inspect(args []string) {
|
||||
if len(args) < 1 {
|
||||
fatal("usage: lkctl inspect <blob-or-file>")
|
||||
}
|
||||
blob := args[0]
|
||||
if b, err := os.ReadFile(blob); err == nil {
|
||||
blob = strings.TrimSpace(string(b))
|
||||
}
|
||||
|
||||
l, err := license.Parse(blob)
|
||||
if err != nil {
|
||||
fatal("%v", err)
|
||||
}
|
||||
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
if err := enc.Encode(l); err != nil {
|
||||
fatal("%v", err)
|
||||
}
|
||||
|
||||
if time.Now().After(l.ExpiresAt) {
|
||||
fmt.Fprintf(os.Stderr, "\nNOTE: expired %s\n", l.ExpiresAt.Format(time.RFC3339))
|
||||
}
|
||||
}
|
||||
|
||||
func fatal(format string, args ...any) {
|
||||
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
Reference in New Issue
Block a user