From dcc7e5643249ce50caf9e6f8128912173b9ea3f1 Mon Sep 17 00:00:00 2001 From: kh Date: Wed, 10 Mar 2021 17:22:14 +0800 Subject: [PATCH] GA --- Dockerfile | 3 +- Makefile | 9 +- PROJECT | 6 + README.md | 108 ++++++ api/v1/unit_types.go | 23 +- api/v1/zz_generated.deepcopy.go | 12 +- .../core.systemd.warmmetal.tech_units.yaml | 76 +++++ config/default/manager_auth_proxy_patch.yaml | 3 +- config/default/manager_config_patch.yaml | 2 +- config/manager/kustomization.yaml | 10 +- config/manager/manager.yaml | 36 +- config/rbac/role.yaml | 34 ++ config/samples/core_v1_unit.yaml | 9 +- config/samples/install.yaml | 319 ++++++++++++++++++ controllers/unit_controller.go | 89 ++++- go.mod | 1 + go.sum | 2 + main.go | 29 +- 18 files changed, 721 insertions(+), 50 deletions(-) create mode 100644 README.md create mode 100644 config/crd/bases/core.systemd.warmmetal.tech_units.yaml create mode 100644 config/rbac/role.yaml create mode 100644 config/samples/install.yaml diff --git a/Dockerfile b/Dockerfile index ce816f3..cd466b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,9 +19,8 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o manager # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details -FROM gcr.io/distroless/static:nonroot +FROM centos/systemd:latest WORKDIR / COPY --from=builder /workspace/manager . -USER 65532:65532 ENTRYPOINT ["/manager"] diff --git a/Makefile b/Makefile index c4f78a3..3938fa4 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Image URL to use all building/pushing image targets -IMG ?= controller:latest +IMG ?= docker.io/warmmetal/kube-systemd-controller:v0.1.0 # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) CRD_OPTIONS ?= "crd:trivialVersions=true,preserveUnknownFields=false" @@ -49,6 +49,9 @@ undeploy: manifests: controller-gen $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases +dump-manifest: manifests kustomize + $(KUSTOMIZE) build config/default > config/samples/install.yaml + # Run go fmt against code fmt: go fmt ./... @@ -62,8 +65,8 @@ generate: controller-gen $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." # Build the docker image -docker-build: test - docker build -t ${IMG} . +docker-build: + kubectl dev build -t ${IMG} . # Push the docker image docker-push: diff --git a/PROJECT b/PROJECT index 55d89a2..b9e3895 100644 --- a/PROJECT +++ b/PROJECT @@ -2,4 +2,10 @@ domain: systemd.warmmetal.tech layout: go.kubebuilder.io/v3 projectName: kube-systemd repo: github.com/warm-metal/kube-systemd +resources: +- api: + crdVersion: v1 + group: core + kind: Unit + version: v1 version: 3-alpha diff --git a/README.md b/README.md new file mode 100644 index 0000000..41f2b5f --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +# KubeSystemd + +**kube-systemd** is a controller to help manage systemd services on each Node in clusters. + +With clusters like minikube on hyberkit, which boot always from an ISO, +**kube-systemd** could save configurations of systemd services and apply them after nodes started. + +**kube-systemd** introduces CRD Unit to save all configurations. + +```yaml +type Unit struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec UnitSpec `json:"spec,omitempty"` + Status UnitStatus `json:"status,omitempty"` +} + +type UnitSpec struct { + // Path defines the absolute path on the host of the unit. + Path string `json:"path,omitempty"` + + // Definition specifies the unit definition. If set, it is written to the unit configuration which Path defines. + // Or, the original unit on the host will be used. + // +optional + Definition string `json:"definition,omitempty"` + + // Config specifies config files and contents on the host with respect to the systemd unit. + // The key is the absolute path of the configuration file. And, the value is the file content. + // +optional + Config map[string]string `json:"config,omitempty"` +} +``` + +## Install +```shell script +kubectl apply -f https://raw.githubusercontent.com/warm-metal/kube-systemd/master/config/samples/install.yaml +``` + +## Demo + +We can create a unit to modify NTP server configuration in a minikube cluster to make sure the cluster clock is always +synchronized to the NTP server. + +```yaml +apiVersion: core.systemd.warmmetal.tech/v1 +kind: Unit +metadata: + name: systemd-timesyncd.service +spec: + path: "/lib/systemd/system/systemd-timesyncd.service" + config: + "/etc/systemd/timesyncd.conf": | + [Time] + NTP=ntp1.aliyun.com +``` + +After the unit executed, we could see that its status changed. +That is, `status.execTimestamp` is updated to the time last executed. +If errors raised, the `status.error` would be also updated. + +```yaml +apiVersion: core.systemd.warmmetal.tech/v1 +kind: Unit +metadata: + annotations: + kubectl.kubernetes.io/last-applied-configuration: | + {"apiVersion":"core.systemd.warmmetal.tech/v1","kind":"Unit","metadata":{"annotations":{},"name":"systemd-timesyncd.service"},"spec":{"config":{"/etc/systemd/timesyncd.conf":"[Time]\nNTP=ntp1.aliyun.com\n"},"path":"/lib/systemd/system/systemd-timesyncd.service"}} + creationTimestamp: "2021-03-10T08:52:30Z" + generation: 1 + managedFields: + - apiVersion: core.systemd.warmmetal.tech/v1 + fieldsType: FieldsV1 + fieldsV1: + f:metadata: + f:annotations: + .: {} + f:kubectl.kubernetes.io/last-applied-configuration: {} + f:spec: + .: {} + f:config: + .: {} + f:/etc/systemd/timesyncd.conf: {} + f:path: {} + manager: kubectl-client-side-apply + operation: Update + time: "2021-03-10T08:52:30Z" + - apiVersion: core.systemd.warmmetal.tech/v1 + fieldsType: FieldsV1 + fieldsV1: + f:status: + .: {} + f:execTimestamp: {} + manager: manager + operation: Update + time: "2021-03-10T08:52:30Z" + name: systemd-timesyncd.service + resourceVersion: "208241" + uid: ad1d4311-b26b-4261-8551-f81f659fa2d3 +spec: + config: + /etc/systemd/timesyncd.conf: | + [Time] + NTP=ntp1.aliyun.com + path: /lib/systemd/system/systemd-timesyncd.service +status: + execTimestamp: "2021-03-10T09:08:46Z" +``` diff --git a/api/v1/unit_types.go b/api/v1/unit_types.go index 70c80a4..410b8e5 100644 --- a/api/v1/unit_types.go +++ b/api/v1/unit_types.go @@ -28,18 +28,37 @@ type UnitSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file - // Foo is an example field of Unit. Edit unit_types.go to remove/update - Foo string `json:"foo,omitempty"` + // Path defines the absolute path on the host of the unit. + Path string `json:"path,omitempty"` + + // Definition specifies the unit definition. If set, it is written to the unit configuration which Path defines. + // Or, the original unit on the host will be used. + // +optional + Definition string `json:"definition,omitempty"` + + // Config specifies config files and contents on the host with respect to the systemd unit. + // The key is the absolute path of the configuration file. And, the value is the file content. + // +optional + Config map[string]string `json:"config,omitempty"` } // UnitStatus defines the observed state of Unit type UnitStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file + + // Timestamp of the last execution + // +optional + ExecTimestamp metav1.Time `json:"execTimestamp,omitempty"` + + // Specify Errors on reconcile + // +optional + Error string `json:"error,omitempty"` } //+kubebuilder:object:root=true //+kubebuilder:subresource:status +//+kubebuilder:resource:scope=Cluster // Unit is the Schema for the units API type Unit struct { diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 3a03306..4792dcb 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -29,8 +29,8 @@ func (in *Unit) DeepCopyInto(out *Unit) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Unit. @@ -86,6 +86,13 @@ func (in *UnitList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UnitSpec) DeepCopyInto(out *UnitSpec) { *out = *in + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UnitSpec. @@ -101,6 +108,7 @@ func (in *UnitSpec) DeepCopy() *UnitSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UnitStatus) DeepCopyInto(out *UnitStatus) { *out = *in + in.ExecTimestamp.DeepCopyInto(&out.ExecTimestamp) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UnitStatus. diff --git a/config/crd/bases/core.systemd.warmmetal.tech_units.yaml b/config/crd/bases/core.systemd.warmmetal.tech_units.yaml new file mode 100644 index 0000000..ebea127 --- /dev/null +++ b/config/crd/bases/core.systemd.warmmetal.tech_units.yaml @@ -0,0 +1,76 @@ + +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: units.core.systemd.warmmetal.tech +spec: + group: core.systemd.warmmetal.tech + names: + kind: Unit + listKind: UnitList + plural: units + singular: unit + scope: Cluster + versions: + - name: v1 + schema: + openAPIV3Schema: + description: Unit is the Schema for the units API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: UnitSpec defines the desired state of Unit + properties: + config: + additionalProperties: + type: string + description: Config specifies config files and contents on the host + with respect to the systemd unit. The key is the absolute path of + the configuration file. And, the value is the file content. + type: object + definition: + description: Definition specifies the unit definition. If set, it + is written to the unit configuration which Path defines. Or, the + original unit on the host will be used. + type: string + path: + description: Path defines the absolute path on the host of the unit. + type: string + type: object + status: + description: UnitStatus defines the observed state of Unit + properties: + error: + description: Specify Errors on reconcile + type: string + execTimestamp: + description: Timestamp of the last execution + format: date-time + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/config/default/manager_auth_proxy_patch.yaml b/config/default/manager_auth_proxy_patch.yaml index 49b1f1a..ffe5760 100644 --- a/config/default/manager_auth_proxy_patch.yaml +++ b/config/default/manager_auth_proxy_patch.yaml @@ -1,7 +1,7 @@ # This patch inject a sidecar container which is a HTTP proxy for the # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. apiVersion: apps/v1 -kind: Deployment +kind: DaemonSet metadata: name: controller-manager namespace: system @@ -23,4 +23,3 @@ spec: args: - "--health-probe-bind-address=:8081" - "--metrics-bind-address=127.0.0.1:8080" - - "--leader-elect" diff --git a/config/default/manager_config_patch.yaml b/config/default/manager_config_patch.yaml index 6c40015..f5df3ed 100644 --- a/config/default/manager_config_patch.yaml +++ b/config/default/manager_config_patch.yaml @@ -1,5 +1,5 @@ apiVersion: apps/v1 -kind: Deployment +kind: DaemonSet metadata: name: controller-manager namespace: system diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 2bcd3ee..9c4a05a 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -5,6 +5,12 @@ generatorOptions: disableNameSuffixHash: true configMapGenerator: -- name: manager-config - files: +- files: - controller_manager_config.yaml + name: manager-config +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: docker.io/warmmetal/kube-systemd-controller + newTag: v0.1.0 diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index 70e3f6a..2a581b5 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -6,7 +6,7 @@ metadata: name: system --- apiVersion: apps/v1 -kind: Deployment +kind: DaemonSet metadata: name: controller-manager namespace: system @@ -16,23 +16,20 @@ spec: selector: matchLabels: control-plane: controller-manager - replicas: 1 template: metadata: labels: control-plane: controller-manager spec: - securityContext: - runAsUser: 65532 containers: - command: - /manager args: - - --leader-elect image: controller:latest + imagePullPolicy: IfNotPresent name: manager securityContext: - allowPrivilegeEscalation: false + privileged: true livenessProbe: httpGet: path: /healthz @@ -52,4 +49,29 @@ spec: requests: cpu: 100m memory: 20Mi - terminationGracePeriodSeconds: 10 + volumeMounts: + - mountPath: /etc + name: systemd-config + - mountPath: /lib/systemd + name: systemd-lib + - mountPath: /etc/systemd + name: systemd-etc + - mountPath: /run/systemd + name: systemd-run + volumes: + - hostPath: + path: /etc + type: Directory + name: systemd-config + - hostPath: + path: /lib/systemd + type: Directory + name: systemd-lib + - hostPath: + path: /etc/systemd + type: Directory + name: systemd-etc + - hostPath: + path: /run/systemd + type: Directory + name: systemd-run diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml new file mode 100644 index 0000000..9bc393b --- /dev/null +++ b/config/rbac/role.yaml @@ -0,0 +1,34 @@ + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: manager-role +rules: +- apiGroups: + - core.systemd.warmmetal.tech + resources: + - units + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - core.systemd.warmmetal.tech + resources: + - units/finalizers + verbs: + - update +- apiGroups: + - core.systemd.warmmetal.tech + resources: + - units/status + verbs: + - get + - patch + - update diff --git a/config/samples/core_v1_unit.yaml b/config/samples/core_v1_unit.yaml index 2d0bd19..07837b1 100644 --- a/config/samples/core_v1_unit.yaml +++ b/config/samples/core_v1_unit.yaml @@ -1,7 +1,10 @@ apiVersion: core.systemd.warmmetal.tech/v1 kind: Unit metadata: - name: unit-sample + name: systemd-timesyncd.service spec: - # Add fields here - foo: bar + path: "/lib/systemd/system/systemd-timesyncd.service" + config: + "/etc/systemd/timesyncd.conf": | + [Time] + NTP=ntp1.aliyun.com diff --git a/config/samples/install.yaml b/config/samples/install.yaml new file mode 100644 index 0000000..63b7080 --- /dev/null +++ b/config/samples/install.yaml @@ -0,0 +1,319 @@ +apiVersion: v1 +kind: Namespace +metadata: + labels: + control-plane: controller-manager + name: kube-systemd-system +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.4.1 + creationTimestamp: null + name: units.core.systemd.warmmetal.tech +spec: + group: core.systemd.warmmetal.tech + names: + kind: Unit + listKind: UnitList + plural: units + singular: unit + scope: Cluster + versions: + - name: v1 + schema: + openAPIV3Schema: + description: Unit is the Schema for the units API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: UnitSpec defines the desired state of Unit + properties: + config: + additionalProperties: + type: string + description: Config specifies config files and contents on the host with respect to the systemd unit. The key is the absolute path of the configuration file. And, the value is the file content. + type: object + definition: + description: Definition specifies the unit definition. If set, it is written to the unit configuration which Path defines. Or, the original unit on the host will be used. + type: string + path: + description: Path defines the absolute path on the host of the unit. + type: string + type: object + status: + description: UnitStatus defines the observed state of Unit + properties: + error: + description: Specify Errors on reconcile + type: string + execTimestamp: + description: Timestamp of the last execution + format: date-time + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: kube-systemd-leader-election-role + namespace: kube-systemd-system +rules: +- apiGroups: + - "" + - coordination.k8s.io + resources: + - configmaps + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + creationTimestamp: null + name: kube-systemd-manager-role +rules: +- apiGroups: + - core.systemd.warmmetal.tech + resources: + - units + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - core.systemd.warmmetal.tech + resources: + - units/finalizers + verbs: + - update +- apiGroups: + - core.systemd.warmmetal.tech + resources: + - units/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kube-systemd-metrics-reader +rules: +- nonResourceURLs: + - /metrics + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: kube-systemd-proxy-role +rules: +- apiGroups: + - authentication.k8s.io + resources: + - tokenreviews + verbs: + - create +- apiGroups: + - authorization.k8s.io + resources: + - subjectaccessreviews + verbs: + - create +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: kube-systemd-leader-election-rolebinding + namespace: kube-systemd-system +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kube-systemd-leader-election-role +subjects: +- kind: ServiceAccount + name: default + namespace: kube-systemd-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: kube-systemd-manager-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kube-systemd-manager-role +subjects: +- kind: ServiceAccount + name: default + namespace: kube-systemd-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: kube-systemd-proxy-rolebinding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: kube-systemd-proxy-role +subjects: +- kind: ServiceAccount + name: default + namespace: kube-systemd-system +--- +apiVersion: v1 +data: + controller_manager_config.yaml: | + apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 + kind: ControllerManagerConfig + health: + healthProbeBindAddress: :8081 + metrics: + bindAddress: 127.0.0.1:8080 + webhook: + port: 9443 + leaderElection: + leaderElect: true + resourceName: b4119a85.systemd.warmmetal.tech +kind: ConfigMap +metadata: + name: kube-systemd-manager-config + namespace: kube-systemd-system +--- +apiVersion: v1 +kind: Service +metadata: + labels: + control-plane: controller-manager + name: kube-systemd-controller-manager-metrics-service + namespace: kube-systemd-system +spec: + ports: + - name: https + port: 8443 + targetPort: https + selector: + control-plane: controller-manager +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + labels: + control-plane: controller-manager + name: kube-systemd-controller-manager + namespace: kube-systemd-system +spec: + selector: + matchLabels: + control-plane: controller-manager + template: + metadata: + labels: + control-plane: controller-manager + spec: + containers: + - args: + - --secure-listen-address=0.0.0.0:8443 + - --upstream=http://127.0.0.1:8080/ + - --logtostderr=true + - --v=10 + image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 + name: kube-rbac-proxy + ports: + - containerPort: 8443 + name: https + - args: + - --health-probe-bind-address=:8081 + - --metrics-bind-address=127.0.0.1:8080 + command: + - /manager + image: docker.io/warmmetal/kube-systemd-controller:v0.1.0 + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + name: manager + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + resources: + limits: + cpu: 100m + memory: 30Mi + requests: + cpu: 100m + memory: 20Mi + securityContext: + privileged: true + volumeMounts: + - mountPath: /etc + name: systemd-config + - mountPath: /lib/systemd + name: systemd-lib + - mountPath: /etc/systemd + name: systemd-etc + - mountPath: /run/systemd + name: systemd-run + volumes: + - hostPath: + path: /etc + type: Directory + name: systemd-config + - hostPath: + path: /lib/systemd + type: Directory + name: systemd-lib + - hostPath: + path: /etc/systemd + type: Directory + name: systemd-etc + - hostPath: + path: /run/systemd + type: Directory + name: systemd-run diff --git a/controllers/unit_controller.go b/controllers/unit_controller.go index eead0ba..c0dbb99 100644 --- a/controllers/unit_controller.go +++ b/controllers/unit_controller.go @@ -18,6 +18,14 @@ package controllers import ( "context" + "golang.org/x/xerrors" + "io/ioutil" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/runtime" @@ -32,8 +40,16 @@ type UnitReconciler struct { client.Client Log logr.Logger Scheme *runtime.Scheme + + Executed map[string]bool } +const ( + configurationDir = "/etc" + libSystemdDir = "/lib/systemd" + etcSystemdDir = "/etc/systemd" +) + //+kubebuilder:rbac:groups=core.systemd.warmmetal.tech,resources=units,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=core.systemd.warmmetal.tech,resources=units/status,verbs=get;update;patch //+kubebuilder:rbac:groups=core.systemd.warmmetal.tech,resources=units/finalizers,verbs=update @@ -50,11 +66,82 @@ type UnitReconciler struct { func (r *UnitReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = r.Log.WithValues("unit", req.NamespacedName) - // your logic here + list := &corev1.UnitList{} + if err := r.List(ctx, list); err != nil { + return ctrl.Result{}, err + } + + nextUnits := make([]*corev1.Unit, 0, len(list.Items)) + for i := range list.Items { + if !r.Executed[list.Items[i].Name] { + nextUnits = append(nextUnits, &list.Items[i]) + } + } + + sort.Slice(nextUnits, func(i, j int) bool { + return nextUnits[i].Name < nextUnits[j].Name + }) + + now := metav1.Now() + for i := range nextUnits { + unit := nextUnits[i] + unit.Status.ExecTimestamp = now + err := startUnit(ctx, unit) + if err != nil { + unit.Status.Error = err.Error() + } + + if err := r.Status().Update(ctx, unit); err != nil { + return ctrl.Result{}, err + } + + if err != nil { + return ctrl.Result{}, err + } + + r.Executed[unit.Name] = true + } return ctrl.Result{}, nil } +func startUnit(ctx context.Context, unit *corev1.Unit) error { + if len(unit.Spec.Path) == 0 { + return xerrors.New("Spec.Path is required") + } + + if !strings.HasPrefix(unit.Spec.Path, libSystemdDir) && !strings.HasPrefix(unit.Spec.Path, etcSystemdDir) { + return xerrors.Errorf("Spec.Path must be in directory %q or %q", libSystemdDir, etcSystemdDir) + } + + if len(unit.Spec.Definition) > 0 { + if err := ioutil.WriteFile(unit.Spec.Path, []byte(unit.Spec.Definition), 0644); err != nil { + return xerrors.Errorf("unable to write unit file %q: %s", unit.Spec.Path, err) + } + } + + for path, content := range unit.Spec.Config { + if !strings.HasPrefix(path, configurationDir) { + return xerrors.Errorf("config must be in directory %q", configurationDir) + } + + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return xerrors.Errorf("unable to create dir %q: %s", path, err) + } + + if err := ioutil.WriteFile(path, []byte(content), 0644); err != nil { + return xerrors.Errorf("unable to write config %q: %s", path, err) + } + } + + systemctl := exec.CommandContext(ctx, "systemctl", "restart", filepath.Base(unit.Spec.Path)) + if err := systemctl.Start(); err != nil { + return xerrors.Errorf("unable to restart service %q", filepath.Base(unit.Spec.Path)) + } + + return nil +} + // SetupWithManager sets up the controller with the Manager. func (r *UnitReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). diff --git a/go.mod b/go.mod index 3e2d40c..c33e442 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/go-logr/logr v0.3.0 // indirect github.com/onsi/ginkgo v1.14.1 // indirect github.com/onsi/gomega v1.10.2 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect k8s.io/apimachinery v0.19.2 k8s.io/client-go v0.19.2 sigs.k8s.io/controller-runtime v0.7.0 diff --git a/go.sum b/go.sum index ed5f385..5c325a1 100644 --- a/go.sum +++ b/go.sum @@ -531,6 +531,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.1.0 h1:Phva6wqu+xR//Njw6iorylFFgn/z547tw5Ne3HZPQ+k= gomodules.xyz/jsonpatch/v2 v2.1.0/go.mod h1:IhYNNY4jnS53ZnfE4PAmpKtDpTCj1JFXc+3mwe7XcUU= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= diff --git a/main.go b/main.go index 2d76235..ed07a6e 100644 --- a/main.go +++ b/main.go @@ -50,13 +50,9 @@ func init() { func main() { var metricsAddr string - var enableLeaderElection bool var probeAddr string flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") - flag.BoolVar(&enableLeaderElection, "leader-elect", false, - "Enable leader election for controller manager. "+ - "Enabling this will ensure there is only one active controller manager.") opts := zap.Options{ Development: true, } @@ -70,8 +66,6 @@ func main() { MetricsBindAddress: metricsAddr, Port: 9443, HealthProbeBindAddress: probeAddr, - LeaderElection: enableLeaderElection, - LeaderElectionID: "b4119a85.systemd.warmmetal.tech", }) if err != nil { setupLog.Error(err, "unable to start manager") @@ -79,25 +73,10 @@ func main() { } if err = (&controllers.UnitReconciler{ - Client: mgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("Unit"), - Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "Unit") - os.Exit(1) - } - if err = (&controllers.UnitReconciler{ - Client: mgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("Unit"), - Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "Unit") - os.Exit(1) - } - if err = (&controllers.UnitReconciler{ - Client: mgr.GetClient(), - Log: ctrl.Log.WithName("controllers").WithName("Unit"), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("Unit"), + Scheme: mgr.GetScheme(), + Executed: make(map[string]bool), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Unit") os.Exit(1)