177 lines
4.2 KiB
Go
177 lines
4.2 KiB
Go
// 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)
|
|
}
|