背景
在 Kubernetes 中,
Secrets是被隔离在各自的 Namespace 范围内的资源。默认情况下,一个Secret只能被创建它的 Namespace 内的 Pod 或 Deployment 所引用,其他 Namespace 中的应用无法直接访问。
一、安装依赖工具
# 安装 Go(1.24.1)
https://studygolang.com/dl/golang/go1.24.1.linux-amd64.tar.gz
# 删除旧版 Go 源码和二进制文件
sudo rm -rf /usr/local/go
sudo rm -rf /root/go
# 下载官方 Go 1.24.1
wget https://go.dev/dl/go1.24.1.linux-amd64.tar.gz
# 安装 Go 到标准路径
sudo tar -C /usr/local -xzf go1.24.1.linux-amd64.tar.gz
# 设置环境变量(写入 .bashrc 或 .zshrc)
export GOROOT=/usr/local/go
export PATH=$GOROOT/bin:$PATH
export GOPROXY=https://goproxy.io,direct
# 验证安装
go version
# 输出:go version go1.24.1 linux/amd64
# 安装 Kubebuilder CLI (kubebuilder 4.6)
wget https://github.com/kubernetes-sigs/kubebuilder/releases/download/v4.6.0/kubebuilder_linux_amd64
chmod +x kubebuilder && mv kubebuilder /usr/local/bin/
# 安装 Kustomize(make deploy 会用到)
go install sigs.k8s.io/kustomize/kustomize/v4@latest
# 安装 Docker 与 kubectl(略)
初始化 Kubebuilder 项目
二、创建项目模板
mkdir secretsync-controller && cd secretsync-controller
kubebuilder init --domain stangj.com --repo github.com/stangj/secretsync-controller
将自动创建项目结构,包含 Go 模块、Makefile 等文件。
三、创建 CRD API:SecretSync
步骤 3:生成 API 模板
kubebuilder create api --group sync --version v1 --kind Secretsync
步骤 4:编辑 CRD 结构(api/v1/secretsync_types.go)
/*
Copyright 2025.
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 v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
// SecretsyncSpec defines the desired state of Secretsync.
type SecretsyncSpec struct {
// 源命名空间
SourceNamespace string `json:"sourceNamespace"`
// 源 Secret 名称
SourceSecretName string `json:"sourceSecretName"`
// 目标命名空间选择器:支持 Labels 动态选择
TargetNamespaceSelector *metav1.LabelSelector `json:"targetNamespaceSelector,omitempty"`
// 目标 Secret 名称(可选,默认与源同名)
TargetSecretName string `json:"targetSecretName,omitempty"`
}
// SecretsyncStatus defines the observed state of Secretsync.
type SecretsyncStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
// 已同步命名空间
SyncedNamespaces []string `json:"syncedNamespaces,omitempty"`
// 同步失败命名空间
FailedNamespaces []string `json:"failedNamespaces,omitempty"`
// 最后同步时间
LastSyncTime *metav1.Time `json:"lastSyncTime,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// Secretsync is the Schema for the secretsyncs API.
type Secretsync struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec SecretsyncSpec `json:"spec,omitempty"`
Status SecretsyncStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// SecretsyncList contains a list of Secretsync.
type SecretsyncList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Secretsync `json:"items"`
}
func init() {
SchemeBuilder.Register(&Secretsync{}, &SecretsyncList{})
}
重点字段说明:
-
TargetNamespaceSelector:使用标签选择器动态选择命名空间 -
TargetSecretName:支持定义不同的目标 Secret 名称 -
Status部分提供状态反馈
四、构建控制器核心逻辑
步骤 4:查看控制器模板
cat internal/controller/secretsync_controller.go
步骤 6:实现控制器逻辑(controllers/secretsync_controller.go)
/*
Copyright 2025.
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 controller
import (
"context"
"fmt"
"reflect"
"time"
"github.com/go-logr/logr"
"github.com/prometheus/client_golang/prometheus"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
syncv1 "github.com/stangj/secretsync-controller/api/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type SecretsyncReconciler struct {
client.Client
Scheme *runtime.Scheme
Log logr.Logger
}
// 以下是控制器所需的 RBAC 权限注解
// +kubebuilder:rbac:groups=sync.example.com,resources=secretsyncs,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=sync.example.com,resources=secretsyncs/status,verbs=get;update;patch
// +kubebuilder:rbac:groups="",resources=namespaces,verbs=list;watch
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;patch;delete
var (
syncTotalCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "secretsync_total",
Help: "Total number of sync operations",
},
[]string{"result"}, // 标签,用于区分成功/失败结果
)
syncLatencySeconds = prometheus.NewHistogram(
prometheus.HistogramOpts{
Name: "secretsync_latency_seconds",
Help: "Time taken for sync",
},
)
lastSuccessTimeGauge = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "secretsync_last_success_time",
Help: "Timestamp of last successful sync",
},
)
)
func init() {
prometheus.MustRegister(syncTotalCounter, syncLatencySeconds, lastSuccessTimeGauge)
}
// Reconcile 是控制器的核心方法,实现了 controller-runtime 的 Reconciler 接口
// 当 Secretsync 资源或相关资源发生变化时被调用
func (r *SecretsyncReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
start := time.Now()
log := r.Log.WithValues("secretsync", req.NamespacedName)
var syncObj syncv1.Secretsync
if err := r.Get(ctx, req.NamespacedName, &syncObj); err != nil {
if errors.IsNotFound(err) {
log.Info("SecretSync CR not found, skip reconciliation")
syncTotalCounter.WithLabelValues("failure").Inc()
return ctrl.Result{}, nil
}
log.Error(err, "Failed to get SecretSync CR")
syncTotalCounter.WithLabelValues("failure").Inc()
return ctrl.Result{}, err
}
if syncObj.Spec.SourceNamespace == "" || syncObj.Spec.SourceSecretName == "" {
log.Error(nil, "Invalid spec: SourceNamespace or SourceSecretName missing", "spec", syncObj.Spec)
syncObj.Status.LastSyncTime = &metav1.Time{Time: time.Now()}
_ = r.Status().Update(ctx, &syncObj)
syncTotalCounter.WithLabelValues("failure").Inc()
return ctrl.Result{}, nil
}
srcNamespaceName := types.NamespacedName{
Namespace: syncObj.Spec.SourceNamespace,
Name: syncObj.Spec.SourceSecretName,
}
var srcSecret corev1.Secret
if err := r.Get(ctx, srcNamespaceName, &srcSecret); err != nil {
log.Error(err, "Failed to get source Secret", "secret", srcNamespaceName)
syncTotalCounter.WithLabelValues("failure").Inc()
return ctrl.Result{}, err
}
namespaces, err := r.getMatchingNamespaces(ctx, syncObj.Spec.TargetNamespaceSelector)
if err != nil {
log.Error(err, "Failed to list matched namespaces")
syncTotalCounter.WithLabelValues("failure").Inc()
return ctrl.Result{}, err
}
// 确定目标 Secret 的名称
targetSecretName := syncObj.Spec.TargetSecretName
if targetSecretName == "" {
targetSecretName = srcSecret.Name
}
var synced []string // 成功同步的命名空间
var failed []string // 失败的命名空间
// 遍历所有目标命名空间,执行同步
for _, ns := range namespaces {
if err := r.syncSecret(ctx, &srcSecret, ns, targetSecretName, &syncObj); err != nil {
log.Error(err, "Failed to sync secret to namespace", "namespace", ns)
failed = append(failed, ns)
} else {
// 同步成功,记录成功的命名空间
synced = append(synced, ns)
}
}
// 更新 Secretsync 资源的状态
now := metav1.Now()
syncObj.Status.SyncedNamespaces = synced
syncObj.Status.FailedNamespaces = failed
syncObj.Status.LastSyncTime = &now
if err := r.Status().Update(ctx, &syncObj); err != nil {
log.Error(err, "Failed to update SecretSync status")
// 状态更新失败不会影响同步操作的返回结果
}
if len(synced) > 0 && len(failed) == 0 {
syncTotalCounter.WithLabelValues("success").Inc()
lastSuccessTimeGauge.Set(float64(now.Unix()))
} else if len(failed) > 0 {
syncTotalCounter.WithLabelValues("failure").Inc()
} else {
syncTotalCounter.WithLabelValues("partial_success").Inc()
}
latency := time.Since(start).Seconds()
syncLatencySeconds.Observe(latency)
if len(failed) > 0 {
return ctrl.Result{}, fmt.Errorf("some target namespaces failed to sync")
}
return ctrl.Result{}, nil
}
// syncSecret 将单个源 Secret 同步到目标命名空间中
// 参数:
// - ctx: 上下文,用于API通信
// - src: 源 Secret 对象
// - namespace: 目标命名空间
// - targetSecretName: 在目标命名空间中创建的 Secret 名称
// - syncObj: Secretsync 对象,用于设置所有者引用
func (r *SecretsyncReconciler) syncSecret(
ctx context.Context,
src *corev1.Secret,
namespace, targetSecretName string,
syncObj *syncv1.Secretsync,
) error {
// 创建目标 Secret 对象
target := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: targetSecretName,
Namespace: namespace,
},
Data: src.Data, // 复制源 Secret 的数据
Type: src.Type, // 复制源 Secret 的类型
}
controllerutil.SetControllerReference(syncObj, target, r.Scheme)
var existing corev1.Secret
existingKey := types.NamespacedName{Namespace: namespace, Name: targetSecretName}
err := r.Get(ctx, existingKey, &existing)
if err != nil {
if errors.IsNotFound(err) {
return r.Create(ctx, target)
}
return err
}
if !reflect.DeepEqual(existing.Data, target.Data) || existing.Type != target.Type {
existing.Data = target.Data
existing.Type = target.Type
return r.Update(ctx, &existing)
}
return nil
}
// getMatchingNamespaces 根据标签选择器获取匹配的命名空间列表
// 参数:
// - ctx: 上下文,用于API通信
// - selector: Kubernetes 标签选择器
// 返回:
// - 匹配的命名空间名称列表
// - 错误(如果有)
func (r *SecretsyncReconciler) getMatchingNamespaces(
ctx context.Context,
selector *metav1.LabelSelector,
) ([]string, error) {
if selector == nil {
return nil, nil
}
selectorLabels, err := metav1.LabelSelectorAsSelector(selector)
if err != nil {
return nil, err
}
var nsList corev1.NamespaceList
if err := r.List(ctx, &nsList, client.MatchingLabelsSelector{Selector: selectorLabels}); err != nil {
return nil, err
}
var namespaces []string
for _, ns := range nsList.Items {
namespaces = append(namespaces, ns.Name)
}
return namespaces, nil
}
// enqueueSecrets 是一个 MapFunc,当监视的 Secret 发生变化时
func (r *SecretsyncReconciler) enqueueSecrets(_ context.Context, obj client.Object) []reconcile.Request {
ctx := context.TODO()
// 转换为 Secret 对象
secret := obj.(*corev1.Secret)
var list syncv1.SecretsyncList
if err := r.List(ctx, &list, &client.ListOptions{}); err != nil {
r.Log.Error(err, "Failed to list SecretSync CRs")
return nil
}
var requests []reconcile.Request
for _, item := range list.Items {
if item.Spec.SourceNamespace == secret.Namespace && item.Spec.SourceSecretName == secret.Name {
requests = append(requests, reconcile.Request{
NamespacedName: client.ObjectKeyFromObject(&item),
})
}
}
return requests
}
// enqueueNamespaces 是一个 MapFunc,当监视的 Namespace 发生变化时
func (r *SecretsyncReconciler) enqueueNamespaces(_ context.Context, obj client.Object) []reconcile.Request {
ctx := context.TODO()
// 转换为 Namespace 对象
ns := obj.(*corev1.Namespace)
var list syncv1.SecretsyncList
if err := r.List(ctx, &list, &client.ListOptions{}); err != nil {
r.Log.Error(err, "Failed to list SecretSync CRs")
return nil
}
var requests []reconcile.Request
for _, item := range list.Items {
// 跳过没有目标命名空间选择器的 Secretsync
if item.Spec.TargetNamespaceSelector == nil {
continue
}
sel, _ := metav1.LabelSelectorAsSelector(item.Spec.TargetNamespaceSelector)
if sel.Matches(labels.Set(ns.Labels)) {
requests = append(requests, reconcile.Request{
NamespacedName: client.ObjectKeyFromObject(&item),
})
}
}
return requests
}
// SetupWithManager 设置控制器与管理器的关联
func (r *SecretsyncReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&syncv1.Secretsync{}).
Watches(
&corev1.Secret{},
handler.EnqueueRequestsFromMapFunc(r.enqueueSecrets),
).
Watches(
&corev1.Namespace{},
handler.EnqueueRequestsFromMapFunc(r.enqueueNamespaces),
).
Complete(r)
}
测试部署到K8S集群
生成本地yaml并部署到k8s集群
# make manifests
# make install
# kubectl get crd | grep stangj
secretsyncs.sync.stangj.com 2025-05-25T08:11:13Z
本地启动controller
# go run cmd/main.go
2025-05-25T17:37:20+08:00 INFO setup starting manager
2025-05-25T17:37:20+08:00 INFO starting server {"name": "health probe", "addr": "[::]:8081"}
2025-05-25T17:37:20+08:00 INFO Starting EventSource {"controller": "secretsync", "controllerGroup": "sync.stangj.com", "controllerKind": "Secretsync", "source": "kind source: *v1.Namespace"}
2025-05-25T17:37:20+08:00 INFO Starting EventSource {"controller": "secretsync", "controllerGroup": "sync.stangj.com", "controllerKind": "Secretsync", "source": "kind source: *v1.Secretsync"}
2025-05-25T17:37:20+08:00 INFO Starting EventSource {"controller": "secretsync", "controllerGroup": "sync.stangj.com", "controllerKind": "Secretsync", "source": "kind source: *v1.Secret"}
2025-05-25T17:37:20+08:00 INFO Starting Controller {"controller": "secretsync", "controllerGroup": "sync.stangj.com", "controllerKind": "Secretsync"}
2025-05-25T17:37:20+08:00 INFO Starting workers {"controller": "secretsync", "controllerGroup": "sync.stangj.com", "controllerKind": "Secretsync", "worker count": 1}
....
编写SecretSync进行同步
# kubectl create ns cert-sync
# cat sync-Secret.yaml
apiVersion: v1
kind: Secret
metadata:
namespace: cert-sync
name: tls
stringData:
tls.crt: "FAKE-CERT1"
tls.key: "FAKE-KEY2"
type: kubernetes.io/tls
# kubectl apply -f sync-Secret.yaml
# kubectl get secrets -n cert-sync -oyaml
apiVersion: v1
items:
- apiVersion: v1
data:
tls.crt: RkFLRS1DRVJUMQ==
tls.key: RkFLRS1LRVky
kind: Secret
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"v1","kind":"Secret","metadata":{"annotations":{},"name":"tls","namespace":"cert-sync"},"stringData":{"tls.crt":"FAKE-CERT1","tls.key":"FAKE-KEY2"},"type":"kubernetes.io/tls"}
creationTimestamp: "2025-05-25T09:40:02Z"
name: tls
namespace: cert-sync
resourceVersion: "24453"
uid: d0708bf8-16c7-4de2-82d0-44da794f1a9e
type: kubernetes.io/tls
kind: List
metadata:
resourceVersion: ""
创建数据同步
创建要同步的名称空间
# kubectl create ns dest1-sync
# kubectl create ns dest2-sync
创建yaml指定同步的名称空间
# vim sync-dest1.yaml
apiVersion: sync.stangj.com/v1
kind: Secretsync
metadata:
name: sync-dest1
spec:
sourceNamespace: cert-sync
sourceSecretName: tls
targetNamespaceSelector:
matchLabels:
secret-sync: "enabled"
# kubectl apply -f sync-dest1.yaml
# kubectl get secretsync
NAME AGE
sync-dest1 33s
给要同步的名称空间打标签 secret-sync: "enabled
# kubectl label namespaces dest1-sync secret-sync=enabled
# kubectl get namespaces dest1-sync --show-labels
NAME STATUS AGE LABELS
dest1-sync Active 9m1s kubernetes.io/metadata.name=dest1-sync,secret-sync=enabled
查看打上标签的名称空间有没有同步secret
# kubectl get secrets -n dest1-sync
NAME TYPE DATA AGE
tls kubernetes.io/tls 2 2m45s
# kubectl get secrets -n dest1-sync -oyaml
apiVersion: v1
items:
- apiVersion: v1
data:
tls.crt: RkFLRS1DRVJUMQ==
tls.key: RkFLRS1LRVky
kind: Secret
metadata:
creationTimestamp: "2025-05-25T09:50:52Z"
name: tls
namespace: dest1-sync
resourceVersion: "25461"
uid: 5abad270-62fa-40a4-afc9-e64003bd8b5b
type: kubernetes.io/tls
kind: List
metadata:
resourceVersion: ""
验证修改数据是否可以同步
# cat sync-Secret.yaml
apiVersion: v1
kind: Secret
metadata:
namespace: cert-sync
name: tls
stringData:
tls.crt: "FAKE-CERT2"
tls.key: "FAKE-KEY3"
type: kubernetes.io/tls
# kubectl apply -f sync-Secret.yaml
# kubectl get secrets -n cert-sync -oyaml
apiVersion: v1
items:
- apiVersion: v1
data:
tls.crt: RkFLRS1DRVJUMg==
tls.key: RkFLRS1LRVkz
kind: Secret
metadata:
annotations:
kubectl.kubernetes.io/last-applied-configuration: |
{"apiVersion":"v1","kind":"Secret","metadata":{"annotations":{},"name":"tls","namespace":"cert-sync"},"stringData":{"tls.crt":"FAKE-CERT2","tls.key":"FAKE-KEY3"},"type":"kubernetes.io/tls"}
creationTimestamp: "2025-05-25T09:40:02Z"
name: tls
namespace: cert-sync
resourceVersion: "26030"
uid: d0708bf8-16c7-4de2-82d0-44da794f1a9e
type: kubernetes.io/tls
kind: List
metadata:
resourceVersion: ""
验证同步的名称空间是否已经同步
# kubectl get secrets -n dest1-sync -oyaml
apiVersion: v1
items:
- apiVersion: v1
data:
tls.crt: RkFLRS1DRVJUMg==
tls.key: RkFLRS1LRVkz
kind: Secret
metadata:
creationTimestamp: "2025-05-25T09:50:52Z"
name: tls
namespace: dest1-sync
resourceVersion: "26031"
uid: 5abad270-62fa-40a4-afc9-e64003bd8b5b
type: kubernetes.io/tls
kind: List
metadata:
resourceVersion: ""
到这里通过标签的方法已经实现了
kubectl delele -f sync-dest1.yaml 就不会在同步了 但是目的名称空间的Secret并没有删除
文章评论