config下的yml
apiVersion: mygroup.my.domain/v1
kind: MyApp
metadata:
labels:
app.kubernetes.io/name: kubebuild
app.kubernetes.io/managed-by: kustomize
name: myapp-sample
spec:
image: nginx:1.25 #镜像
replicas: 3 #副本数
port: 80 #services的端口
types
/*
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.
// MyAppSpec defines the desired state of MyApp.
type MyAppSpec struct {
// Nginx 镜像名
Image string `json:"image,omitempty"`
// 副本数
Replicas *int32 `json:"replicas,omitempty"`
// 暴露端口
Port int32 `json:"port,omitempty"`
}
type MyAppCondition struct {
// 类型,如 Available、Progressing、Degraded
Type string `json:"type"`
// 状态,True/False/Unknown
Status string `json:"status"`
// 原因
Reason string `json:"reason,omitempty"`
// 详细信息
Message string `json:"message,omitempty"`
// 最后变更时间
LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"`
}
// MyAppStatus defines the observed state of MyApp.
type MyAppStatus struct {
// 当前阶段,如 Pending、Running、Failed
Phase string `json:"phase,omitempty"`
// 详细状态条件
Conditions []MyAppCondition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// MyApp is the Schema for the myapps API.
type MyApp struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec MyAppSpec `json:"spec,omitempty"`
Status MyAppStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// MyAppList contains a list of MyApp.
type MyAppList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []MyApp `json:"items"`
}
func init() {
SchemeBuilder.Register(&MyApp{}, &MyAppList{})
}
controller
/*
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"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
intstr "k8s.io/apimachinery/pkg/util/intstr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
mygroupv1 "kubebuild/api/v1"
)
// MyAppReconciler reconciles a MyApp object
type MyAppReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=mygroup.my.domain,resources=myapps,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=mygroup.my.domain,resources=myapps/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=mygroup.my.domain,resources=myapps/finalizers,verbs=update
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the MyApp object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/reconcile
func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := logf.FromContext(ctx)
// 1. 获取 MyApp 实例
var myapp mygroupv1.MyApp
if err := r.Get(ctx, req.NamespacedName, &myapp); err != nil {
if errors.IsNotFound(err) {
// 资源已删除,无需处理
return ctrl.Result{}, nil
}
return ctrl.Result{}, err
}
// 2. 构建期望的 Deployment
replicas := int32(1)
if myapp.Spec.Replicas != nil {
replicas = *myapp.Spec.Replicas
}
labels := map[string]string{
"app": myapp.Name,
}
deploy := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: myapp.Name + "-nginx",
Namespace: myapp.Namespace,
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(&myapp, myapp.GroupVersionKind()),
},
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{
MatchLabels: labels,
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "nginx",
Image: myapp.Spec.Image,
Ports: []corev1.ContainerPort{
{
ContainerPort: myapp.Spec.Port,
},
},
},
},
},
},
},
}
// 3. 检查并创建/更新 Deployment
var found appsv1.Deployment
err := r.Get(ctx, client.ObjectKey{Name: deploy.Name, Namespace: deploy.Namespace}, &found)
if err != nil && errors.IsNotFound(err) {
if err := r.Create(ctx, deploy); err != nil {
log.Error(err, "创建 Deployment 失败")
return ctrl.Result{}, err
}
} else if err == nil {
// 若参数有变更则更新
needUpdate := false
if *found.Spec.Replicas != replicas ||
found.Spec.Template.Spec.Containers[0].Image != myapp.Spec.Image ||
len(found.Spec.Template.Spec.Containers[0].Ports) == 0 ||
found.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort != myapp.Spec.Port {
found.Spec.Replicas = &replicas
found.Spec.Template.Spec.Containers[0].Image = myapp.Spec.Image
found.Spec.Template.Spec.Containers[0].Ports = []corev1.ContainerPort{
{ContainerPort: myapp.Spec.Port},
}
needUpdate = true
}
if needUpdate {
if err := r.Update(ctx, &found); err != nil {
log.Error(err, "更新 Deployment 失败")
return ctrl.Result{}, err
}
}
} else {
log.Error(err, "获取 Deployment 失败")
return ctrl.Result{}, err
}
// 4. 构建期望的 Service
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: myapp.Name + "-nginx",
Namespace: myapp.Namespace,
OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(&myapp, myapp.GroupVersionKind()),
},
},
Spec: corev1.ServiceSpec{
Selector: labels,
Ports: []corev1.ServicePort{
{
Port: myapp.Spec.Port,
TargetPort: intstrFromInt32(myapp.Spec.Port),
},
},
Type: corev1.ServiceTypeClusterIP,
},
}
// 5. 检查并创建/更新 Service
var foundSvc corev1.Service
err = r.Get(ctx, client.ObjectKey{Name: svc.Name, Namespace: svc.Namespace}, &foundSvc)
if err != nil && errors.IsNotFound(err) {
if err := r.Create(ctx, svc); err != nil {
log.Error(err, "创建 Service 失败")
return ctrl.Result{}, err
}
} else if err == nil {
// 若端口有变更则更新
needUpdate := false
if len(foundSvc.Spec.Ports) == 0 ||
foundSvc.Spec.Ports[0].Port != myapp.Spec.Port {
foundSvc.Spec.Ports = []corev1.ServicePort{
{
Port: myapp.Spec.Port,
TargetPort: intstrFromInt32(myapp.Spec.Port),
},
}
needUpdate = true
}
if needUpdate {
if err := r.Update(ctx, &foundSvc); err != nil {
log.Error(err, "更新 Service 失败")
return ctrl.Result{}, err
}
}
} else {
log.Error(err, "获取 Service 失败")
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
// intstrFromInt32 用于将 int32 转为 IntOrString
func intstrFromInt32(i int32) intstr.IntOrString {
return intstr.IntOrString{Type: intstr.Int, IntVal: i}
}
// SetupWithManager sets up the controller with the Manager.
func (r *MyAppReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&mygroupv1.MyApp{}).
Named("myapp").
Complete(r)
}