// Command lkctl issues and inspects Vantage licences by hand. // // lkctl keypair // lkctl issue --instance-id= --instance-name="Acme" --tier=professional --term=1y // lkctl inspect // // issue reads the signing key from LICENSE_SIGNING_KEY. package main import ( "encoding/json" "flag" "fmt" "os" "strings" "time" "gitea.hostxtra.co.uk/vantage/vantage-shared/license" "github.com/google/uuid" "github.com/hyperboloide/lk" ) 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 | enterprise (required)") deployment := fs.String("deployment", "cloud", "cloud | self_hosted") 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(*deployment, *tier) if !ok { fatal("no plan for deployment %q tier %q", *deployment, *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.Deployment == license.DeploymentSelfHosted && *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, SupportLevel: plan.SupportLevel, 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 := 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) }