From 2a91bcf14764693a34e044e3a5fc9ed31d45fa80 Mon Sep 17 00:00:00 2001 From: Jan Broer Date: Thu, 31 Mar 2016 03:47:33 +0200 Subject: [PATCH] Support for claiming existing certificate + Store certificate in Volume * Adds a new required "certificate name" parameter (backward compatible): When there is an existing certificate by that name it will be overwritten with a renewed certificate * fullchain.pem and privatekey.pem are now stored in a Volume under /etc/letsencrypt/production/certificates/. This is until we find a better way to share the private key with other services. --- Dockerfile | 3 +- Dockerfile.dev | 3 + Makefile | 2 +- VERSION | 2 +- context.go | 30 ++++-- letsencrypt/account.go | 25 +++-- letsencrypt/client.go | 220 ++++++++++++++++++++++++--------------- letsencrypt/providers.go | 10 +- manager.go | 80 +++++++------- rancher/certificate.go | 19 ++-- rancher/loadbalancer.go | 20 ++-- rancher/wait.go | 2 +- 12 files changed, 252 insertions(+), 164 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6a1ed5d..6d0e652 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,9 +3,10 @@ MAINTAINER Jan Broer RUN apk add --no-cache ca-certificates -ENV LETSENCRYPT_RELEASE v0.2.5 +ENV LETSENCRYPT_RELEASE v0.2.6 ADD https://github.com/janeczku/rancher-letsencrypt/releases/download/${LETSENCRYPT_RELEASE}/rancher-letsencrypt-linux-amd64.tar.gz /tmp/rancher-letsencrypt.tar.gz + RUN tar -zxvf /tmp/rancher-letsencrypt.tar.gz -C /usr/bin \ && chmod +x /usr/bin/rancher-letsencrypt diff --git a/Dockerfile.dev b/Dockerfile.dev index 4cf0025..913a0ae 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -4,8 +4,11 @@ MAINTAINER Jan Broer RUN apk add --no-cache ca-certificates ADD build/rancher-letsencrypt-linux-amd64 /usr/bin/rancher-letsencrypt + RUN chmod +x /usr/bin/rancher-letsencrypt VOLUME /etc/letsencrypt +ENV DEBUG=true + ENTRYPOINT ["/usr/bin/rancher-letsencrypt"] \ No newline at end of file diff --git a/Makefile b/Makefile index 5632081..6557c2f 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ dockerhub: image docker push $(DOCKER_IMAGE):$(VERSION) image: - docker build -t $(DOCKER_IMAGE):$(VERSION) -f Dockerfile.dev . + docker build -t $(DOCKER_IMAGE):dev-$(SHA) -f Dockerfile.dev . version: @echo $(VERSION) $(SHA) diff --git a/VERSION b/VERSION index b88fb90..400feeb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.2.5 +v0.2.6 diff --git a/context.go b/context.go index 0276280..45d01bd 100644 --- a/context.go +++ b/context.go @@ -12,7 +12,7 @@ import ( ) const ( - DESCRIPTION = "Created by Let's Encrypt Certificate Manager" + CERT_DESCRIPTION = "Created by Let's Encrypt Certificate Manager" ISSUER_PRODUCTION = "Let's Encrypt" ISSUER_STAGING = "fake CA" PREFIX_PROD = "[LE] " @@ -38,12 +38,13 @@ func (c *Context) InitContext() { cattleUrl := getEnvOption("CATTLE_URL", true) cattleApiKey := getEnvOption("CATTLE_ACCESS_KEY", true) cattleSecretKey := getEnvOption("CATTLE_SECRET_KEY", true) - debug := getEnvOption("DEBUG", false) + debugParam := getEnvOption("DEBUG", false) eulaParam := getEnvOption("EULA", false) apiVerParam := getEnvOption("API_VERSION", true) emailParam := getEnvOption("EMAIL", true) domainParam := getEnvOption("DOMAINS", true) keyTypeParam := getEnvOption("PUBLIC_KEY_TYPE", true) + certNameParam := getEnvOption("CERT_NAME", false) timeParam := getEnvOption("RENEWAL_TIME", true) providerParam := getEnvOption("PROVIDER", true) @@ -63,18 +64,26 @@ func (c *Context) InitContext() { logrus.Fatalf("Invalid value for RENEWAL_TIME: %s", timeParam) } - var serverURI string + var certAutoName string + switch apiVerParam { case "Production": - serverURI = letsencrypt.PRODUCTION_URI - c.RancherCertName = PREFIX_PROD + c.Domains[0] + certAutoName = PREFIX_PROD + c.Domains[0] case "Sandbox": - serverURI = letsencrypt.STAGING_URI - c.RancherCertName = PREFIX_STAGING + c.Domains[0] + certAutoName = PREFIX_STAGING + c.Domains[0] default: logrus.Fatalf("Invalid value for API_VERSION: %s", apiVerParam) } + apiVersion := letsencrypt.ApiVersion(apiVerParam) + keyType := letsencrypt.KeyType(keyTypeParam) + + if len(certNameParam) != 0 { + c.RancherCertName = certNameParam + } else { + c.RancherCertName = certAutoName + } + c.Rancher, err = rancher.NewClient(cattleUrl, cattleApiKey, cattleSecretKey) if err != nil { logrus.Fatalf("Rancher client: %v", err) @@ -92,19 +101,18 @@ func (c *Context) InitContext() { DynCustomerName: os.Getenv("DYN_CUSTOMER_NAME"), DynUserName: os.Getenv("DYN_USER_NAME"), DynPassword: os.Getenv("DYN_PASSWORD"), - AwsRegionName: "us-east-1", } - c.Acme, err = letsencrypt.NewClient(emailParam, keyTypeParam, serverURI, providerOpts) + c.Acme, err = letsencrypt.NewClient(emailParam, keyType, apiVersion, providerOpts) if err != nil { logrus.Fatalf("LetsEncrypt client: %v", err) } // Enable debug/test mode - if strings.EqualFold(debug, "true") { + if strings.EqualFold(debugParam, "true") { logrus.SetLevel(logrus.DebugLevel) c.Debug = true - c.Acme.EnableDebugLogging() + c.Acme.EnableDebug() } } diff --git a/letsencrypt/account.go b/letsencrypt/account.go index 236df0f..a8646bc 100644 --- a/letsencrypt/account.go +++ b/letsencrypt/account.go @@ -7,21 +7,25 @@ import ( "io/ioutil" "os" "path" + "strings" "github.com/Sirupsen/logrus" lego "github.com/xenolf/lego/acme" ) type Account struct { - Email string `json:"email"` - key crypto.PrivateKey + Email string `json:"email"` Registration *lego.RegistrationResource `json:"registrations"` + + key crypto.PrivateKey + path string } // NewAccount creates a new or gets a stored LE account for the given email -func NewAccount(email string, keyType lego.KeyType) (*Account, error) { - keyFile := path.Join(configPath(), email+".key") - accountFile := path.Join(configPath(), email+".json") +func NewAccount(email string, apiVer ApiVersion, keyType lego.KeyType) (*Account, error) { + accPath := accountPath(email, apiVer) + keyFile := path.Join(accPath, "account.key") + accountFile := path.Join(accPath, "account.json") var privKey crypto.PrivateKey if _, err := os.Stat(keyFile); os.IsNotExist(err) { @@ -39,7 +43,7 @@ func NewAccount(email string, keyType lego.KeyType) (*Account, error) { } if _, err := os.Stat(accountFile); os.IsNotExist(err) { - return &Account{Email: email, key: privKey}, nil + return &Account{Email: email, key: privKey, path: accPath}, nil } fileBytes, err := ioutil.ReadFile(accountFile) @@ -54,6 +58,7 @@ func NewAccount(email string, keyType lego.KeyType) (*Account, error) { } acc.key = privKey + acc.path = accPath return &acc, nil } @@ -63,7 +68,7 @@ func (a *Account) Save() error { if err != nil { return err } - accountFile := path.Join(configPath(), a.Email+".json") + accountFile := path.Join(a.path, "account.json") return ioutil.WriteFile(accountFile, jsonBytes, 0700) } @@ -83,3 +88,9 @@ func (a *Account) GetPrivateKey() crypto.PrivateKey { func (a *Account) GetRegistration() *lego.RegistrationResource { return a.Registration } + +func accountPath(email string, apiVer ApiVersion) string { + path := path.Join(ConfigDir, strings.ToLower(string(apiVer)), "accounts", email) + maybeCreatePath(path) + return path +} diff --git a/letsencrypt/client.go b/letsencrypt/client.go index 99e1297..c82aba2 100644 --- a/letsencrypt/client.go +++ b/letsencrypt/client.go @@ -15,54 +15,81 @@ import ( ) const ( - PRODUCTION_URI = "https://acme-v01.api.letsencrypt.org/directory" - STAGING_URI = "https://acme-staging.api.letsencrypt.org/directory" - DATA_DIR = "/etc/letsencrypt" + ConfigDir = "/etc/letsencrypt" + ProductionApiUri = "https://acme-v01.api.letsencrypt.org/directory" + StagingApiUri = "https://acme-staging.api.letsencrypt.org/directory" +) + +type KeyType string + +const ( + RSA2048 KeyType = "RSA-2048" + RSA4096 KeyType = "RSA-4096" + RSA8192 KeyType = "RSA-8192" + EC256 KeyType = "ECDSA-256" + EC384 KeyType = "ECDSA-384" +) + +type ApiVersion string + +const ( + Production ApiVersion = "Production" + Sandbox ApiVersion = "Sandbox" ) // AcmeCertificate represents a CA issued certificate, // PrivateKey and Certificate are both PEM encoded. +// +// Anonymous fields: +// PrivateKey []byte +// Certificate []byte +// Domain string type AcmeCertificate struct { - PrivateKey []byte `json:"privateKey"` - Certificate []byte `json:"certificate"` + lego.CertificateResource ExpiryDate time.Time `json:"expiryDate"` SerialNumber string `json:"serialnumber"` } -type acmeCertificateStore struct { - CertRes lego.CertificateResource `json:"certRes"` - AcmeCert AcmeCertificate `json:"acmeCert"` -} - // Client represents a Lets Encrypt client type Client struct { - client *lego.Client + client *lego.Client + apiVersion ApiVersion } // NewClient returns a new Lets Encrypt client -func NewClient(email, keyTypeStr, uri string, provider ProviderOpts) (*Client, error) { +func NewClient(email string, kt KeyType, apiVer ApiVersion, provider ProviderOpts) (*Client, error) { var keyType lego.KeyType - switch strings.ToUpper(keyTypeStr) { - case "RSA-2048": + switch kt { + case RSA2048: keyType = lego.RSA2048 - case "RSA-4096": + case RSA4096: keyType = lego.RSA4096 - case "RSA-8192": + case RSA8192: keyType = lego.RSA8192 - case "ECDSA-256": + case EC256: keyType = lego.EC256 - case "ECDSA-384": + case EC384: keyType = lego.EC384 default: - return nil, fmt.Errorf("Invalid private key type: %s", keyTypeStr) + return nil, fmt.Errorf("Invalid private key type: %s", string(kt)) + } + + var serverUri string + switch apiVer { + case Production: + serverUri = ProductionApiUri + case Sandbox: + serverUri = StagingApiUri + default: + return nil, fmt.Errorf("Invalid LE API version: %s", string(apiVer)) } - acc, err := NewAccount(email, keyType) + acc, err := NewAccount(email, apiVer, keyType) if err != nil { return nil, fmt.Errorf("Could not initialize account store for %s: %v", email, err) } - client, err := lego.NewClient(uri, acc, keyType) + client, err := lego.NewClient(serverUri, acc, keyType) if err != nil { return nil, fmt.Errorf("Could not create client: %v", err) } @@ -105,10 +132,16 @@ func NewClient(email, keyTypeStr, uri string, provider ProviderOpts) (*Client, e client.ExcludeChallenges([]lego.Challenge{lego.HTTP01, lego.TLSSNI01}) return &Client{ - client: client, + client: client, + apiVersion: apiVer, }, nil } +// EnableDebugLogging enables logging in the upstream lego library +func (c *Client) EnableDebug() { + lego.Logger = log.New(os.Stdout, "", 0) +} + // Issue obtains a new SAN certificate from the Lets Encrypt CA func (c *Client) Issue(domains []string) (*AcmeCertificate, map[string]error) { cert, failures := c.client.ObtainCertificate(domains, true, nil) @@ -117,30 +150,26 @@ func (c *Client) Issue(domains []string) (*AcmeCertificate, map[string]error) { } expirydate, _ := lego.GetPEMCertExpiration(cert.Certificate) - serial, _ := getPEMCertSerialNo(cert.Certificate) + serialnum, _ := getPEMCertSerialNo(cert.Certificate) acmeCert := AcmeCertificate{ - cert.PrivateKey, - cert.Certificate, - expirydate, - serial, + CertificateResource: cert, + ExpiryDate: expirydate, + SerialNumber: serialnum, } - saveCertStore(cert, acmeCert) + c.saveCertificate(acmeCert) return &acmeCert, nil } // Renew obtains a renewed SAN certificate from the Lets Encrypt CA func (c *Client) Renew(domains []string) (*AcmeCertificate, error) { domain := domains[0] - certStore, err := loadCertStore(domain) + currentCert, err := c.loadCertificate(domain) if err != nil { - return nil, err + logrus.Fatalf(err.Error()) } - - certRes := certStore.CertRes - certRes.PrivateKey = certStore.AcmeCert.PrivateKey - certRes.Certificate = certStore.AcmeCert.Certificate + certRes := currentCert.CertificateResource newCert, err := c.client.RenewCertificate(certRes, true) if err != nil { @@ -148,98 +177,125 @@ func (c *Client) Renew(domains []string) (*AcmeCertificate, error) { } expirydate, _ := lego.GetPEMCertExpiration(newCert.Certificate) - serial, _ := getPEMCertSerialNo(newCert.Certificate) + serialnum, _ := getPEMCertSerialNo(newCert.Certificate) acmeCert := AcmeCertificate{ - newCert.PrivateKey, - newCert.Certificate, - expirydate, - serial, + CertificateResource: newCert, + ExpiryDate: expirydate, + SerialNumber: serialnum, } - saveCertStore(newCert, acmeCert) + c.saveCertificate(acmeCert) return &acmeCert, nil } -// GetStoredCert returns a locally stored certificate for the given domains -func (c *Client) GetStoredCert(domains []string) (bool, *AcmeCertificate) { +// GetStoredCertificate returns a locally stored certificate for the given domains +func (c *Client) GetStoredCertificate(domains []string) (bool, *AcmeCertificate) { domain := domains[0] - if !haveCertStore(domain) { + if !c.haveCertificate(domain) { return false, nil } - certStore, err := loadCertStore(domain) + acmeCert, err := c.loadCertificate(domain) if err != nil { - logrus.Error(err) + // Don't fail here. Try to create a new certificate instead. + logrus.Error(err.Error()) return false, nil } - return true, &certStore.AcmeCert -} - -// EnableDebugLogging enables logging in the upstream lego library -func (c *Client) EnableDebugLogging() { - lego.Logger = log.New(os.Stdout, "", 0) + return true, &acmeCert } -func haveCertStore(domain string) bool { - path := certStorePath(domain) - if _, err := os.Stat(path); os.IsNotExist(err) { +func (c *Client) haveCertificate(domain string) bool { + certPath := c.CertPath(domain) + if _, err := os.Stat(path.Join(certPath, "metadata.json")); err != nil { + logrus.Debugf("No existing acme certificate resource found for '%s'", domain) return false } return true } -func loadCertStore(domain string) (*acmeCertificateStore, error) { - file := certStorePath(domain) - certStoreBytes, err := ioutil.ReadFile(file) +func (c *Client) loadCertificate(domain string) (AcmeCertificate, error) { + var acmeCert AcmeCertificate + certPath := c.CertPath(domain) + + logrus.Debugf("Loading acme certificate resource for '%s' from '%s'", domain, certPath) + + certIn := path.Join(certPath, "fullchain.pem") + privIn := path.Join(certPath, "privkey.pem") + metaIn := path.Join(certPath, "metadata.json") + + certBytes, err := ioutil.ReadFile(certIn) + if err != nil { + return acmeCert, fmt.Errorf("Failed to load certificate for domain '%s': %s", domain, err.Error()) + } + + metaBytes, err := ioutil.ReadFile(metaIn) + if err != nil { + return acmeCert, fmt.Errorf("Failed to load meta data for domain '%s': %s", domain, err.Error()) + } + + keyBytes, err := ioutil.ReadFile(privIn) if err != nil { - return nil, fmt.Errorf("Error loading certificate store for %s: %v", domain, err) + return acmeCert, fmt.Errorf("Failed to load private key for domain '%s': %s", domain, err.Error()) } - var certStore acmeCertificateStore - err = json.Unmarshal(certStoreBytes, &certStore) + err = json.Unmarshal(metaBytes, &acmeCert) if err != nil { - return nil, fmt.Errorf("Error parsing certificate store for %s: %v", domain, err) + return acmeCert, fmt.Errorf("Failed to unmarshal meta data for domain '%s': %s", domain, err.Error()) } - return &certStore, nil + acmeCert.PrivateKey = keyBytes + acmeCert.Certificate = certBytes + + return acmeCert, nil } -func saveCertStore(certRes lego.CertificateResource, acmeCert AcmeCertificate) { - out := certStorePath(certRes.Domain) +func (c *Client) saveCertificate(acmeCert AcmeCertificate) { + certPath := c.CertPath(acmeCert.Domain) + maybeCreatePath(certPath) + + logrus.Debugf("Saving acme certificate resource for '%s' to '%s'", acmeCert.Domain, certPath) + + certOut := path.Join(certPath, "fullchain.pem") + privOut := path.Join(certPath, "privkey.pem") + metaOut := path.Join(certPath, "metadata.json") - certStore := acmeCertificateStore{ - CertRes: certRes, - AcmeCert: acmeCert, + err := ioutil.WriteFile(certOut, acmeCert.Certificate, 0600) + if err != nil { + logrus.Fatalf("Failed to save certificate for domain %s\n\t%s", acmeCert.Domain, err.Error()) } - jsonBytes, err := json.MarshalIndent(certStore, "", "\t") + err = ioutil.WriteFile(privOut, acmeCert.PrivateKey, 0600) if err != nil { - logrus.Fatalf("Unable to marshal certificate store for domain %s: %s", certRes.Domain, err.Error()) + logrus.Fatalf("Failed to save private key for domain %s\n\t%s", acmeCert.Domain, err.Error()) } - err = ioutil.WriteFile(out, jsonBytes, 0600) + jsonBytes, err := json.MarshalIndent(acmeCert, "", "\t") if err != nil { - logrus.Fatalf("Unable to save certificate store for domain %s: %s", certRes.Domain, err.Error()) + logrus.Fatalf("Failed to marshal meta data for domain %s\n\t%s", acmeCert.Domain, err.Error()) } -} -func configPath() string { - path := path.Join(DATA_DIR, ".acme") - if err := checkFolder(path); err != nil { - logrus.Fatalf("Could not check/create config directory: %v", err) + err = ioutil.WriteFile(metaOut, jsonBytes, 0600) + if err != nil { + logrus.Fatalf("Failed to save meta data for domain %s\n\t%s", acmeCert.Domain, err.Error()) } - return path } -func certStorePath(domain string) string { - path := path.Join(configPath(), domain+".json") +func (c *Client) ConfigPath() string { + path := path.Join(ConfigDir, strings.ToLower(string(c.apiVersion))) + maybeCreatePath(path) return path } -func checkFolder(path string) error { +func (c *Client) CertPath(domain string) string { + return path.Join(c.ConfigPath(), "certificates", domain) +} + +func maybeCreatePath(path string) { if _, err := os.Stat(path); os.IsNotExist(err) { - return os.MkdirAll(path, 0700) + err = os.MkdirAll(path, 0700) + if err != nil { + logrus.Fatalf("Error creating path: %v", err) + } } - return nil } diff --git a/letsencrypt/providers.go b/letsencrypt/providers.go index b54a8c7..3bf899d 100644 --- a/letsencrypt/providers.go +++ b/letsencrypt/providers.go @@ -25,9 +25,8 @@ type ProviderOpts struct { DoAccessToken string // AWS Route 53 credentials - AwsAccessKey string - AwsSecretKey string - AwsRegionName string + AwsAccessKey string + AwsSecretKey string // DNSimple credentials DNSimpleEmail string @@ -105,11 +104,8 @@ func makeRoute53Provider(opts ProviderOpts) (lego.ChallengeProvider, error) { if len(opts.AwsSecretKey) == 0 { return nil, fmt.Errorf("AWS secret key is not set") } - if len(opts.AwsRegionName) == 0 { - return nil, fmt.Errorf("AWS region name is not set") - } - os.Setenv("AWS_REGION", opts.AwsRegionName) + os.Setenv("AWS_REGION", "us-east-1") os.Setenv("AWS_ACCESS_KEY_ID", opts.AwsAccessKey) os.Setenv("AWS_SECRET_ACCESS_KEY", opts.AwsSecretKey) diff --git a/manager.go b/manager.go index 7bd1882..7b592ca 100644 --- a/manager.go +++ b/manager.go @@ -21,40 +21,38 @@ func (c *Context) Run() { } func (c *Context) startup() { - var haveLocal bool - var haveRemote bool - - ok, acmeCert := c.Acme.GetStoredCert(c.Domains) + var haveLocal, haveRemote bool + ok, acmeCert := c.Acme.GetStoredCertificate(c.Domains) if ok { haveLocal = true c.ExpiryDate = acmeCert.ExpiryDate - logrus.Info("Found local store for certificate") + logrus.Infof("Found locally stored certificate for '%s'", strings.Join(c.Domains, " | ")) } rancherCert, err := c.Rancher.FindCertByName(c.RancherCertName) if err != nil { - logrus.Fatalf("Failed to lookup Rancher certificates: %v", err) + logrus.Fatalf("Error looking up Rancher certificates: %v", err) } if rancherCert != nil { haveRemote = true c.RancherCertId = rancherCert.Id + logrus.Infof("Found existing Rancher certificate '%s'", rancherCert.Name) } if haveLocal && haveRemote { - if rancherCert.SerialNumber != acmeCert.SerialNumber { - logrus.Fatalf("Cannot manage existing Rancher certificate %s: Serial number not matching local store", rancherCert.Name) + if rancherCert.SerialNumber == acmeCert.SerialNumber { + logrus.Infof("Managing renewal of Rancher certificate '%s'", rancherCert.Name) + return } - logrus.Infof("Managing existing Rancher certificate: %s", rancherCert.Name) + logrus.Infof("Certificate serial number mismatch. Overwriting Rancher certificate '%s'", rancherCert.Name) + c.updateRancherCert(acmeCert.PrivateKey, acmeCert.Certificate) return } - if !haveLocal && haveRemote { - logrus.Fatalf("Cannot manage existing Rancher certificate %s: Not in local store", rancherCert.Name) - } - if haveLocal && !haveRemote { - c.addCertToRancher(acmeCert.PrivateKey, acmeCert.Certificate) + logrus.Infof("Adding Rancher certificate '%s'", rancherCert.Name) + c.addRancherCert(acmeCert.PrivateKey, acmeCert.Certificate) return } @@ -63,29 +61,48 @@ func (c *Context) startup() { acmeCert, failures := c.Acme.Issue(c.Domains) if len(failures) > 0 { for k, v := range failures { - logrus.Errorf("[%s] Failed to obtain certificate: %s", k, v.Error()) + logrus.Errorf("[%s] Error obtaining certificate: %s", k, v.Error()) } os.Exit(1) } - logrus.Infof("Successfully obtained certificate") + logrus.Infof("Successfully obtained SSL certificate") c.ExpiryDate = acmeCert.ExpiryDate - c.addCertToRancher(acmeCert.PrivateKey, acmeCert.Certificate) + if haveRemote { + logrus.Infof("Overwriting Rancher certificate '%s'", rancherCert.Name) + c.updateRancherCert(acmeCert.PrivateKey, acmeCert.Certificate) + return + } + + c.addRancherCert(acmeCert.PrivateKey, acmeCert.Certificate) } -func (c *Context) addCertToRancher(privateKey, cert []byte) { - rancherCert, err := c.Rancher.AddCertificate(c.RancherCertName, DESCRIPTION, privateKey, cert) +func (c *Context) addRancherCert(privateKey, cert []byte) { + rancherCert, err := c.Rancher.AddCertificate(c.RancherCertName, CERT_DESCRIPTION, privateKey, cert) if err != nil { - logrus.Fatalf("Failed to add Rancher certificate resource: %v", err) + logrus.Fatalf("Failed to add Rancher certificate '%s': %v", c.RancherCertName, err) } c.RancherCertId = rancherCert.Id - logrus.Infof("Added Rancher certificate resource: %s", c.RancherCertName) + logrus.Infof("Added Rancher certificate '%s'", c.RancherCertName) +} + +func (c *Context) updateRancherCert(privateKey, cert []byte) { + err := c.Rancher.UpdateCertificate(c.RancherCertId, CERT_DESCRIPTION, privateKey, cert) + if err != nil { + logrus.Fatalf("Failed to update Rancher certificate '%s': %v", c.RancherCertName, err) + } + logrus.Infof("Updated Rancher certificate '%s'", c.RancherCertName) + + err = c.Rancher.UpgradeLoadBalancers(c.RancherCertId) + if err != nil { + logrus.Fatalf("Error upgrading load balancers: %v", err) + } } func (c *Context) renew() { - logrus.Infof("Trying to renew certificate for %s", strings.Join(c.Domains, " | ")) + logrus.Infof("Trying to renew certificate for '%s'", strings.Join(c.Domains, " | ")) acmeCert, err := c.Acme.Renew(c.Domains) if err != nil { @@ -95,17 +112,7 @@ func (c *Context) renew() { logrus.Infof("Successfully renewed certificate") c.ExpiryDate = acmeCert.ExpiryDate - err = c.Rancher.UpdateCertificate(c.RancherCertId, acmeCert.Certificate) - if err != nil { - logrus.Fatalf("Failed to update Rancher certificate resource: %v", err) - } - - logrus.Infof("Updated Rancher certificate resource %s", c.RancherCertName) - - err = c.Rancher.UpgradeLoadBalancers(c.RancherCertId) - if err != nil { - logrus.Fatalf("Error upgrading load balancers: %v", err) - } + c.updateRancherCert(acmeCert.PrivateKey, acmeCert.Certificate) } func (c *Context) timer() <-chan time.Time { @@ -116,13 +123,14 @@ func (c *Context) timer() <-chan time.Time { left = 10 * time.Second } - // Test mode + logrus.Infof("Next certificate renewal scheduled for %s", next.Format("2006/01/02 15:04 MST")) + + // Debug option set to true enables test mode if c.Debug { - logrus.Debug("Test mode enabled: Certificate renewal in 120 seconds") + logrus.Debug("Test mode enabled: certificate will be renewed in 120 seconds") left = 120 * time.Second } - logrus.Infof("Next certificate renewal scheduled for %s", next.Format("2006/01/02 15:04 MST")) return time.After(left) } diff --git a/rancher/certificate.go b/rancher/certificate.go index 272d8dd..b7af7ad 100644 --- a/rancher/certificate.go +++ b/rancher/certificate.go @@ -24,7 +24,7 @@ func (r *Client) AddCertificate(name, descr string, privateKey, cert []byte) (*r return nil, err } - logrus.Debugf("Added certificate: %s", rancherCert.Name) + logrus.Debugf("Waiting for added certificate '%s' to become active", rancherCert.Name) if err := r.WaitCertificate(rancherCert); err != nil { return nil, err @@ -34,28 +34,31 @@ func (r *Client) AddCertificate(name, descr string, privateKey, cert []byte) (*r } // UpdateCertificate updates an existing certificate resource using the given PEM encoded certificate -func (r *Client) UpdateCertificate(certId string, cert []byte) error { +func (r *Client) UpdateCertificate(certId, descr string, privateKey, cert []byte) error { certString := string(cert[:]) + keyString := string(privateKey[:]) rancherCert, err := r.client.Certificate.ById(certId) if err != nil { return err } rancherCert, err = r.client.Certificate.Update(rancherCert, &rancherClient.Certificate{ - Cert: certString, + Description: descr, + Cert: certString, + Key: keyString, }) if err != nil { return err } - logrus.Debugf("Updated certificate %s", rancherCert.Name) + logrus.Debugf("Waiting for updated certificate '%s' to become active", rancherCert.Name) return r.WaitCertificate(rancherCert) } // FindCertByName retrieves an existing certificate func (r *Client) FindCertByName(name string) (*rancherClient.Certificate, error) { - logrus.Debugf("Looking up certificate by name %s", name) + logrus.Debugf("Looking up Rancher certificate by name: '%s'", name) certificates, err := r.client.Certificate.List(&rancherClient.ListOpts{ Filters: map[string]interface{}{ @@ -72,7 +75,7 @@ func (r *Client) FindCertByName(name string) (*rancherClient.Certificate, error) return nil, nil } - logrus.Debugf("Found existing certificate %s", name) + logrus.Debugf("Found existing Rancher certificate by name: '%s'", name) return &certificates.Data[0], nil } @@ -84,9 +87,9 @@ func (r *Client) GetCertById(certId string) (*rancherClient.Certificate, error) } if rancherCert == nil { - return nil, fmt.Errorf("Certificate with Id %s does not exist.", certId) + return nil, fmt.Errorf("Rancher certificate with Id '%s' does not exist", certId) } - logrus.Debugf("Found certificate %s by Id %s", rancherCert.Name, certId) + logrus.Debugf("Got Rancher certificate '%s' by Id '%s'", rancherCert.Name, certId) return rancherCert, nil } diff --git a/rancher/loadbalancer.go b/rancher/loadbalancer.go index 7df898d..f44a476 100644 --- a/rancher/loadbalancer.go +++ b/rancher/loadbalancer.go @@ -13,22 +13,22 @@ func (r *Client) UpgradeLoadBalancers(certId string) error { } if len(balancers) == 0 { - logrus.Info("Certificate is not being used by any load balancers") + logrus.Info("Certificate is not being used by any load balancer") return nil } for _, id := range balancers { lb, err := r.client.LoadBalancerService.ById(id) if err != nil { - logrus.Errorf("Failed to acquire load balancer by id %s: %v", id, err) + logrus.Errorf("Failed to get load balancer by id '%s': %v", id, err) continue } - logrus.Infof("Upgrading load balancer %s...", lb.Name) + err = r.upgrade(lb) if err != nil { - logrus.Errorf("Failed to upgrade load balancer %s: %v", lb.Name, err) + logrus.Errorf("Failed to upgrade load balancer '%s': %v", lb.Name, err) } else { - logrus.Infof("Successfully upgraded load balancer %s with renewed certificate", lb.Name) + logrus.Infof("Upgraded load balancer '%s' with renewed certificate", lb.Name) } } @@ -43,6 +43,8 @@ func (r *Client) upgrade(lb *rancherClient.LoadBalancerService) error { } upgrade.ToServiceStrategy = &rancherClient.ToServiceUpgradeStrategy{} + logrus.Debugf("Upgrading load balancer '%s'", lb.Name) + service, err := r.client.LoadBalancerService.ActionUpgrade(lb, upgrade) if err != nil { return err @@ -50,11 +52,11 @@ func (r *Client) upgrade(lb *rancherClient.LoadBalancerService) error { err = r.WaitService(service) if err != nil { - logrus.Warnf("Upgrade check: %v", err) + logrus.Warnf(err.Error()) } if service.State == "upgraded" { - logrus.Debugf("Finishing upgrade of load balancer: %s", lb.Name) + logrus.Debugf("Finishing upgrade for load balancer '%s'", lb.Name) service, err = r.client.Service.ActionFinishupgrade(service) if err != nil { @@ -62,7 +64,7 @@ func (r *Client) upgrade(lb *rancherClient.LoadBalancerService) error { } err = r.WaitService(service) if err != nil { - logrus.Warnf("Upgrade check: %v", err) + logrus.Warnf(err.Error()) } } @@ -72,7 +74,7 @@ func (r *Client) upgrade(lb *rancherClient.LoadBalancerService) error { func (r *Client) findLoadBalancerServicesByCert(certId string) ([]string, error) { var results []string - logrus.Debugf("Looking up load balancers matching certificate id %s", certId) + logrus.Debugf("Looking up load balancers matching certificate id '%s'", certId) balancers, err := r.client.LoadBalancerService.List(&rancherClient.ListOpts{ Filters: map[string]interface{}{ diff --git a/rancher/wait.go b/rancher/wait.go index eea0ba6..f3cf52f 100644 --- a/rancher/wait.go +++ b/rancher/wait.go @@ -34,7 +34,7 @@ func backoff(maxDuration time.Duration, timeoutMessage string, f func() (bool, e // WaitFor waits for a resource to reach a certain state. func (r *Client) WaitFor(resource *rancherClient.Resource, output interface{}, transitioning func() string) error { - return backoff(2*time.Minute, fmt.Sprintf("Time out waiting for %s:%s to transition", resource.Type, resource.Id), func() (bool, error) { + return backoff(2*time.Minute, fmt.Sprintf("Time out waiting for %s:%s to become active", resource.Type, resource.Id), func() (bool, error) { err := r.client.Reload(resource, output) if err != nil { return false, err