mirror of
https://github.com/alibaba/higress.git
synced 2026-02-21 14:30:54 +08:00
312 lines
8.5 KiB
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)
|
|
}
|