Skip to content

Commit

Permalink
feat: Configuring certificate issuers for apps (#232)
Browse files Browse the repository at this point in the history
* listing certificates

* adding and removing cert issuers

* adding unit tests

* linting

* adding confirmation when unset cert issuer
  • Loading branch information
gvicentin authored Oct 17, 2024
1 parent 83be8cd commit 9f33056
Show file tree
Hide file tree
Showing 3 changed files with 371 additions and 84 deletions.
272 changes: 218 additions & 54 deletions tsuru/client/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
package client

import (
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
Expand All @@ -14,8 +16,8 @@ import (
"net/http"
"net/url"
"os"
"sort"
"strings"
"time"

"github.com/tsuru/gnuflag"
"github.com/tsuru/go-tsuruclient/pkg/config"
Expand Down Expand Up @@ -85,7 +87,7 @@ func (c *CertificateSet) Run(context *cmd.Context) error {
return err
}
defer response.Body.Close()
fmt.Fprintln(context.Stdout, "Successfully created the certificated.")
fmt.Fprintln(context.Stdout, "Successfully created the certificate.")
return nil
}

Expand Down Expand Up @@ -167,6 +169,19 @@ func (c *CertificateList) Flags() *gnuflag.FlagSet {
return c.fs
}

type cnameCertificate struct {
Certificate string `json:"certificate"`
Issuer string `json:"issuer"`
}

type routerCertificate struct {
CNameCertificates map[string]cnameCertificate `json:"cnames"`
}

type appCertificate struct {
RouterCertificates map[string]routerCertificate `json:"routers"`
}

func (c *CertificateList) Run(context *cmd.Context) error {
appName, err := c.AppNameByFlag()
if err != nil {
Expand All @@ -185,59 +200,46 @@ func (c *CertificateList) Run(context *cmd.Context) error {
return err
}
defer response.Body.Close()
rawCerts := make(map[string]map[string]string)
err = json.NewDecoder(response.Body).Decode(&rawCerts)
appCerts := appCertificate{}
err = json.NewDecoder(response.Body).Decode(&appCerts)
if err != nil {
return err
}

if c.json {
return c.renderJSON(context, rawCerts)
}

routerNames := []string{}
routerMap := make(map[string][]string)
for k := range rawCerts {
routerNames = append(routerNames, k)
for v := range rawCerts[k] {
routerMap[k] = append(routerMap[k], v)
}
}
sort.Strings(routerNames)
for k := range routerMap {
sort.Strings(routerMap[k])
return c.renderJSON(context, appCerts)
}

if c.raw {
for _, r := range routerNames {
fmt.Fprintf(context.Stdout, "%s:\n", r)
for n, rawCert := range rawCerts[r] {
if rawCert == "" {
rawCert = "No certificate.\n"
for router, routerCerts := range appCerts.RouterCertificates {
fmt.Fprintf(context.Stdout, "%s:\n", router)
for cname, cnameCert := range routerCerts.CNameCertificates {
if cnameCert.Certificate == "" {
fmt.Fprintf(context.Stdout, "%s:\nNo certificate.", cname)
continue
}
fmt.Fprintf(context.Stdout, "%s:\n%s", n, rawCert)
fmt.Fprintf(context.Stdout, "%s:\n%s", cname, cnameCert.Certificate)
}
}

return nil
}

tbl := tablecli.NewTable()
tbl.LineSeparator = true
tbl.Headers = tablecli.Row{"Router", "CName", "Expires", "Issuer", "Subject"}
dateFormat := "2006-01-02 15:04:05"
for r, cnames := range routerMap {
for _, n := range cnames {
rawCert := rawCerts[r][n]
if rawCert == "" {
tbl.AddRow(tablecli.Row{r, n, "-", "-", "-"})
continue
}
cert, err := parseCert([]byte(rawCert))
tbl.Headers = tablecli.Row{"Router", "CName", "Public Key Info", "Certificate Validity"}
for router, routerCerts := range appCerts.RouterCertificates {
for cname, cnameCert := range routerCerts.CNameCertificates {
cert, err := parseCert([]byte(cnameCert.Certificate))
if err != nil {
tbl.AddRow(tablecli.Row{r, n, err.Error(), "-", "-"})
tbl.AddRow(tablecli.Row{router, cname, err.Error(), "-"})
continue
}
tbl.AddRow(tablecli.Row{r, n, formatter.Local(cert.NotAfter).Format(dateFormat),
formatName(&cert.Issuer), formatName(&cert.Subject),
tbl.AddRow(tablecli.Row{
router,
formatCName(cname, cnameCert.Issuer),
formatPublicKeyInfo(*cert),
formatCertificateValidity(*cert),
})
}
}
Expand All @@ -246,7 +248,54 @@ func (c *CertificateList) Run(context *cmd.Context) error {
return nil
}

func (c *CertificateList) renderJSON(context *cmd.Context, rawCerts map[string]map[string]string) error {
func publicKeySize(publicKey interface{}) int {
switch pk := publicKey.(type) {
case *rsa.PublicKey:
return pk.Size() * 8 // convert bytes to bits
case *ecdsa.PublicKey:
return pk.Params().BitSize
}
return 0
}

func formatCName(cname string, issuer string) (cnameStr string) {
cnameStr += fmt.Sprintf("%s\n", cname)

if issuer != "" {
cnameStr += fmt.Sprintln(" managed by: cert-manager")
cnameStr += fmt.Sprintf(" issuer: %s\n", issuer)
}

return
}

func formatPublicKeyInfo(cert x509.Certificate) (pkInfo string) {
publicKey := cert.PublicKeyAlgorithm.String()
if publicKey != "" {
pkInfo += fmt.Sprintf("Algorithm\n%s\n\n", publicKey)
}

publicKeySize := publicKeySize(cert.PublicKey)
if publicKeySize > 0 {
pkInfo += fmt.Sprintf("Key size (in bits)\n%d", publicKeySize)
}

return
}

func formatCertificateValidity(cert x509.Certificate) string {
return fmt.Sprintf(
"Not before\n%s\n\nNot after\n%s",
formatTime(cert.NotBefore),
formatTime(cert.NotAfter),
)
}

func formatTime(t time.Time) string {
return t.UTC().Format(time.RFC3339)
}

func (c *CertificateList) renderJSON(context *cmd.Context, appCerts appCertificate) error {
type certificateJSONFriendly struct {
Router string `json:"router"`
Domain string `json:"domain"`
Expand All @@ -258,19 +307,15 @@ func (c *CertificateList) renderJSON(context *cmd.Context, rawCerts map[string]m

data := []certificateJSONFriendly{}

for router, domainMap := range rawCerts {
domainLoop:
for domain, raw := range domainMap {
if raw == "" {
continue domainLoop
}
for router, routerCerts := range appCerts.RouterCertificates {
for cname, cnameCert := range routerCerts.CNameCertificates {
item := certificateJSONFriendly{
Domain: domain,
Domain: cname,
Router: router,
Raw: raw,
Raw: cnameCert.Certificate,
}

parsedCert, err := parseCert([]byte(raw))
parsedCert, err := parseCert([]byte(cnameCert.Certificate))
if err == nil {
item.Issuer = &parsedCert.Issuer
item.Subject = &parsedCert.Subject
Expand Down Expand Up @@ -298,11 +343,130 @@ func parseCert(data []byte) (*x509.Certificate, error) {
return cert, nil
}

func formatName(n *pkix.Name) string {
country := strings.Join(n.Country, ",")
state := strings.Join(n.Province, ",")
locality := strings.Join(n.Locality, ",")
org := strings.Join(n.Organization, ",")
cname := n.CommonName
return fmt.Sprintf("C=%s; ST=%s; \nL=%s; O=%s;\nCN=%s", country, state, locality, org, cname)
type CertificateIssuerSet struct {
tsuruClientApp.AppNameMixIn
cname string
fs *gnuflag.FlagSet
}

func (c *CertificateIssuerSet) Info() *cmd.Info {
return &cmd.Info{
Name: "certificate-issuer-set",
Usage: "certificate issuer set [-a/--app appname] [-c/--cname CNAME] [issuer]",
Desc: `Creates or update a certificate issuer into the specific app.`,
MinArgs: 1,
}
}

func (c *CertificateIssuerSet) Flags() *gnuflag.FlagSet {
if c.fs == nil {
c.fs = c.AppNameMixIn.Flags()
cname := "App CNAME"
c.fs.StringVar(&c.cname, "cname", "", cname)
c.fs.StringVar(&c.cname, "c", "", cname)
}
return c.fs
}

func (c *CertificateIssuerSet) Run(context *cmd.Context) error {
appName, err := c.AppNameByFlag()
if err != nil {
return err
}

if c.cname == "" {
return errors.New("You must set cname.")
}

issuer := context.Args[0]
if issuer == "" {
return errors.New("You must set issuer.")
}

v := url.Values{}
v.Set("cname", c.cname)
v.Set("issuer", issuer)
u, err := config.GetURLVersion("1.0", fmt.Sprintf("/apps/%s/certissuer", appName))
if err != nil {
return err
}

request, err := http.NewRequest(http.MethodPut, u, strings.NewReader(v.Encode()))
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
response, err := tsuruHTTP.AuthenticatedClient.Do(request)
if err != nil {
return err
}
defer response.Body.Close()

fmt.Fprintln(context.Stdout, "Successfully created the certificate issuer.")
return nil
}

type CertificateIssuerUnset struct {
tsuruClientApp.AppNameMixIn
cmd.ConfirmationCommand
fs *gnuflag.FlagSet
cname string
}

func (c *CertificateIssuerUnset) Info() *cmd.Info {
return &cmd.Info{
Name: "certificate-issuer-unset",
Usage: "certificate issuer unset [-a/--app appname] [-c/--cname CNAME] [-y/--assume-yes]",
Desc: `Unset a certificate issuer from a specific app.`,
}
}

func (c *CertificateIssuerUnset) Flags() *gnuflag.FlagSet {
if c.fs == nil {
c.fs = mergeFlagSet(
c.AppNameMixIn.Flags(),
c.ConfirmationCommand.Flags(),
)

cname := "App CNAME"
c.fs.StringVar(&c.cname, "cname", "", cname)
c.fs.StringVar(&c.cname, "c", "", cname)
}
return c.fs
}

func (c *CertificateIssuerUnset) Run(context *cmd.Context) error {
appName, err := c.AppNameByFlag()
if err != nil {
return err
}

if c.cname == "" {
return errors.New("You must set cname.")
}

if !c.Confirm(context, fmt.Sprintf(`Are you sure you want to remove certificate issuer for cname: "%s"?`, c.cname)) {
return nil
}

v := url.Values{}
v.Set("cname", c.cname)
u, err := config.GetURLVersion("1.0", fmt.Sprintf("/apps/%s/certissuer?%s", appName, v.Encode()))
if err != nil {
return err
}

request, err := http.NewRequest(http.MethodDelete, u, nil)
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
response, err := tsuruHTTP.AuthenticatedClient.Do(request)
if err != nil {
return err
}
defer response.Body.Close()

fmt.Fprintln(context.Stdout, "Certificate issuer removed.")
return nil
}
Loading

0 comments on commit 9f33056

Please sign in to comment.