Refactor into RenderAgent and ApplyAgent

This commit is contained in:
2026-04-29 16:41:40 +08:00
parent 6d290a97ae
commit e1959bee6d
21 changed files with 1501 additions and 697 deletions

View File

@@ -0,0 +1,284 @@
package render
import (
"fmt"
"strings"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
monov1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
buildinfo "example.com/monok8s/pkg/buildinfo"
)
type AgentConf struct {
Namespace string
Image string
ImagePullSecrets []string
Labels map[string]string
}
func RenderAgentDaemonSets(conf AgentConf) (string, error) {
objs, err := buildAgentDaemonSetObjects(conf)
if err != nil {
return "", err
}
return renderObjects(objs)
}
func buildAgentDaemonSetObjects(conf AgentConf) ([]runtime.Object, error) {
if strings.TrimSpace(conf.Namespace) == "" {
return nil, fmt.Errorf("namespace is required")
}
conf.Labels = map[string]string{
"app.kubernetes.io/name": monov1alpha1.NodeAgentName,
"app.kubernetes.io/component": "agent",
"app.kubernetes.io/part-of": "monok8s",
"app.kubernetes.io/managed-by": monov1alpha1.NodeControlName,
}
return []runtime.Object{
buildAgentServiceAccount(conf),
buildAgentClusterRole(conf),
buildAgentClusterRoleBinding(conf),
buildAgentDaemonSet(conf),
}, nil
}
func buildAgentNamespace(conf AgentConf) *corev1.Namespace {
return &corev1.Namespace{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Namespace",
},
ObjectMeta: metav1.ObjectMeta{
Name: conf.Namespace,
Labels: copyStringMap(conf.Labels),
},
}
}
func buildAgentServiceAccount(conf AgentConf) *corev1.ServiceAccount {
return &corev1.ServiceAccount{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "ServiceAccount",
},
ObjectMeta: metav1.ObjectMeta{
Name: monov1alpha1.NodeAgentName,
Namespace: conf.Namespace,
Labels: copyStringMap(conf.Labels),
},
}
}
func buildAgentClusterRole(conf AgentConf) *rbacv1.ClusterRole {
wantRules := []rbacv1.PolicyRule{
{
APIGroups: []string{monov1alpha1.Group},
Resources: []string{"osupgrades"},
Verbs: []string{"get"},
},
{
APIGroups: []string{monov1alpha1.Group},
Resources: []string{"osupgradeprogresses"},
Verbs: []string{"get", "list", "watch", "create", "patch", "update"},
},
{
APIGroups: []string{monov1alpha1.Group},
Resources: []string{"osupgradeprogresses/status"},
Verbs: []string{"get", "list", "watch", "create", "patch", "update"},
},
{
APIGroups: []string{""},
Resources: []string{"nodes"},
Verbs: []string{"get", "list", "watch"},
},
}
return &rbacv1.ClusterRole{
TypeMeta: metav1.TypeMeta{
APIVersion: "rbac.authorization.k8s.io/v1",
Kind: "ClusterRole",
},
ObjectMeta: metav1.ObjectMeta{
Name: monov1alpha1.NodeAgentName,
Labels: copyStringMap(conf.Labels),
},
Rules: wantRules,
}
}
func buildAgentClusterRoleBinding(conf AgentConf) *rbacv1.ClusterRoleBinding {
return &rbacv1.ClusterRoleBinding{
TypeMeta: metav1.TypeMeta{
APIVersion: "rbac.authorization.k8s.io/v1",
Kind: "ClusterRoleBinding",
},
ObjectMeta: metav1.ObjectMeta{
Name: monov1alpha1.NodeAgentName,
Labels: copyStringMap(conf.Labels),
},
RoleRef: rbacv1.RoleRef{
APIGroup: rbacv1.GroupName,
Kind: "ClusterRole",
Name: monov1alpha1.NodeAgentName,
},
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: monov1alpha1.NodeAgentName,
Namespace: conf.Namespace,
},
},
}
}
func buildAgentDaemonSet(conf AgentConf) *appsv1.DaemonSet {
privileged := true
dsLabels := monov1alpha1.NodeAgentLabels()
image, pullPolicy := agentImage(conf)
return &appsv1.DaemonSet{
TypeMeta: metav1.TypeMeta{
APIVersion: "apps/v1",
Kind: "DaemonSet",
},
ObjectMeta: metav1.ObjectMeta{
Name: monov1alpha1.NodeAgentName,
Namespace: conf.Namespace,
Labels: copyStringMap(conf.Labels),
},
Spec: appsv1.DaemonSetSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app.kubernetes.io/name": monov1alpha1.NodeAgentName,
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: dsLabels,
},
Spec: corev1.PodSpec{
ServiceAccountName: monov1alpha1.NodeAgentName,
HostNetwork: true,
HostPID: true,
DNSPolicy: corev1.DNSClusterFirstWithHostNet,
ImagePullSecrets: imagePullSecrets(conf.ImagePullSecrets),
NodeSelector: map[string]string{
monov1alpha1.NodeControlKey: "true",
},
Tolerations: []corev1.Toleration{
{Operator: corev1.TolerationOpExists},
},
Containers: []corev1.Container{
{
Name: "agent",
Image: image,
ImagePullPolicy: pullPolicy,
Args: []string{"agent", "--env-file", "$(CLUSTER_ENV_FILE)"},
Env: []corev1.EnvVar{
{
Name: "NODE_NAME",
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
APIVersion: "v1",
FieldPath: "spec.nodeName",
},
},
},
{
Name: "CLUSTER_ENV_FILE",
Value: "/host/opt/monok8s/config/cluster.env",
},
{
Name: "FW_ENV_CONFIG_FILE",
Value: "/host/etc/fw_env.config",
},
},
SecurityContext: &corev1.SecurityContext{
Privileged: &privileged,
},
VolumeMounts: []corev1.VolumeMount{
{
Name: "host-dev",
MountPath: "/dev",
},
{
Name: "host-etc",
MountPath: "/host/etc",
ReadOnly: true,
},
{
Name: "host-config",
MountPath: "/host/opt/monok8s/config",
ReadOnly: true,
},
},
},
},
Volumes: []corev1.Volume{
{
Name: "host-dev",
VolumeSource: corev1.VolumeSource{
HostPath: &corev1.HostPathVolumeSource{
Path: "/dev",
Type: hostPathType(corev1.HostPathDirectory),
},
},
},
{
Name: "host-etc",
VolumeSource: corev1.VolumeSource{
HostPath: &corev1.HostPathVolumeSource{
Path: "/etc",
Type: hostPathType(corev1.HostPathDirectory),
},
},
},
{
Name: "host-config",
VolumeSource: corev1.VolumeSource{
HostPath: &corev1.HostPathVolumeSource{
Path: "/opt/monok8s/config",
Type: hostPathType(corev1.HostPathDirectory),
},
},
},
},
},
},
},
}
}
func agentImage(conf AgentConf) (string, corev1.PullPolicy) {
if conf.Image != "" {
return conf.Image, corev1.PullIfNotPresent
}
return fmt.Sprintf("localhost/monok8s/node-control:%s", buildinfo.Version), corev1.PullNever
}
func copyStringMap(in map[string]string) map[string]string {
if len(in) == 0 {
return nil
}
out := make(map[string]string, len(in))
for k, v := range in {
out[k] = v
}
return out
}
func hostPathType(t corev1.HostPathType) *corev1.HostPathType {
return &t
}

View File

@@ -0,0 +1,203 @@
package render
import (
"context"
"fmt"
"reflect"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes"
)
func ApplyAgentDaemonSets(ctx context.Context, kubeClient kubernetes.Interface, conf AgentConf) error {
objs, err := buildAgentDaemonSetObjects(conf)
if err != nil {
return err
}
if err := applyAgentNamespace(ctx, kubeClient, buildAgentNamespace(conf)); err != nil {
return fmt.Errorf("apply namespace: %w", err)
}
for _, obj := range objs {
if err := applyAgentObject(ctx, kubeClient, obj); err != nil {
return err
}
}
return nil
}
func applyAgentObject(ctx context.Context, kubeClient kubernetes.Interface, obj runtime.Object) error {
switch want := obj.(type) {
case *corev1.ServiceAccount:
return applyAgentServiceAccount(ctx, kubeClient, want)
case *rbacv1.ClusterRole:
return applyAgentClusterRole(ctx, kubeClient, want)
case *rbacv1.ClusterRoleBinding:
return applyAgentClusterRoleBinding(ctx, kubeClient, want)
case *appsv1.DaemonSet:
return applyAgentDaemonSet(ctx, kubeClient, want)
default:
return fmt.Errorf("unsupported agent object type %T", obj)
}
}
func applyAgentNamespace(ctx context.Context, kubeClient kubernetes.Interface, want *corev1.Namespace) error {
existing, err := kubeClient.CoreV1().Namespaces().Get(ctx, want.Name, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
_, err = kubeClient.CoreV1().Namespaces().Create(ctx, want, metav1.CreateOptions{})
return err
}
if err != nil {
return err
}
labels, changed := mergeStringMapsInto(existing.Labels, want.Labels)
if !changed {
return nil
}
existing.Labels = labels
_, err = kubeClient.CoreV1().Namespaces().Update(ctx, existing, metav1.UpdateOptions{})
return err
}
func applyAgentServiceAccount(ctx context.Context, kubeClient kubernetes.Interface, want *corev1.ServiceAccount) error {
existing, err := kubeClient.CoreV1().ServiceAccounts(want.Namespace).Get(ctx, want.Name, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
_, err = kubeClient.CoreV1().ServiceAccounts(want.Namespace).Create(ctx, want, metav1.CreateOptions{})
return err
}
if err != nil {
return err
}
changed := false
if !reflect.DeepEqual(existing.Labels, want.Labels) {
existing.Labels = want.Labels
changed = true
}
if !changed {
return nil
}
_, err = kubeClient.CoreV1().ServiceAccounts(want.Namespace).Update(ctx, existing, metav1.UpdateOptions{})
return err
}
func applyAgentClusterRole(ctx context.Context, kubeClient kubernetes.Interface, want *rbacv1.ClusterRole) error {
existing, err := kubeClient.RbacV1().ClusterRoles().Get(ctx, want.Name, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
_, err = kubeClient.RbacV1().ClusterRoles().Create(ctx, want, metav1.CreateOptions{})
return err
}
if err != nil {
return err
}
changed := false
if !reflect.DeepEqual(existing.Labels, want.Labels) {
existing.Labels = want.Labels
changed = true
}
if !reflect.DeepEqual(existing.Rules, want.Rules) {
existing.Rules = want.Rules
changed = true
}
if !changed {
return nil
}
_, err = kubeClient.RbacV1().ClusterRoles().Update(ctx, existing, metav1.UpdateOptions{})
return err
}
func applyAgentClusterRoleBinding(ctx context.Context, kubeClient kubernetes.Interface, want *rbacv1.ClusterRoleBinding) error {
existing, err := kubeClient.RbacV1().ClusterRoleBindings().Get(ctx, want.Name, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
_, err = kubeClient.RbacV1().ClusterRoleBindings().Create(ctx, want, metav1.CreateOptions{})
return err
}
if err != nil {
return err
}
// roleRef is immutable. If it differs, fail loudly instead of pretending we can patch it.
if !reflect.DeepEqual(existing.RoleRef, want.RoleRef) {
return fmt.Errorf("existing ClusterRoleBinding %q has different roleRef and must be recreated", want.Name)
}
changed := false
if !reflect.DeepEqual(existing.Labels, want.Labels) {
existing.Labels = want.Labels
changed = true
}
if !reflect.DeepEqual(existing.Subjects, want.Subjects) {
existing.Subjects = want.Subjects
changed = true
}
if !changed {
return nil
}
_, err = kubeClient.RbacV1().ClusterRoleBindings().Update(ctx, existing, metav1.UpdateOptions{})
return err
}
func applyAgentDaemonSet(ctx context.Context, kubeClient kubernetes.Interface, want *appsv1.DaemonSet) error {
existing, err := kubeClient.AppsV1().DaemonSets(want.Namespace).Get(ctx, want.Name, metav1.GetOptions{})
if apierrors.IsNotFound(err) {
_, err = kubeClient.AppsV1().DaemonSets(want.Namespace).Create(ctx, want, metav1.CreateOptions{})
return err
}
if err != nil {
return err
}
changed := false
if !reflect.DeepEqual(existing.Labels, want.Labels) {
existing.Labels = want.Labels
changed = true
}
if !reflect.DeepEqual(existing.Spec, want.Spec) {
existing.Spec = want.Spec
changed = true
}
if !changed {
return nil
}
_, err = kubeClient.AppsV1().DaemonSets(want.Namespace).Update(ctx, existing, metav1.UpdateOptions{})
return err
}
func mergeStringMapsInto(dst map[string]string, src map[string]string) (map[string]string, bool) {
if len(src) == 0 {
return dst, false
}
changed := false
if dst == nil {
dst = map[string]string{}
changed = true
}
for k, v := range src {
if dst[k] != v {
dst[k] = v
changed = true
}
}
return dst, changed
}

View File

@@ -1,7 +1,6 @@
package render
import (
"bytes"
"fmt"
appsv1 "k8s.io/api/apps/v1"
@@ -9,7 +8,6 @@ import (
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apimachinery/pkg/util/intstr"
monov1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
@@ -17,9 +15,10 @@ import (
)
type ControllerConf struct {
Namespace string
Image string
Labels map[string]string
Namespace string
Image string
ImagePullSecrets []string
Labels map[string]string
}
func RenderControllerDeployments(conf ControllerConf) (string, error) {
@@ -41,27 +40,7 @@ func RenderControllerDeployments(conf ControllerConf) (string, error) {
buildControllerDeployment(conf),
}
s := runtime.NewScheme()
_ = corev1.AddToScheme(s)
_ = rbacv1.AddToScheme(s)
_ = appsv1.AddToScheme(s)
serializer := json.NewYAMLSerializer(json.DefaultMetaFactory, s, s)
var buf bytes.Buffer
for i, obj := range objs {
if i > 0 {
if _, err := fmt.Fprintln(&buf, "---"); err != nil {
return "", err
}
}
if err := serializer.Encode(obj, &buf); err != nil {
return "", err
}
}
return buf.String(), nil
return renderObjects(objs)
}
func buildControllerServiceAccount(conf ControllerConf) *corev1.ServiceAccount {
@@ -191,6 +170,7 @@ func buildControllerDeployment(conf ControllerConf) *appsv1.Deployment {
},
Spec: corev1.PodSpec{
ServiceAccountName: monov1alpha1.ControllerName,
ImagePullSecrets: imagePullSecrets(conf.ImagePullSecrets),
Containers: []corev1.Container{
{
Name: "controller",

View File

@@ -0,0 +1,74 @@
package render
import (
"bytes"
"fmt"
"strings"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/yaml"
)
func renderObjects(objs []runtime.Object) (string, error) {
var buf bytes.Buffer
for i, obj := range objs {
if i > 0 {
if _, err := fmt.Fprintln(&buf, "---"); err != nil {
return "", err
}
}
b, err := renderObjectYAML(obj)
if err != nil {
return "", err
}
if _, err := buf.Write(b); err != nil {
return "", err
}
}
return buf.String(), nil
}
func renderObjectYAML(obj runtime.Object) ([]byte, error) {
b, err := yaml.Marshal(obj)
if err != nil {
return nil, err
}
var m map[string]any
if err := yaml.Unmarshal(b, &m); err != nil {
return nil, err
}
delete(m, "status")
return yaml.Marshal(m)
}
func imagePullSecrets(names []string) []corev1.LocalObjectReference {
if len(names) == 0 {
return nil
}
refs := make([]corev1.LocalObjectReference, 0, len(names))
for _, name := range names {
name = strings.TrimSpace(name)
if name == "" {
continue
}
refs = append(refs, corev1.LocalObjectReference{
Name: name,
})
}
if len(refs) == 0 {
return nil
}
return refs
}

View File

@@ -1,16 +1,11 @@
package render
import (
"bytes"
"fmt"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apimachinery/pkg/util/intstr"
monov1alpha1 "example.com/monok8s/pkg/apis/monok8s/v1alpha1"
@@ -39,27 +34,7 @@ func RenderSSHDDeployments(namespace, authKeys string) (string, error) {
buildSSHDDeployment(vals, namespace, labels),
}
s := runtime.NewScheme()
_ = corev1.AddToScheme(s)
_ = rbacv1.AddToScheme(s)
_ = appsv1.AddToScheme(s)
serializer := json.NewYAMLSerializer(json.DefaultMetaFactory, s, s)
var buf bytes.Buffer
for i, obj := range objs {
if i > 0 {
if _, err := fmt.Fprintln(&buf, "---"); err != nil {
return "", err
}
}
if err := serializer.Encode(obj, &buf); err != nil {
return "", err
}
}
return buf.String(), nil
return renderObjects(objs)
}
func buildSSHDConfigMap(