Files
higress/pkg/cert/config.go

312 lines
8.5 KiB
Go

// Copyright (c) 2022 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cert
import (
"context"
"fmt"
"strings"
"sync/atomic"
"time"
"istio.io/istio/pkg/config/host"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"sigs.k8s.io/yaml"
)
const (
ConfigmapCertName = "higress-https"
ConfigmapCertConfigKey = "cert"
DefaultRenewBeforeDays = 30
RenewMaxDays = 90
)
type IssuerName string
const (
IssuerTypeAliyunSSL IssuerName = "aliyunssl"
IssuerTypeLetsencrypt IssuerName = "letsencrypt"
)
// Config is the configuration of automatic https.
type Config struct {
AutomaticHttps bool `json:"automaticHttps"`
FallbackForInvalidSecret bool `json:"fallbackForInvalidSecret"`
RenewBeforeDays int `json:"renewBeforeDays"`
CredentialConfig []CredentialEntry `json:"credentialConfig"`
ACMEIssuer []ACMEIssuerEntry `json:"acmeIssuer"`
Version string `json:"version"`
}
func (c *Config) GetIssuer(issuerName IssuerName) *ACMEIssuerEntry {
for _, issuer := range c.ACMEIssuer {
if issuer.Name == issuerName {
return &issuer
}
}
return nil
}
func (c *Config) MatchSecretNameByDomain(domain string) string {
for _, credential := range c.CredentialConfig {
for _, credDomain := range credential.Domains {
if host.Name(strings.ToLower(domain)).SubsetOf(host.Name(strings.ToLower(credDomain))) {
return credential.TLSSecret
}
}
}
return ""
}
func (c *Config) GetSecretNameByDomain(issuerName IssuerName, domain string) string {
for _, credential := range c.CredentialConfig {
if credential.TLSIssuer == issuerName {
for _, credDomain := range credential.Domains {
if host.Name(strings.ToLower(domain)).SubsetOf(host.Name(strings.ToLower(credDomain))) {
return credential.TLSSecret
}
}
}
}
return ""
}
func ParseTLSSecret(tlsSecret string) (string, string) {
secrets := strings.Split(tlsSecret, "/")
switch len(secrets) {
case 1:
return "", tlsSecret
case 2:
return secrets[0], secrets[1]
}
return "", ""
}
func (c *Config) Validate() error {
// check acmeIssuer
if c.AutomaticHttps {
if len(c.ACMEIssuer) == 0 {
return fmt.Errorf("no acmeIssuer configuration found when automaticHttps is enable")
}
for _, issuer := range c.ACMEIssuer {
switch issuer.Name {
case IssuerTypeLetsencrypt:
if issuer.Email == "" {
return fmt.Errorf("acmeIssuer %s email is empty", issuer.Name)
}
if !ValidateEmail(issuer.Email) {
return fmt.Errorf("acmeIssuer %s email %s is invalid", issuer.Name, issuer.Email)
}
default:
return fmt.Errorf("acmeIssuer name %s is not supported", issuer.Name)
}
}
}
// check credentialConfig
for _, credential := range c.CredentialConfig {
if len(credential.Domains) == 0 {
return fmt.Errorf("credentialConfig domains is empty")
}
if credential.TLSSecret == "" {
return fmt.Errorf("credentialConfig tlsSecret is empty")
} else {
ns, secret := ParseTLSSecret(credential.TLSSecret)
if ns == "" && secret == "" {
return fmt.Errorf("credentialConfig tlsSecret %s is not supported", credential.TLSSecret)
}
}
if credential.TLSIssuer == IssuerTypeLetsencrypt {
if len(credential.Domains) > 1 {
return fmt.Errorf("credentialConfig tlsIssuer %s only support one domain", credential.TLSIssuer)
}
}
if credential.TLSIssuer != IssuerTypeLetsencrypt && len(credential.TLSIssuer) > 0 {
return fmt.Errorf("credential tls issuer %s is not supported", credential.TLSIssuer)
}
}
if c.RenewBeforeDays <= 0 {
return fmt.Errorf("RenewBeforeDays should be large than zero")
}
if c.RenewBeforeDays >= RenewMaxDays {
return fmt.Errorf("RenewBeforeDays should be less than %d", RenewMaxDays)
}
return nil
}
type CredentialEntry struct {
Domains []string `json:"domains"`
TLSIssuer IssuerName `json:"tlsIssuer,omitempty"`
TLSSecret string `json:"tlsSecret,omitempty"`
CACertSecret string `json:"cacertSecret,omitempty"`
}
type ACMEIssuerEntry struct {
Name IssuerName `json:"name"`
Email string `json:"email"`
AK string `json:"ak"` // Only applicable for certain issuers like 'aliyunssl'
SK string `json:"sk"` // Only applicable for certain issuers like 'aliyunssl'
}
type ConfigMgr struct {
client kubernetes.Interface
config atomic.Value
namespace string
}
func (c *ConfigMgr) SetConfig(config *Config) {
c.config.Store(config)
}
func (c *ConfigMgr) GetConfig() *Config {
value := c.config.Load()
if value != nil {
if config, ok := value.(*Config); ok {
return config
}
}
return nil
}
func (c *ConfigMgr) InitConfig(email string) (*Config, error) {
var defaultConfig *Config
cm, err := c.GetConfigmap()
if err != nil {
if errors.IsNotFound(err) {
if len(strings.TrimSpace(email)) == 0 {
email = getRandEmail()
}
defaultConfig = newDefaultConfig(email)
err2 := c.ApplyConfigmap(defaultConfig)
if err2 != nil {
return nil, err2
}
}
return nil, err
} else {
defaultConfig, err = c.ParseConfigFromConfigmap(cm)
if err != nil {
return nil, err
}
}
return defaultConfig, nil
}
func (c *ConfigMgr) ParseConfigFromConfigmap(configmap *v1.ConfigMap) (*Config, error) {
if _, ok := configmap.Data[ConfigmapCertConfigKey]; !ok {
return nil, fmt.Errorf("no cert key %s in configmap %s", ConfigmapCertConfigKey, configmap.Name)
}
config := newDefaultConfig("")
if err := yaml.Unmarshal([]byte(configmap.Data[ConfigmapCertConfigKey]), config); err != nil {
return nil, fmt.Errorf("data:%s, convert to higress config error, error: %+v", configmap.Data[ConfigmapCertConfigKey], err)
}
// validate config
if err := config.Validate(); err != nil {
return nil, err
}
return config, nil
}
func (c *ConfigMgr) GetConfigFromConfigmap() (*Config, error) {
var config *Config
cm, err := c.GetConfigmap()
if err != nil {
return nil, err
} else {
config, err = c.ParseConfigFromConfigmap(cm)
if err != nil {
return nil, err
}
}
return config, nil
}
func (c *ConfigMgr) GetConfigmap() (configmap *v1.ConfigMap, err error) {
configmapName := ConfigmapCertName
cm, err := c.client.CoreV1().ConfigMaps(c.namespace).Get(context.Background(), configmapName, metav1.GetOptions{})
return cm, err
}
func (c *ConfigMgr) ApplyConfigmap(config *Config) error {
configmapName := ConfigmapCertName
cm := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: c.namespace,
Name: configmapName,
},
}
bytes, err := yaml.Marshal(config)
if err != nil {
return err
}
cm.Data = make(map[string]string, 0)
cm.Data[ConfigmapCertConfigKey] = string(bytes)
_, err = c.client.CoreV1().ConfigMaps(c.namespace).Get(context.Background(), configmapName, metav1.GetOptions{})
if err != nil {
if errors.IsNotFound(err) {
if _, err = c.client.CoreV1().ConfigMaps(c.namespace).Create(context.Background(), cm, metav1.CreateOptions{}); err != nil {
return err
}
} else {
return err
}
} else {
if _, err = c.client.CoreV1().ConfigMaps(c.namespace).Update(context.Background(), cm, metav1.UpdateOptions{}); err != nil {
return err
}
}
return nil
}
func NewConfigMgr(namespace string, client kubernetes.Interface) (*ConfigMgr, error) {
configMgr := &ConfigMgr{
client: client,
namespace: namespace,
}
return configMgr, nil
}
func newDefaultConfig(email string) *Config {
defaultIssuer := []ACMEIssuerEntry{
{
Name: IssuerTypeLetsencrypt,
Email: email,
},
}
defaultCredentialConfig := make([]CredentialEntry, 0)
config := &Config{
AutomaticHttps: true,
FallbackForInvalidSecret: false,
RenewBeforeDays: DefaultRenewBeforeDays,
ACMEIssuer: defaultIssuer,
CredentialConfig: defaultCredentialConfig,
Version: time.Now().Format("20060102030405"),
}
return config
}
func getRandEmail() string {
num1 := rangeRandom(100, 100000)
num2 := rangeRandom(100, 100000)
return fmt.Sprintf("your%d@yours%d.com", num1, num2)
}