mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 13:41:30 +00:00
feat: M17 OpenSSL/Custom CA issuer connector + M16b CLI tool with bulk import
M17: Script-based issuer connector delegating sign/revoke/CRL to user-provided scripts. Compatible with any CA tooling (OpenSSL, cfssl, custom PKI). Configurable timeout, environment variable passthrough. 14 tests including timeout enforcement. M16b: certctl-cli wraps all 76 REST API endpoints for terminal workflows. Supports certs/agents/jobs list/get/renew/revoke/cancel, bulk PEM import with progress reporting, server health status, table and JSON output formats. Zero external dependencies (stdlib only). 14 tests with mock HTTP server. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+203
@@ -0,0 +1,203 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Parse global flags
|
||||
fs := flag.NewFlagSet("certctl-cli", flag.ExitOnError)
|
||||
fs.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, `certctl-cli — CLI for certificate lifecycle management
|
||||
|
||||
Usage:
|
||||
certctl-cli [global flags] <command> [command flags]
|
||||
|
||||
Global flags:
|
||||
`)
|
||||
fs.PrintDefaults()
|
||||
fmt.Fprintf(os.Stderr, `
|
||||
Commands:
|
||||
certs list List certificates
|
||||
certs get ID Get certificate details
|
||||
certs renew ID Trigger certificate renewal
|
||||
certs revoke ID Revoke a certificate
|
||||
|
||||
agents list List agents
|
||||
agents get ID Get agent details
|
||||
|
||||
jobs list List jobs
|
||||
jobs get ID Get job details
|
||||
jobs cancel ID Cancel a pending job
|
||||
|
||||
import FILE Bulk import certificates from PEM file(s)
|
||||
|
||||
status Show server health + summary stats
|
||||
version Show CLI version
|
||||
|
||||
Examples:
|
||||
certctl-cli --server http://localhost:8443 --api-key mykey certs list
|
||||
certctl-cli certs renew mc-prod --format json
|
||||
certctl-cli import certs.pem
|
||||
`)
|
||||
}
|
||||
|
||||
serverURL := fs.String("server", os.Getenv("CERTCTL_SERVER_URL"), "certctl server URL (env: CERTCTL_SERVER_URL)")
|
||||
if *serverURL == "" {
|
||||
*serverURL = "http://localhost:8443"
|
||||
}
|
||||
|
||||
apiKey := fs.String("api-key", os.Getenv("CERTCTL_API_KEY"), "API key for authentication (env: CERTCTL_API_KEY)")
|
||||
format := fs.String("format", "table", "Output format: table, json")
|
||||
|
||||
fs.Parse(os.Args[1:])
|
||||
|
||||
args := fs.Args()
|
||||
if len(args) == 0 {
|
||||
fs.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Create client
|
||||
client := cli.NewClient(*serverURL, *apiKey, *format)
|
||||
|
||||
// Dispatch to appropriate command
|
||||
command := args[0]
|
||||
cmdArgs := args[1:]
|
||||
|
||||
var err error
|
||||
switch command {
|
||||
case "certs":
|
||||
err = handleCerts(client, cmdArgs)
|
||||
case "agents":
|
||||
err = handleAgents(client, cmdArgs)
|
||||
case "jobs":
|
||||
err = handleJobs(client, cmdArgs)
|
||||
case "import":
|
||||
err = handleImport(client, cmdArgs)
|
||||
case "status":
|
||||
err = handleStatus(client)
|
||||
case "version":
|
||||
fmt.Println("certctl-cli version 0.1.0")
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", command)
|
||||
fs.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func handleCerts(client *cli.Client, args []string) error {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "usage: certs <list|get|renew|revoke> [options]\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
subcommand := args[0]
|
||||
subArgs := args[1:]
|
||||
|
||||
switch subcommand {
|
||||
case "list":
|
||||
return client.ListCertificates(subArgs)
|
||||
case "get":
|
||||
if len(subArgs) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "usage: certs get <id>\n")
|
||||
return nil
|
||||
}
|
||||
return client.GetCertificate(subArgs[0])
|
||||
case "renew":
|
||||
if len(subArgs) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "usage: certs renew <id>\n")
|
||||
return nil
|
||||
}
|
||||
return client.RenewCertificate(subArgs[0])
|
||||
case "revoke":
|
||||
if len(subArgs) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "usage: certs revoke <id> [--reason <reason>]\n")
|
||||
return nil
|
||||
}
|
||||
id := subArgs[0]
|
||||
reason := "unspecified"
|
||||
if len(subArgs) > 2 && subArgs[1] == "--reason" {
|
||||
reason = subArgs[2]
|
||||
}
|
||||
return client.RevokeCertificate(id, reason)
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown subcommand: certs %s\n", subcommand)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleAgents(client *cli.Client, args []string) error {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "usage: agents <list|get> [options]\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
subcommand := args[0]
|
||||
subArgs := args[1:]
|
||||
|
||||
switch subcommand {
|
||||
case "list":
|
||||
return client.ListAgents(subArgs)
|
||||
case "get":
|
||||
if len(subArgs) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "usage: agents get <id>\n")
|
||||
return nil
|
||||
}
|
||||
return client.GetAgent(subArgs[0])
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown subcommand: agents %s\n", subcommand)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleJobs(client *cli.Client, args []string) error {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "usage: jobs <list|get|cancel> [options]\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
subcommand := args[0]
|
||||
subArgs := args[1:]
|
||||
|
||||
switch subcommand {
|
||||
case "list":
|
||||
return client.ListJobs(subArgs)
|
||||
case "get":
|
||||
if len(subArgs) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "usage: jobs get <id>\n")
|
||||
return nil
|
||||
}
|
||||
return client.GetJob(subArgs[0])
|
||||
case "cancel":
|
||||
if len(subArgs) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "usage: jobs cancel <id>\n")
|
||||
return nil
|
||||
}
|
||||
return client.CancelJob(subArgs[0])
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown subcommand: jobs %s\n", subcommand)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func handleImport(client *cli.Client, args []string) error {
|
||||
if len(args) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "usage: import <file> [file2 ...]\n")
|
||||
return nil
|
||||
}
|
||||
return client.ImportCertificates(args)
|
||||
}
|
||||
|
||||
func handleStatus(client *cli.Client) error {
|
||||
return client.GetStatus()
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
acmeissuer "github.com/shankar0123/certctl/internal/connector/issuer/acme"
|
||||
"github.com/shankar0123/certctl/internal/connector/issuer/local"
|
||||
opensslissuer "github.com/shankar0123/certctl/internal/connector/issuer/openssl"
|
||||
stepcaissuer "github.com/shankar0123/certctl/internal/connector/issuer/stepca"
|
||||
notifyopsgenie "github.com/shankar0123/certctl/internal/connector/notifier/opsgenie"
|
||||
notifypagerduty "github.com/shankar0123/certctl/internal/connector/notifier/pagerduty"
|
||||
@@ -117,15 +118,27 @@ func main() {
|
||||
}, logger)
|
||||
logger.Info("initialized step-ca issuer connector")
|
||||
|
||||
// Initialize OpenSSL/Custom CA issuer connector (for script-based CA integrations).
|
||||
// Delegates certificate signing to user-provided scripts.
|
||||
opensslConnector := opensslissuer.New(&opensslissuer.Config{
|
||||
SignScript: os.Getenv("CERTCTL_OPENSSL_SIGN_SCRIPT"),
|
||||
RevokeScript: os.Getenv("CERTCTL_OPENSSL_REVOKE_SCRIPT"),
|
||||
CRLScript: os.Getenv("CERTCTL_OPENSSL_CRL_SCRIPT"),
|
||||
TimeoutSeconds: getEnvIntDefault(os.Getenv("CERTCTL_OPENSSL_TIMEOUT_SECONDS"), 30),
|
||||
}, logger)
|
||||
logger.Info("initialized OpenSSL/Custom CA issuer connector")
|
||||
|
||||
// Build issuer registry: maps issuer IDs (from database) to connector implementations.
|
||||
// "iss-local" matches the seed data issuer ID for the Local CA.
|
||||
// "iss-acme-staging" and "iss-acme-prod" are conventional IDs for ACME issuers.
|
||||
// "iss-stepca" is the step-ca private CA connector.
|
||||
// "iss-openssl" is the custom CA/OpenSSL connector.
|
||||
issuerRegistry := map[string]service.IssuerConnector{
|
||||
"iss-local": service.NewIssuerConnectorAdapter(localCA),
|
||||
"iss-acme-staging": service.NewIssuerConnectorAdapter(acmeConnector),
|
||||
"iss-acme-prod": service.NewIssuerConnectorAdapter(acmeConnector),
|
||||
"iss-stepca": service.NewIssuerConnectorAdapter(stepcaConnector),
|
||||
"iss-openssl": service.NewIssuerConnectorAdapter(opensslConnector),
|
||||
}
|
||||
logger.Info("issuer registry configured", "issuers", len(issuerRegistry))
|
||||
|
||||
@@ -400,3 +413,15 @@ func main() {
|
||||
|
||||
logger.Info("certctl server stopped")
|
||||
}
|
||||
|
||||
// getEnvIntDefault parses an integer from a string with a default fallback.
|
||||
func getEnvIntDefault(s string, defaultVal int) int {
|
||||
if s == "" {
|
||||
return defaultVal
|
||||
}
|
||||
val, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return defaultVal
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user