From 6a151865f7f3652409edce952420f66d2c25da3b Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 19 Nov 2024 22:04:00 +0800 Subject: [PATCH] feat: implement k8s secret `Deployer` --- .../providers/k8s-secret/k8s_secret.go | 158 ++++++++++++++++++ .../providers/k8s-secret/k8s_secret_test.go | 80 +++++++++ .../providers/local/{define.go => defines.go} | 0 .../providers/ssh/{define.go => defines.go} | 0 .../providers/webhook/webhook_test.go | 2 +- 5 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 internal/pkg/core/deployer/providers/k8s-secret/k8s_secret.go create mode 100644 internal/pkg/core/deployer/providers/k8s-secret/k8s_secret_test.go rename internal/pkg/core/deployer/providers/local/{define.go => defines.go} (100%) rename internal/pkg/core/deployer/providers/ssh/{define.go => defines.go} (100%) diff --git a/internal/pkg/core/deployer/providers/k8s-secret/k8s_secret.go b/internal/pkg/core/deployer/providers/k8s-secret/k8s_secret.go new file mode 100644 index 00000000..1e75b615 --- /dev/null +++ b/internal/pkg/core/deployer/providers/k8s-secret/k8s_secret.go @@ -0,0 +1,158 @@ +package k8ssecret + +import ( + "context" + "errors" + "strings" + + xerrors "github.com/pkg/errors" + k8sCore "k8s.io/api/core/v1" + k8sMeta "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + + "github.com/usual2970/certimate/internal/pkg/core/deployer" + "github.com/usual2970/certimate/internal/pkg/utils/x509" +) + +type K8sSecretDeployerConfig struct { + // kubeconfig 文件内容。 + KubeConfig string `json:"kubeConfig,omitempty"` + // K8s 命名空间。 + Namespace string `json:"namespace,omitempty"` + // K8s Secret 名称。 + SecretName string `json:"secretName"` + // K8s Secret 中用于存放证书的 Key。 + SecretDataKeyForCrt string `json:"secretDataKeyForCrt,omitempty"` + // K8s Secret 中用于存放私钥的 Key。 + SecretDataKeyForKey string `json:"secretDataKeyForKey,omitempty"` +} + +type K8sSecretDeployer struct { + config *K8sSecretDeployerConfig + logger deployer.Logger +} + +func New(config *K8sSecretDeployerConfig) (*K8sSecretDeployer, error) { + return NewWithLogger(config, deployer.NewNilLogger()) +} + +func NewWithLogger(config *K8sSecretDeployerConfig, logger deployer.Logger) (*K8sSecretDeployer, error) { + if config == nil { + return nil, errors.New("config is nil") + } + + if logger == nil { + return nil, errors.New("logger is nil") + } + + return &K8sSecretDeployer{ + logger: logger, + config: config, + }, nil +} + +func (d *K8sSecretDeployer) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) { + if d.config.Namespace == "" { + return nil, errors.New("config `namespace` is required") + } + if d.config.SecretName == "" { + return nil, errors.New("config `secretName` is required") + } + if d.config.SecretDataKeyForCrt == "" { + return nil, errors.New("config `secretDataKeyForCrt` is required") + } + if d.config.SecretDataKeyForKey == "" { + return nil, errors.New("config `secretDataKeyForKey` is required") + } + + certX509, err := x509.ParseCertificateFromPEM(certPem) + if err != nil { + return nil, err + } + + // 连接 + client, err := createK8sClient(d.config.KubeConfig) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create k8s client") + } + + var secretPayload *k8sCore.Secret + secretAnnotations := map[string]string{ + "certimate/common-name": certX509.Subject.CommonName, + "certimate/subject-sn": certX509.Subject.SerialNumber, + "certimate/subject-alt-names": strings.Join(certX509.DNSNames, ","), + "certimate/issuer-sn": certX509.Issuer.SerialNumber, + "certimate/issuer-org": strings.Join(certX509.Issuer.Organization, ","), + } + + // 获取 Secret 实例,如果不存在则创建 + secretPayload, err = client.CoreV1().Secrets(d.config.Namespace).Get(context.TODO(), d.config.SecretName, k8sMeta.GetOptions{}) + if err != nil { + secretPayload = &k8sCore.Secret{ + TypeMeta: k8sMeta.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: k8sMeta.ObjectMeta{ + Name: d.config.SecretName, + Annotations: secretAnnotations, + }, + Type: k8sCore.SecretType("kubernetes.io/tls"), + } + secretPayload.Data = make(map[string][]byte) + secretPayload.Data[d.config.SecretDataKeyForCrt] = []byte(certPem) + secretPayload.Data[d.config.SecretDataKeyForKey] = []byte(privkeyPem) + + _, err = client.CoreV1().Secrets(d.config.Namespace).Create(context.TODO(), secretPayload, k8sMeta.CreateOptions{}) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create k8s secret") + } else { + d.logger.Appendf("k8s secret created", secretPayload) + return &deployer.DeployResult{}, nil + } + } + + // 更新 Secret 实例 + secretPayload.Type = k8sCore.SecretType("kubernetes.io/tls") + if secretPayload.ObjectMeta.Annotations == nil { + secretPayload.ObjectMeta.Annotations = secretAnnotations + } else { + for k, v := range secretAnnotations { + secretPayload.ObjectMeta.Annotations[k] = v + } + } + secretPayload, err = client.CoreV1().Secrets(d.config.Namespace).Update(context.TODO(), secretPayload, k8sMeta.UpdateOptions{}) + if err != nil { + return nil, xerrors.Wrap(err, "failed to update k8s secret") + } + + d.logger.Appendf("k8s secret updated", secretPayload) + + return &deployer.DeployResult{}, nil +} + +func createK8sClient(kubeConfig string) (*kubernetes.Clientset, error) { + var config *rest.Config + var err error + if kubeConfig == "" { + config, err = rest.InClusterConfig() + } else { + kubeConfig, err := clientcmd.NewClientConfigFromBytes([]byte(kubeConfig)) + if err != nil { + return nil, err + } + config, err = kubeConfig.ClientConfig() + } + if err != nil { + return nil, err + } + + client, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/internal/pkg/core/deployer/providers/k8s-secret/k8s_secret_test.go b/internal/pkg/core/deployer/providers/k8s-secret/k8s_secret_test.go new file mode 100644 index 00000000..f6028307 --- /dev/null +++ b/internal/pkg/core/deployer/providers/k8s-secret/k8s_secret_test.go @@ -0,0 +1,80 @@ +package k8ssecret_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + dpK8sSecret "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/k8s-secret" +) + +var ( + fInputCertPath string + fInputKeyPath string + fNamespace string + fSecretName string + fSecretDataKeyForCrt string + fSecretDataKeyForKey string +) + +func init() { + argsPrefix := "CERTIMATE_DEPLOYER_K8SSECRET_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fNamespace, argsPrefix+"NAMESPACE", "default", "") + flag.StringVar(&fSecretName, argsPrefix+"SECRETNAME", "", "") + flag.StringVar(&fSecretDataKeyForCrt, argsPrefix+"SECRETDATAKEYFORCRT", "tls.crt", "") + flag.StringVar(&fSecretDataKeyForKey, argsPrefix+"SECRETDATAKEYFORKEY", "tls.key", "") +} + +/* +Shell command to run this test: + + go test -v webhook_test.go -args \ + --CERTIMATE_DEPLOYER_K8SSECRET_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_K8SSECRET_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_K8SSECRET_NAMESPACE="default" \ + --CERTIMATE_DEPLOYER_K8SSECRET_SECRETNAME="secret" \ + --CERTIMATE_DEPLOYER_K8SSECRET_SECRETDATAKEYFORCRT="tls.crt" \ + --CERTIMATE_DEPLOYER_K8SSECRET_SECRETDATAKEYFORKEY="tls.key" +*/ +func Test(t *testing.T) { + flag.Parse() + + t.Run("Deploy", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("NAMESPACE: %v", fNamespace), + fmt.Sprintf("SECRETNAME: %v", fSecretName), + fmt.Sprintf("SECRETDATAKEYFORCRT: %v", fSecretDataKeyForCrt), + fmt.Sprintf("SECRETDATAKEYFORKEY: %v", fSecretDataKeyForKey), + }, "\n")) + + deployer, err := dpK8sSecret.New(&dpK8sSecret.K8sSecretDeployerConfig{ + Namespace: fNamespace, + SecretName: fSecretName, + SecretDataKeyForCrt: fSecretDataKeyForCrt, + SecretDataKeyForKey: fSecretDataKeyForKey, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + t.Logf("ok: %v", res) + }) +} diff --git a/internal/pkg/core/deployer/providers/local/define.go b/internal/pkg/core/deployer/providers/local/defines.go similarity index 100% rename from internal/pkg/core/deployer/providers/local/define.go rename to internal/pkg/core/deployer/providers/local/defines.go diff --git a/internal/pkg/core/deployer/providers/ssh/define.go b/internal/pkg/core/deployer/providers/ssh/defines.go similarity index 100% rename from internal/pkg/core/deployer/providers/ssh/define.go rename to internal/pkg/core/deployer/providers/ssh/defines.go diff --git a/internal/pkg/core/deployer/providers/webhook/webhook_test.go b/internal/pkg/core/deployer/providers/webhook/webhook_test.go index 8c2cbde8..d30f9599 100644 --- a/internal/pkg/core/deployer/providers/webhook/webhook_test.go +++ b/internal/pkg/core/deployer/providers/webhook/webhook_test.go @@ -36,7 +36,7 @@ Shell command to run this test: func Test(t *testing.T) { flag.Parse() - t.Run("Notify", func(t *testing.T) { + t.Run("Deploy", func(t *testing.T) { t.Log(strings.Join([]string{ "args:", fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),