Skip to content

Commit 5662705

Browse files
committed
feat(certs): add support for wildcard certificates and domains with new cert cli commands
Add support for wildcard certificates that also contain SAN, and wildcard domains. The certificate domain model records far more information now. New CLI commands added: `certs:info`, `certs:attach`, and `certs:detach` * `certs:list` output has drastically changed to account for all the new pretty information. * `certs:create` dropped `--san` and `--common-name` as those are automagically discovered by the API. It also now requires a `name` to identify a certificate which is then used in other commands. * `certs:attach` and `certs:detach` are used to tie a single certificate to one or more domains. No need to duplicate a certificate to achieve that. Certificates are stores in k8s as secrets with the certificate name, which get attached to the given application namespace they are in (via `attach`) and router can query for a domain to certificate name mapping
1 parent 72b7367 commit 5662705

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+2449
-383
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ install:
3535
- make prep-bintray-json
3636
script:
3737
- make test
38-
- make -C client/ build test
38+
- make -C client/ bootstrap build test
3939
- mv client/deis client/deis-linux-amd64
4040
- GOOS=darwin GOARCH=amd64 make -C client build
4141
- mv client/deis client/deis-darwin-amd64

client/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export GO15VENDOREXPERIMENT=1
33
# the filepath to this repository, relative to $GOPATH/src
44
repo_path = github.com/deis/workflow/client
55

6-
DEV_ENV_IMAGE := quay.io/deis/go-dev:0.4.0
6+
DEV_ENV_IMAGE := quay.io/deis/go-dev:0.5.0
77
DEV_ENV_WORK_DIR := /go/src/${repo_path}
88
DEV_ENV_PREFIX := docker run --rm -e GO15VENDOREXPERIMENT=1 -e CGO_ENABLED=0 -v ${CURDIR}:${DEV_ENV_WORK_DIR} -w ${DEV_ENV_WORK_DIR}
99
DEV_ENV_CMD := ${DEV_ENV_PREFIX} ${DEV_ENV_IMAGE}

client/cmd/certs.go

Lines changed: 141 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package cmd
33
import (
44
"fmt"
55
"io/ioutil"
6+
"os"
67
"strings"
8+
"time"
79

8-
"github.com/deis/pkg/prettyprint"
10+
"github.com/olekukonko/tablewriter"
911

1012
"github.com/deis/workflow/client/controller/client"
1113
"github.com/deis/workflow/client/controller/models/certs"
@@ -34,45 +36,67 @@ func CertsList(results int) error {
3436
return nil
3537
}
3638

37-
certMap := make(map[string]string)
38-
nameMax := 0
39-
expiresMax := 0
39+
table := tablewriter.NewWriter(os.Stdout)
40+
table.SetAlignment(tablewriter.ALIGN_LEFT)
41+
table.SetBorder(false)
42+
table.SetAutoFormatHeaders(false)
43+
table.SetHeaderLine(true)
44+
table.SetHeader([]string{"Name", "Common Name", "SubjectAltName", "Expires", "Fingerprint", "Domains", "Updated", "Created"})
4045
for _, cert := range certList {
41-
certMap[cert.Name] = cert.Expires
42-
43-
if len(cert.Name) > nameMax {
44-
nameMax = len(cert.Name)
45-
}
46-
if len(cert.Expires) > nameMax {
47-
expiresMax = len(cert.Expires)
46+
domains := strings.Join(cert.Domains[:], ",")
47+
san := strings.Join(cert.SubjectAltName[:], ",")
48+
49+
// Make dates more readable
50+
now := time.Now()
51+
expires := cert.Expires.Time.Format("2 Jan 2006")
52+
created := cert.Created.Time.Format("2 Jan 2006")
53+
updated := cert.Updated.Time.Format("2 Jan 2006")
54+
55+
if cert.Expires.Time.Before(now) {
56+
expires += " (expired)"
57+
} else {
58+
// Ghetto solution
59+
expires += " (in"
60+
year := cert.Expires.Time.Year() - now.Year()
61+
month := cert.Expires.Time.Month() - now.Month()
62+
day := cert.Expires.Time.Day() - now.Day()
63+
64+
if year > 0 {
65+
expires += fmt.Sprintf(" %d year", year)
66+
if year > 1 {
67+
expires += "s"
68+
}
69+
} else if month > 0 {
70+
expires += fmt.Sprintf(" %d month", month)
71+
if month > 1 {
72+
expires += "s"
73+
}
74+
} else if day != 0 {
75+
// special handling on negative days
76+
if day < 0 {
77+
day *= -1
78+
}
79+
80+
expires += fmt.Sprintf(" %d day", day)
81+
if day > 1 {
82+
expires += "s"
83+
}
84+
}
85+
expires += ")"
4886
}
49-
}
5087

51-
nameHeader := "Common Name"
52-
expiresHeader := "Expires"
53-
tabSpaces := 5
54-
bufferSpaces := tabSpaces
88+
// show a shorter version of the fingerprint
89+
fingerprint := cert.Fingerprint[:5] + "[...]" + cert.Fingerprint[len(cert.Fingerprint)-5:]
5590

56-
if nameMax < len(nameHeader) {
57-
tabSpaces += len(nameHeader) - nameMax
58-
nameMax = len(nameHeader)
59-
} else {
60-
bufferSpaces += nameMax - len(nameHeader)
91+
table.Append([]string{cert.Name, cert.CommonName, san, expires, fingerprint, domains, updated, created})
6192
}
93+
table.Render()
6294

63-
if expiresMax < len(expiresHeader) {
64-
expiresMax = len(expiresHeader)
65-
}
66-
67-
fmt.Printf("%s%s%s\n", nameHeader, strings.Repeat(" ", bufferSpaces), expiresHeader)
68-
fmt.Printf("%s%s%s\n", strings.Repeat("-", nameMax), strings.Repeat(" ", 5),
69-
strings.Repeat("-", expiresMax))
70-
fmt.Print(prettyprint.PrettyTabs(certMap, tabSpaces))
7195
return nil
7296
}
7397

7498
// CertAdd adds a cert to the controller.
75-
func CertAdd(cert, key, commonName, sans string) error {
99+
func CertAdd(cert string, key string, name string) error {
76100
c, err := client.New()
77101

78102
if err != nil {
@@ -81,7 +105,7 @@ func CertAdd(cert, key, commonName, sans string) error {
81105

82106
fmt.Print("Adding SSL endpoint... ")
83107
quit := progress()
84-
err = processCertsAdd(c, cert, key, commonName, sans)
108+
err = doCertAdd(c, cert, key, name)
85109
quit <- true
86110
<-quit
87111

@@ -93,48 +117,117 @@ func CertAdd(cert, key, commonName, sans string) error {
93117
return nil
94118
}
95119

96-
func processCertsAdd(c *client.Client, cert, key, commonName, sans string) error {
97-
if sans != "" {
98-
for _, san := range strings.Split(sans, ",") {
99-
if err := doCertAdd(c, cert, key, san); err != nil {
100-
return err
101-
}
102-
}
103-
return nil
120+
func doCertAdd(c *client.Client, cert string, key string, name string) error {
121+
certFile, err := ioutil.ReadFile(cert)
122+
if err != nil {
123+
return err
124+
}
125+
126+
keyFile, err := ioutil.ReadFile(key)
127+
if err != nil {
128+
return err
104129
}
105130

106-
return doCertAdd(c, cert, key, commonName)
131+
_, err = certs.New(c, string(certFile), string(keyFile), name)
132+
return err
107133
}
108134

109-
func doCertAdd(c *client.Client, cert string, key string, commonName string) error {
110-
certFile, err := ioutil.ReadFile(cert)
135+
// CertRemove deletes a cert from the controller.
136+
func CertRemove(name string) error {
137+
c, err := client.New()
138+
if err != nil {
139+
return err
140+
}
111141

142+
fmt.Printf("Removing %s... ", name)
143+
quit := progress()
144+
145+
certs.Delete(c, name)
146+
147+
quit <- true
148+
<-quit
149+
150+
if err == nil {
151+
fmt.Println("done")
152+
}
153+
154+
return err
155+
}
156+
157+
// CertInfo gets info about certficiate
158+
func CertInfo(name string) error {
159+
c, err := client.New()
112160
if err != nil {
113161
return err
114162
}
115163

116-
keyFile, err := ioutil.ReadFile(key)
164+
cert, err := certs.Get(c, name)
165+
if err != nil {
166+
return err
167+
}
168+
169+
domains := strings.Join(cert.Domains[:], ",")
170+
if domains == "" {
171+
domains = "No connected domains"
172+
}
173+
174+
san := strings.Join(cert.SubjectAltName[:], ",")
175+
if san == "" {
176+
san = "N/A"
177+
}
178+
179+
fmt.Printf("=== %s Certificate\n", cert.Name)
180+
fmt.Println("Common Name(s): ", cert.CommonName)
181+
fmt.Println("Expires At: ", cert.Expires)
182+
fmt.Println("Starts At: ", cert.Starts)
183+
fmt.Println("Fingerprint: ", cert.Fingerprint)
184+
fmt.Println("Subject Alt Name: ", san)
185+
fmt.Println("Issuer: ", cert.Issuer)
186+
fmt.Println("Subject: ", cert.Subject)
187+
fmt.Println()
188+
fmt.Println("Connected Domains: ", domains)
189+
fmt.Println("Owner: ", cert.Owner)
190+
fmt.Println("Created: ", cert.Created)
191+
fmt.Println("Updated: ", cert.Updated)
192+
193+
return nil
194+
}
195+
196+
// CertAttach attaches a certificate to a domain
197+
func CertAttach(name string, domain string) error {
198+
c, err := client.New()
117199

118200
if err != nil {
119201
return err
120202
}
121203

122-
_, err = certs.New(c, string(certFile), string(keyFile), commonName)
204+
fmt.Printf("Attaching certificate %s to domain %s... ", name, domain)
205+
quit := progress()
206+
207+
certs.Attach(c, name, domain)
208+
209+
quit <- true
210+
<-quit
211+
212+
if err == nil {
213+
fmt.Println("done")
214+
}
215+
123216
return err
124217
}
125218

126-
// CertRemove deletes a cert from the controller.
127-
func CertRemove(commonName string) error {
219+
// CertDetach detaches a certificate from a domain
220+
func CertDetach(name string, domain string) error {
128221
c, err := client.New()
129222

130223
if err != nil {
131224
return err
132225
}
133226

134-
fmt.Printf("Removing %s... ", commonName)
227+
fmt.Printf("Detaching certificate %s from domain %s... ", name, domain)
135228
quit := progress()
136229

137-
certs.Delete(c, commonName)
230+
certs.Detach(c, name, domain)
138231

139232
quit <- true
140233
<-quit

client/controller/api/certs.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,34 @@
11
package api
22

3+
import "github.com/deis/pkg/time"
4+
35
// Cert is the definition of the cert object.
46
// Some fields are omtempty because they are only
57
// returned when creating or getting a cert.
68
type Cert struct {
7-
Updated string `json:"updated,omitempty"`
8-
Created string `json:"created,omitempty"`
9-
Name string `json:"common_name"`
10-
Expires string `json:"expires"`
11-
Owner string `json:"owner,omitempty"`
12-
ID int `json:"id,omitempty"`
9+
Updated time.Time `json:"updated,omitempty"`
10+
Created time.Time `json:"created,omitempty"`
11+
Name string `json:"name"`
12+
CommonName string `json:"common_name"`
13+
Expires time.Time `json:"expires"`
14+
Starts time.Time `json:"starts"`
15+
Fingerprint string `json:"fingerprint"`
16+
Issuer string `json:"issuer"`
17+
Subject string `json:"subject"`
18+
SubjectAltName []string `json:"san,omitempty"`
19+
Domains []string `json:"domains,omitempty"`
20+
Owner string `json:"owner,omitempty"`
21+
ID int `json:"id,omitempty"`
1322
}
1423

15-
// CertCreateRequest is the definition of POST /v2/certs/.
24+
// CertCreateRequest is the definition of POST and PUT to /v2/certs/
1625
type CertCreateRequest struct {
1726
Certificate string `json:"certificate"`
1827
Key string `json:"key"`
19-
Name string `json:"common_name,omitempty"`
28+
Name string `json:"name"`
29+
}
30+
31+
// CertAttachRequest is the defintion of post to /v2/certs/<cert>/domain
32+
type CertAttachRequest struct {
33+
Domain string `json:"domain"`
2034
}

client/controller/models/certs/certs.go

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,15 @@ func List(c *client.Client, results int) ([]api.Cert, int, error) {
2424
return res, count, nil
2525
}
2626

27-
// New creates a new cert.
28-
func New(c *client.Client, cert string, key string, commonName string) (api.Cert, error) {
29-
req := api.CertCreateRequest{Certificate: cert, Key: key, Name: commonName}
27+
// New creates a cert.
28+
func New(c *client.Client, cert string, key string, name string) (api.Cert, error) {
29+
req := api.CertCreateRequest{Certificate: cert, Key: key, Name: name}
3030
reqBody, err := json.Marshal(req)
31-
3231
if err != nil {
3332
return api.Cert{}, err
3433
}
3534

3635
resBody, err := c.BasicRequest("POST", "/v2/certs/", reqBody)
37-
3836
if err != nil {
3937
return api.Cert{}, err
4038
}
@@ -47,10 +45,45 @@ func New(c *client.Client, cert string, key string, commonName string) (api.Cert
4745
return resCert, nil
4846
}
4947

48+
// Get information for a certificate
49+
func Get(c *client.Client, name string) (api.Cert, error) {
50+
url := fmt.Sprintf("/v2/certs/%s", name)
51+
body, err := c.BasicRequest("GET", url, nil)
52+
if err != nil {
53+
return api.Cert{}, err
54+
}
55+
56+
res := api.Cert{}
57+
if err = json.Unmarshal([]byte(body), &res); err != nil {
58+
return api.Cert{}, err
59+
}
60+
61+
return res, nil
62+
}
63+
5064
// Delete removes a cert.
51-
func Delete(c *client.Client, commonName string) error {
52-
u := fmt.Sprintf("/v2/certs/%s", commonName)
65+
func Delete(c *client.Client, name string) error {
66+
url := fmt.Sprintf("/v2/certs/%s", name)
67+
_, err := c.BasicRequest("DELETE", url, nil)
68+
return err
69+
}
70+
71+
// Attach a certificate to a domain
72+
func Attach(c *client.Client, name string, domain string) error {
73+
req := api.CertAttachRequest{Domain: domain}
74+
reqBody, err := json.Marshal(req)
75+
if err != nil {
76+
return err
77+
}
78+
79+
url := fmt.Sprintf("/v2/certs/%s/domain/", name)
80+
_, err = c.BasicRequest("POST", url, reqBody)
81+
return err
82+
}
5383

54-
_, err := c.BasicRequest("DELETE", u, nil)
84+
// Detach a certificate from a domain
85+
func Detach(c *client.Client, name string, domain string) error {
86+
url := fmt.Sprintf("/v2/certs/%s/domain/%s", name, domain)
87+
_, err := c.BasicRequest("DELETE", url, nil)
5588
return err
5689
}

0 commit comments

Comments
 (0)