├── config ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── certmanager │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── certificate.yaml ├── samples │ ├── kustomization.yaml │ ├── seaweedfs_s3_config.json │ ├── seaweed_v1_seaweed.yaml │ ├── seaweed_v1_seaweed_with_iam_embedded.yaml │ ├── seaweed_v1_migration_example.yaml │ ├── seaweed_v1_seaweed_topology.yaml │ └── seaweed_v1_seaweed_tree_topology.yaml ├── webhook │ ├── kustomization.yaml │ ├── service.yaml │ ├── kustomizeconfig.yaml │ └── manifests.yaml ├── rbac │ ├── auth_proxy_client_clusterrole.yaml │ ├── role_binding.yaml │ ├── auth_proxy_role_binding.yaml │ ├── leader_election_role_binding.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_service.yaml │ ├── kustomization.yaml │ ├── seaweed_viewer_role.yaml │ ├── seaweed_editor_role.yaml │ ├── leader_election_role.yaml │ └── role.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── crd │ ├── patches │ │ ├── cainjection_in_seaweeds.yaml │ │ └── webhook_in_seaweeds.yaml │ ├── kustomizeconfig.yaml │ └── kustomization.yaml └── default │ ├── manager_webhook_patch.yaml │ ├── webhookcainjection_patch.yaml │ ├── manager_auth_proxy_patch.yaml │ └── kustomization.yaml ├── .dockerignore ├── deploy └── helm │ ├── Chart.yaml │ ├── .helmignore │ ├── templates │ ├── rbac │ │ ├── seaweed_viewer_role.yaml │ │ ├── role_binding.yaml │ │ ├── leader_election_role_binding.yaml │ │ ├── seaweed_editor_role.yaml │ │ ├── leader_election_role.yaml │ │ └── role.yaml │ ├── container-registry-secret.yaml │ ├── webhook │ │ ├── service.yaml │ │ ├── mutating-webhook.yaml │ │ ├── validating-webhook.yaml │ │ └── job-update-webhook-certificates.yaml │ ├── configmap-grafana-dashboard.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ ├── servicemonitor.yaml │ ├── deployment.yaml │ └── _helpers.tpl │ ├── values.yaml │ └── README.md ├── PROJECT ├── .github └── workflows │ ├── pre-commit.yaml │ ├── make-test.yaml │ ├── make-test-e2e.yaml │ ├── image.yml │ ├── helm_chart_release.yml │ └── integration-test.yml ├── seaweedfs-operator.iml ├── hack └── boilerplate.go.txt ├── internal └── controller │ ├── controller_filer_configmap.go │ ├── controller_master_configmap.go │ ├── controller_master_servicemonitor.go │ ├── controller_filer_servicemonitor.go │ ├── seaweed_maintenance.go │ ├── label │ └── label.go │ ├── controller_ingress.go │ ├── controller_volume_servicemonitor.go │ ├── swadmin │ └── seaweed_admin.go │ ├── helper_test.go │ ├── seaweed_controller_test.go │ ├── controller_filer_ingress.go │ ├── suite_test.go │ ├── controller_master_service.go │ ├── controller_filer_service.go │ ├── seaweed_controller.go │ ├── controller_filer.go │ ├── controller_master_statefulset.go │ ├── controller_master.go │ ├── helper.go │ ├── controller_volume_service.go │ └── controller_filer_statefulset.go ├── api └── v1 │ ├── groupversion_info.go │ ├── seaweed_webhook.go │ └── component_accessor.go ├── Dockerfile ├── .gitignore ├── .pre-commit-config.yaml ├── test ├── e2e │ ├── e2e_suite_test.go │ └── e2e_test.go ├── utils │ └── utils.go └── INTEGRATION_TESTING.md ├── IAM_SUPPORT.md └── cmd └── main.go /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## This file is auto-generated, do not modify ## 2 | resources: 3 | - seaweed_v1_seaweed.yaml 4 | -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: ["/metrics"] 7 | verbs: ["get"] 8 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller:latest 7 | newName: ghcr.io/seaweedfs/seaweedfs-operator 8 | newTag: v0.0.1 9 | -------------------------------------------------------------------------------- /config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: webhook-service 6 | namespace: system 7 | spec: 8 | ports: 9 | - port: 443 10 | targetPort: 9443 11 | selector: 12 | control-plane: controller-manager 13 | -------------------------------------------------------------------------------- /deploy/helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: seaweedfs-operator 3 | description: A Helm chart for the seaweedfs-operator 4 | type: application 5 | version: 0.1.9 6 | appVersion: "1.0.8" 7 | maintainers: 8 | - name: chrislusf 9 | url: https://github.com/chrislusf 10 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: seaweedfs.com 2 | layout: go.kubebuilder.io/v2 3 | repo: github.com/seaweedfs/seaweedfs-operator 4 | projectName: seaweedfs-operator 5 | resources: 6 | - group: seaweed 7 | kind: Seaweed 8 | version: v1 9 | version: 3-alpha 10 | plugins: 11 | go.operator-sdk.io/v2-alpha: {} 12 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yaml: -------------------------------------------------------------------------------- 1 | name: Pre-commit 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | pre-commit: 8 | runs-on: ubuntu-22.04 9 | steps: 10 | - uses: actions/checkout@v4.1.7 11 | - uses: actions/setup-go@v5.0.1 12 | with: 13 | go-version: 1.22.4 14 | - uses: pre-commit/action@v3.0.1 15 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: ["authentication.k8s.io"] 7 | resources: 8 | - tokenreviews 9 | verbs: ["create"] 10 | - apiGroups: ["authorization.k8s.io"] 11 | resources: 12 | - subjectaccessreviews 13 | verbs: ["create"] 14 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: controller-manager-metrics-service 7 | namespace: system 8 | spec: 9 | ports: 10 | - name: https 11 | port: 8443 12 | targetPort: https 13 | selector: 14 | control-plane: controller-manager 15 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_seaweeds.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: seaweeds.seaweed.seaweedfs.com 9 | -------------------------------------------------------------------------------- /seaweedfs-operator.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | name: controller-manager-metrics-monitor 9 | namespace: system 10 | spec: 11 | endpoints: 12 | - path: /metrics 13 | port: https 14 | selector: 15 | matchLabels: 16 | control-plane: controller-manager 17 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - role.yaml 3 | - role_binding.yaml 4 | - leader_election_role.yaml 5 | - leader_election_role_binding.yaml 6 | # Comment the following 4 lines if you want to disable 7 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 8 | # which protects your /metrics endpoint. 9 | - auth_proxy_service.yaml 10 | - auth_proxy_role.yaml 11 | - auth_proxy_role_binding.yaml 12 | - auth_proxy_client_clusterrole.yaml 13 | -------------------------------------------------------------------------------- /config/rbac/seaweed_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view seaweeds. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: seaweed-viewer-role 6 | rules: 7 | - apiGroups: 8 | - seaweed.seaweedfs.com 9 | resources: 10 | - seaweeds 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - seaweed.seaweedfs.com 17 | resources: 18 | - seaweeds/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /deploy/helm/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /deploy/helm/templates/rbac/seaweed_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view seaweeds. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: seaweed-viewer-role 6 | rules: 7 | - apiGroups: 8 | - seaweed.seaweedfs.com 9 | resources: 10 | - seaweeds 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - seaweed.seaweedfs.com 17 | resources: 18 | - seaweeds/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /deploy/helm/templates/container-registry-secret.yaml: -------------------------------------------------------------------------------- 1 | {{ if (include "seaweedfs-operator.createPullSecret" .) }} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: {{ include "seaweedfs-operator.fullname" . }}-container-registry 6 | labels: 7 | app: {{ include "seaweedfs-operator.fullname" . }} 8 | type: kubernetes.io/dockerconfigjson 9 | data: 10 | .dockerconfigjson: {{ include "seaweedfs-operator.imagePullSecret" .Values.image.credentials }} 11 | {{ end }} 12 | -------------------------------------------------------------------------------- /config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref and var substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: cert-manager.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: cert-manager.io 8 | path: spec/issuerRef/name 9 | 10 | varReference: 11 | - kind: Certificate 12 | group: cert-manager.io 13 | path: spec/commonName 14 | - kind: Certificate 15 | group: cert-manager.io 16 | path: spec/dnsNames 17 | -------------------------------------------------------------------------------- /deploy/helm/templates/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.webhook.enabled }} 2 | 3 | apiVersion: v1 4 | kind: Service 5 | metadata: 6 | name: {{ include "seaweedfs-operator.fullname" . }}-webhook 7 | labels: 8 | app: {{ include "seaweedfs-operator.fullname" . }} 9 | spec: 10 | type: ClusterIP 11 | ports: 12 | - name: https 13 | port: 443 14 | targetPort: 9443 15 | selector: 16 | {{- include "seaweedfs-operator.selectorLabels" . | nindent 4 }} 17 | 18 | {{- end }} 19 | -------------------------------------------------------------------------------- /deploy/helm/templates/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: {{ include "seaweedfs-operator.fullname" . }}-manager-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: {{ include "seaweedfs-operator.fullname" . }}-manager-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: {{ include "seaweedfs-operator.serviceAccountName" . }} 12 | namespace: {{ .Release.Namespace }} 13 | -------------------------------------------------------------------------------- /deploy/helm/templates/configmap-grafana-dashboard.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.grafanaDashboard.enabled }} 2 | 3 | apiVersion: v1 4 | kind: ConfigMap 5 | metadata: 6 | name: {{ include "seaweedfs-operator.fullname" . }}-grafana-dashboard 7 | labels: 8 | app: {{ include "seaweedfs-operator.fullname" . }} 9 | grafana_dashboard: {{ include "seaweedfs-operator.fullname" . }} 10 | data: 11 | seaweedfs.json: | 12 | {{- $.Files.Get "dashboard/seaweedfs-grafana-dashboard.json" | nindent 4 }} 13 | 14 | {{- end }} 15 | -------------------------------------------------------------------------------- /config/rbac/seaweed_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit seaweeds. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: seaweed-editor-role 6 | rules: 7 | - apiGroups: 8 | - seaweed.seaweedfs.com 9 | resources: 10 | - seaweeds 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - seaweed.seaweedfs.com 21 | resources: 22 | - seaweeds/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /deploy/helm/templates/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: {{ include "seaweedfs-operator.fullname" . }}-leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: {{ include "seaweedfs-operator.fullname" . }}-leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: {{ include "seaweedfs-operator.serviceAccountName" . }} 12 | namespace: {{ .Release.Namespace }} 13 | -------------------------------------------------------------------------------- /deploy/helm/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "seaweedfs-operator.fullname" . }} 5 | labels: 6 | app: {{ include "seaweedfs-operator.fullname" . }} 7 | app.kubernetes.io/component: metrics 8 | spec: 9 | type: ClusterIP 10 | ports: 11 | - name: {{ .Values.service.portName }} 12 | port: {{ .Values.service.port }} 13 | targetPort: {{ .Values.port.number }} 14 | selector: 15 | {{- include "seaweedfs-operator.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /deploy/helm/templates/rbac/seaweed_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit seaweeds. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: seaweed-editor-role 6 | rules: 7 | - apiGroups: 8 | - seaweed.seaweedfs.com 9 | resources: 10 | - seaweeds 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - seaweed.seaweedfs.com 21 | resources: 22 | - seaweeds/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /deploy/helm/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.rbac.serviceAccount.create (ne .Values.rbac.serviceAccount.name "default") }} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "seaweedfs-operator.serviceAccountName" . }} 6 | labels: 7 | {{- include "seaweedfs-operator.labels" . | nindent 4 }} 8 | {{- with .Values.rbac.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | automountServiceAccountToken: {{ .Values.rbac.serviceAccount.automount }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | group: apiextensions.k8s.io 8 | path: spec/conversion/webhookClientConfig/service/name 9 | 10 | namespace: 11 | - kind: CustomResourceDefinition 12 | group: apiextensions.k8s.io 13 | path: spec/conversion/webhookClientConfig/service/namespace 14 | create: false 15 | 16 | varReference: 17 | - path: metadata/annotations 18 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /config/default/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | ports: 12 | - containerPort: 9443 13 | name: webhook-server 14 | protocol: TCP 15 | volumeMounts: 16 | - mountPath: /tmp/k8s-webhook-server/serving-certs 17 | name: cert 18 | readOnly: true 19 | volumes: 20 | - name: cert 21 | secret: 22 | defaultMode: 420 23 | secretName: webhook-server-cert 24 | -------------------------------------------------------------------------------- /config/default/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. 3 | apiVersion: admissionregistration.k8s.io/v1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | name: mutating-webhook-configuration 7 | annotations: 8 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 9 | --- 10 | apiVersion: admissionregistration.k8s.io/v1 11 | kind: ValidatingWebhookConfiguration 12 | metadata: 13 | name: validating-webhook-configuration 14 | annotations: 15 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 16 | -------------------------------------------------------------------------------- /config/samples/seaweedfs_s3_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "identities": [ 3 | { 4 | "name": "user1", 5 | "actions": [ 6 | "Admin" 7 | ], 8 | "credentials": [ 9 | { 10 | "accessKey": "testkey", 11 | "secretKey": "testpass" 12 | }, 13 | { 14 | "accessKey": "testkey2", 15 | "secretKey": "testpass2" 16 | } 17 | ] 18 | }, 19 | { 20 | "name": "user2", 21 | "actions": [ 22 | "Read", 23 | "List", 24 | "Write" 25 | ], 26 | "credentials": [ 27 | { 28 | "accessKey": "testkey3", 29 | "secretKey": "testpass3" 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - configmaps/status 23 | verbs: 24 | - get 25 | - update 26 | - patch 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - events 31 | verbs: 32 | - create 33 | - patch 34 | - apiGroups: 35 | - coordination.k8s.io 36 | resources: 37 | - leases 38 | verbs: 39 | - create 40 | - get 41 | - list 42 | - update 43 | -------------------------------------------------------------------------------- /internal/controller/controller_filer_configmap.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | 7 | seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 8 | ) 9 | 10 | func (r *SeaweedReconciler) createFilerConfigMap(m *seaweedv1.Seaweed) *corev1.ConfigMap { 11 | labels := labelsForFiler(m.Name) 12 | 13 | toml := "" 14 | if m.Spec.Filer.Config != nil { 15 | toml = *m.Spec.Filer.Config 16 | } 17 | 18 | dep := &corev1.ConfigMap{ 19 | ObjectMeta: metav1.ObjectMeta{ 20 | Name: m.Name + "-filer", 21 | Namespace: m.Namespace, 22 | Labels: labels, 23 | }, 24 | Data: map[string]string{ 25 | "filer.toml": toml, 26 | }, 27 | } 28 | return dep 29 | } 30 | -------------------------------------------------------------------------------- /deploy/helm/templates/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: {{ include "seaweedfs-operator.fullname" . }}-leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - configmaps/status 23 | verbs: 24 | - get 25 | - update 26 | - patch 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - events 31 | verbs: 32 | - create 33 | - patch 34 | - apiGroups: 35 | - coordination.k8s.io 36 | resources: 37 | - leases 38 | verbs: 39 | - create 40 | - get 41 | - list 42 | - update 43 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_seaweeds.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: seaweeds.seaweed.seaweedfs.com 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhook: 11 | conversionReviewVersions: 12 | - v1 13 | - v1beta 14 | clientConfig: 15 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 16 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 17 | caBundle: Cg== 18 | service: 19 | namespace: system 20 | name: webhook-service 21 | path: /convert 22 | -------------------------------------------------------------------------------- /config/samples/seaweed_v1_seaweed.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: seaweed.seaweedfs.com/v1 2 | kind: Seaweed 3 | metadata: 4 | name: seaweed1 5 | namespace: default 6 | spec: 7 | # Add fields here 8 | image: chrislusf/seaweedfs:latest 9 | volumeServerDiskCount: 1 10 | hostSuffix: seaweed.abcdefg.com 11 | master: 12 | replicas: 3 13 | volumeSizeLimitMB: 1024 14 | volume: 15 | replicas: 1 16 | requests: 17 | storage: 2Gi 18 | # Topology configuration for rack/datacenter-aware placement 19 | rack: "rack1" 20 | dataCenter: "dc1" 21 | filer: 22 | replicas: 2 23 | s3: 24 | enabled: true 25 | configSecret: 26 | name: test-secret 27 | key: seaweedfs_s3_config.json 28 | config: | 29 | [leveldb2] 30 | enabled = true 31 | dir = "/data/filerldb2" 32 | -------------------------------------------------------------------------------- /deploy/helm/templates/webhook/mutating-webhook.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.webhook.enabled }} 2 | 3 | apiVersion: admissionregistration.k8s.io/v1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | creationTimestamp: null 7 | name: mutating-webhook-configuration 8 | webhooks: 9 | - clientConfig: 10 | service: 11 | name: {{ include "seaweedfs-operator.fullname" . }}-webhook 12 | namespace: {{ .Release.Namespace }} 13 | port: 443 14 | path: {{ include "seaweedfs-operator.mutatingWebhookPath" . }} 15 | name: mseaweed.kb.io 16 | failurePolicy: Fail 17 | sideEffects: None 18 | admissionReviewVersions: 19 | - v1 20 | rules: 21 | - apiGroups: 22 | - seaweed.seaweedfs.com 23 | apiVersions: 24 | - v1 25 | operations: 26 | - CREATE 27 | - UPDATE 28 | resources: 29 | - seaweeds 30 | 31 | {{- end }} 32 | -------------------------------------------------------------------------------- /deploy/helm/templates/webhook/validating-webhook.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.webhook.enabled }} 2 | 3 | apiVersion: admissionregistration.k8s.io/v1 4 | kind: ValidatingWebhookConfiguration 5 | metadata: 6 | creationTimestamp: null 7 | name: validating-webhook-configuration 8 | webhooks: 9 | - clientConfig: 10 | service: 11 | name: {{ include "seaweedfs-operator.fullname" . }}-webhook 12 | namespace: {{ .Release.Namespace }} 13 | port: 443 14 | path: {{ include "seaweedfs-operator.validatingWebhookPath" . }} 15 | name: vseaweed.kb.io 16 | failurePolicy: Fail 17 | sideEffects: None 18 | admissionReviewVersions: 19 | - v1 20 | rules: 21 | - apiGroups: 22 | - seaweed.seaweedfs.com 23 | apiVersions: 24 | - v1 25 | operations: 26 | - CREATE 27 | - UPDATE 28 | resources: 29 | - seaweeds 30 | 31 | {{- end }} 32 | -------------------------------------------------------------------------------- /deploy/helm/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceMonitor.enabled }} 2 | 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | name: {{ include "seaweedfs-operator.fullname" . }}-metrics-monitor 7 | spec: 8 | endpoints: 9 | - port: {{ .Values.service.portName }} 10 | {{- if .Values.serviceMonitor.interval }} 11 | interval: {{ .Values.serviceMonitor.interval }} 12 | {{- end }} 13 | {{- if .Values.serviceMonitor.scrapeTimeout }} 14 | scrapeTimeout: {{ .Values.serviceMonitor.scrapeTimeout }} 15 | {{- end }} 16 | {{- if .Values.serviceMonitor.honorLabels }} 17 | honorLabels: {{ .Values.serviceMonitor.honorLabels }} 18 | {{- end }} 19 | path: /metrics 20 | selector: 21 | matchLabels: 22 | app: {{ include "seaweedfs-operator.fullname" . }} 23 | 24 | {{- end }} 25 | -------------------------------------------------------------------------------- /internal/controller/controller_master_configmap.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | 7 | seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 8 | ) 9 | 10 | func (r *SeaweedReconciler) createMasterConfigMap(m *seaweedv1.Seaweed) *corev1.ConfigMap { 11 | labels := labelsForMaster(m.Name) 12 | 13 | toml := "" 14 | if m.Spec.Master.Config != nil { 15 | toml = *m.Spec.Master.Config 16 | } 17 | 18 | dep := &corev1.ConfigMap{ 19 | ObjectMeta: metav1.ObjectMeta{ 20 | Name: m.Name + "-master", 21 | Namespace: m.Namespace, 22 | Labels: labels, 23 | }, 24 | Data: map[string]string{ 25 | "master.toml": toml, 26 | }, 27 | } 28 | // Set master instance as the owner and controller 29 | // ctrl.SetControllerReference(m, dep, r.Scheme) 30 | return dep 31 | } 32 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.5.0 14 | args: 15 | - "--secure-listen-address=0.0.0.0:8443" 16 | - "--upstream=http://127.0.0.1:8080/" 17 | - "--logtostderr=true" 18 | - "--v=10" 19 | ports: 20 | - containerPort: 8443 21 | name: https 22 | - name: manager 23 | args: 24 | - --leader-elect 25 | - --health-probe-bind-address=:8081 26 | -------------------------------------------------------------------------------- /config/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting vars. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | 14 | namespace: 15 | - kind: MutatingWebhookConfiguration 16 | group: admissionregistration.k8s.io 17 | path: webhooks/clientConfig/service/namespace 18 | create: true 19 | - kind: ValidatingWebhookConfiguration 20 | group: admissionregistration.k8s.io 21 | path: webhooks/clientConfig/service/namespace 22 | create: true 23 | 24 | varReference: 25 | - path: metadata/annotations 26 | -------------------------------------------------------------------------------- /internal/controller/controller_master_servicemonitor.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | monitorv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" 5 | seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | func (r *SeaweedReconciler) createMasterServiceMonitor(m *seaweedv1.Seaweed) *monitorv1.ServiceMonitor { 10 | labels := labelsForMaster(m.Name) 11 | 12 | dep := &monitorv1.ServiceMonitor{ 13 | ObjectMeta: metav1.ObjectMeta{ 14 | Name: m.Name + "-master", 15 | Namespace: m.Namespace, 16 | Labels: labels, 17 | }, 18 | Spec: monitorv1.ServiceMonitorSpec{ 19 | Endpoints: []monitorv1.Endpoint{ 20 | { 21 | Path: "/metrics", 22 | Port: "master-metrics", 23 | }, 24 | }, 25 | Selector: metav1.LabelSelector{ 26 | MatchLabels: labels, 27 | }, 28 | }, 29 | } 30 | 31 | return dep 32 | } 33 | -------------------------------------------------------------------------------- /internal/controller/controller_filer_servicemonitor.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | 6 | monitorv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" 7 | seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 8 | ) 9 | 10 | func (r *SeaweedReconciler) createFilerServiceMonitor(m *seaweedv1.Seaweed) *monitorv1.ServiceMonitor { 11 | labels := labelsForFiler(m.Name) 12 | 13 | dep := &monitorv1.ServiceMonitor{ 14 | ObjectMeta: metav1.ObjectMeta{ 15 | Name: m.Name + "-filer", 16 | Namespace: m.Namespace, 17 | Labels: labels, 18 | }, 19 | Spec: monitorv1.ServiceMonitorSpec{ 20 | Endpoints: []monitorv1.Endpoint{ 21 | { 22 | Path: "/metrics", 23 | Port: "filer-metrics", 24 | }, 25 | }, 26 | Selector: metav1.LabelSelector{ 27 | MatchLabels: labels, 28 | }, 29 | }, 30 | } 31 | 32 | return dep 33 | } 34 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/seaweed.seaweedfs.com_seaweeds.yaml 6 | # +kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patchesStrategicMerge: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | #- patches/webhook_in_seaweeds.yaml 12 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 13 | 14 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. 15 | # patches here are for enabling the CA injection for each CRD 16 | #- patches/cainjection_in_seaweeds.yaml 17 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch 18 | 19 | # the following config is for teaching kustomize how to do kustomization for CRDs. 20 | configurations: 21 | - kustomizeconfig.yaml 22 | -------------------------------------------------------------------------------- /config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | # WARNING: Targets CertManager 0.11 check https://docs.cert-manager.io/en/latest/tasks/upgrading/index.html for 4 | # breaking changes 5 | apiVersion: cert-manager.io/v1alpha2 6 | kind: Issuer 7 | metadata: 8 | name: selfsigned-issuer 9 | namespace: system 10 | spec: 11 | selfSigned: {} 12 | --- 13 | apiVersion: cert-manager.io/v1alpha2 14 | kind: Certificate 15 | metadata: 16 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 17 | namespace: system 18 | spec: 19 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 20 | dnsNames: 21 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 22 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 23 | issuerRef: 24 | kind: Issuer 25 | name: selfsigned-issuer 26 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 27 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | - services 12 | verbs: 13 | - create 14 | - delete 15 | - get 16 | - list 17 | - patch 18 | - update 19 | - watch 20 | - apiGroups: 21 | - "" 22 | resources: 23 | - pods 24 | verbs: 25 | - get 26 | - list 27 | - apiGroups: 28 | - apps 29 | resources: 30 | - statefulsets 31 | verbs: 32 | - create 33 | - delete 34 | - get 35 | - list 36 | - patch 37 | - update 38 | - watch 39 | - apiGroups: 40 | - extensions 41 | - networking.k8s.io 42 | resources: 43 | - ingresses 44 | verbs: 45 | - create 46 | - delete 47 | - get 48 | - list 49 | - patch 50 | - update 51 | - watch 52 | - apiGroups: 53 | - seaweed.seaweedfs.com 54 | resources: 55 | - seaweeds 56 | verbs: 57 | - create 58 | - delete 59 | - get 60 | - list 61 | - patch 62 | - update 63 | - watch 64 | - apiGroups: 65 | - seaweed.seaweedfs.com 66 | resources: 67 | - seaweeds/status 68 | verbs: 69 | - get 70 | - patch 71 | - update 72 | -------------------------------------------------------------------------------- /internal/controller/seaweed_maintenance.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | 7 | seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 8 | "github.com/seaweedfs/seaweedfs-operator/internal/controller/swadmin" 9 | ctrl "sigs.k8s.io/controller-runtime" 10 | ) 11 | 12 | func (r *SeaweedReconciler) maintenance(m *seaweedv1.Seaweed) (done bool, result ctrl.Result, err error) { 13 | 14 | masters := getMasterPeersString(m) 15 | 16 | r.Log.V(0).Info("wait to connect to masters", "masters", masters) 17 | 18 | // this step blocks since the operator can not access the masters when running from outside of the k8s cluster 19 | sa := swadmin.NewSeaweedAdmin(masters, ioutil.Discard) 20 | 21 | // For now this is an example of the admin commands 22 | // master by default has some maintenance commands already. 23 | r.Log.V(0).Info("volume.list") 24 | sa.Output = os.Stdout 25 | if err := sa.ProcessCommand("volume.list"); err != nil { 26 | r.Log.V(0).Info("volume.list", "error", err) 27 | } 28 | 29 | sa.ProcessCommand("lock") 30 | if err := sa.ProcessCommand("volume.balance -force"); err != nil { 31 | r.Log.V(0).Info("volume.balance", "error", err) 32 | } 33 | sa.ProcessCommand("unlock") 34 | 35 | return ReconcileResult(nil) 36 | 37 | } 38 | -------------------------------------------------------------------------------- /config/webhook/manifests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: admissionregistration.k8s.io/v1 3 | kind: MutatingWebhookConfiguration 4 | metadata: 5 | name: mutating-webhook-configuration 6 | webhooks: 7 | - admissionReviewVersions: 8 | - v1 9 | clientConfig: 10 | service: 11 | name: webhook-service 12 | namespace: system 13 | path: /mutate-seaweed-seaweedfs-com-v1-seaweed 14 | failurePolicy: Fail 15 | name: mseaweed.kb.io 16 | rules: 17 | - apiGroups: 18 | - seaweed.seaweedfs.com 19 | apiVersions: 20 | - v1 21 | operations: 22 | - CREATE 23 | - UPDATE 24 | resources: 25 | - seaweeds 26 | sideEffects: None 27 | --- 28 | apiVersion: admissionregistration.k8s.io/v1 29 | kind: ValidatingWebhookConfiguration 30 | metadata: 31 | name: validating-webhook-configuration 32 | webhooks: 33 | - admissionReviewVersions: 34 | - v1 35 | clientConfig: 36 | service: 37 | name: webhook-service 38 | namespace: system 39 | path: /validate-seaweed-seaweedfs-com-v1-seaweed 40 | failurePolicy: Fail 41 | name: vseaweed.kb.io 42 | rules: 43 | - apiGroups: 44 | - seaweed.seaweedfs.com 45 | apiVersions: 46 | - v1 47 | operations: 48 | - CREATE 49 | - UPDATE 50 | resources: 51 | - seaweeds 52 | sideEffects: None 53 | -------------------------------------------------------------------------------- /.github/workflows/make-test.yaml: -------------------------------------------------------------------------------- 1 | name: Make run tests 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - labeled 8 | - synchronize 9 | paths: 10 | - '**/*.go' 11 | - 'go.mod' 12 | - 'go.sum' 13 | - 'Makefile' 14 | 15 | jobs: 16 | test: 17 | name: test on k8s ${{ matrix.k8s.attribute }} version 18 | # Pull request has label 'ok-to-test' or the author is a member of the organization 19 | if: contains(github.event.pull_request.labels.*.name, 'ok-to-test') || contains(fromJSON('["COLLABORATOR", "MEMBER", "OWNER"]'), github.event.pull_request.author_association) 20 | strategy: 21 | matrix: 22 | k8s: 23 | - version: v1.28.0 24 | attribute: penultimate 25 | - version: v1.29.0 26 | attribute: previous 27 | - version: default 28 | attribute: latest 29 | runs-on: ubuntu-22.04 30 | steps: 31 | - uses: actions/checkout@v4.1.7 32 | - uses: actions/setup-go@v5.0.1 33 | with: 34 | go-version: '1.24' 35 | - run: | 36 | if [ "${{ matrix.k8s.version }}" = "default" ]; then 37 | # For latest version use default from Makefile 38 | make test 39 | else 40 | K8S_VERSION=${{ matrix.k8s.version }} make test 41 | fi 42 | -------------------------------------------------------------------------------- /api/v1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1 contains API Schema definitions for the seaweed v1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=seaweed.seaweedfs.com 20 | package v1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "seaweed.seaweedfs.com", Version: "v1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /config/samples/seaweed_v1_seaweed_with_iam_embedded.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: seaweed.seaweedfs.com/v1 2 | kind: Seaweed 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: seaweed 6 | app.kubernetes.io/instance: seaweed-sample 7 | app.kubernetes.io/part-of: seaweedfs-operator 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: seaweedfs-operator 10 | name: seaweed-sample 11 | spec: 12 | image: chrislusf/seaweedfs:latest 13 | 14 | # Master servers 15 | master: 16 | replicas: 1 17 | volumeSizeLimitMB: 1024 18 | 19 | # Volume servers 20 | volume: 21 | replicas: 1 22 | 23 | # Filer servers with S3 and embedded IAM (recommended) 24 | # IAM API is now embedded in S3 by default (on the same port as S3: 8333) 25 | # This follows the pattern used by MinIO and Ceph RGW 26 | filer: 27 | replicas: 1 28 | s3: 29 | enabled: true 30 | # iam: true # Optional: IAM is enabled by default when S3 is enabled 31 | # # Set to false to explicitly disable embedded IAM 32 | requests: 33 | memory: "128Mi" 34 | cpu: "100m" 35 | limits: 36 | memory: "256Mi" 37 | cpu: "200m" 38 | service: 39 | type: ClusterIP 40 | 41 | # Note: The IAM API is accessible on the same port as S3 (8333) 42 | # No separate IAM service needed when using embedded IAM 43 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.24 AS builder 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | WORKDIR /workspace 7 | # Copy the Go Modules manifests 8 | COPY go.mod go.mod 9 | COPY go.sum go.sum 10 | # cache deps before building and copying source so that we don't need to re-download as much 11 | # and so that source changes don't invalidate our downloaded layer 12 | RUN go mod download 13 | 14 | # Copy the go source 15 | COPY cmd/main.go cmd/main.go 16 | COPY api/ api/ 17 | COPY internal/controller/ internal/controller/ 18 | 19 | # Build 20 | # the GOARCH has not a default value to allow the binary be built according to the host where the command 21 | # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 22 | # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 23 | # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 24 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go 25 | 26 | # Use distroless as minimal base image to package the manager binary 27 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 28 | FROM gcr.io/distroless/static:nonroot 29 | WORKDIR / 30 | COPY --from=builder /workspace/manager . 31 | USER 65532:65532 32 | 33 | ENTRYPOINT ["/manager"] 34 | -------------------------------------------------------------------------------- /internal/controller/label/label.go: -------------------------------------------------------------------------------- 1 | package label 2 | 3 | const ( 4 | // The following labels are recommended by kubernetes https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/ 5 | 6 | // ManagedByLabelKey is Kubernetes recommended label key, it represents the tool being used to manage the operation of an application 7 | // For resources managed by SeaweedFS Operator, its value is always seaweedfs-operator 8 | ManagedByLabelKey string = "app.kubernetes.io/managed-by" 9 | // ComponentLabelKey is Kubernetes recommended label key, it represents the component within the architecture 10 | ComponentLabelKey string = "app.kubernetes.io/component" 11 | // NameLabelKey is Kubernetes recommended label key, it represents the name of the application 12 | NameLabelKey string = "app.kubernetes.io/name" 13 | // InstanceLabelKey is Kubernetes recommended label key, it represents a unique name identifying the instance of an application 14 | // It's set by helm when installing a release 15 | InstanceLabelKey string = "app.kubernetes.io/instance" 16 | // VersionLabelKey is Kubernetes recommended label key, it represents the version of the app 17 | VersionLabelKey string = "app.kubernetes.io/version" 18 | 19 | // PodName is to select pod by name 20 | // https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#pod-selector 21 | PodName string = "statefulset.kubernetes.io/pod-name" 22 | ) 23 | -------------------------------------------------------------------------------- /internal/controller/controller_ingress.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/seaweedfs/seaweedfs-operator/internal/controller/label" 5 | ctrl "sigs.k8s.io/controller-runtime" 6 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 7 | 8 | seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 9 | ) 10 | 11 | func (r *SeaweedReconciler) ensureSeaweedIngress(seaweedCR *seaweedv1.Seaweed) (done bool, result ctrl.Result, err error) { 12 | 13 | if seaweedCR.Spec.HostSuffix != nil && len(*seaweedCR.Spec.HostSuffix) != 0 { 14 | if done, result, err = r.ensureAllIngress(seaweedCR); done { 15 | return 16 | } 17 | } 18 | 19 | return 20 | } 21 | 22 | func (r *SeaweedReconciler) ensureAllIngress(seaweedCR *seaweedv1.Seaweed) (bool, ctrl.Result, error) { 23 | log := r.Log.WithValues("sw-ingress", seaweedCR.Name) 24 | 25 | ingressService := r.createAllIngress(seaweedCR) 26 | if err := controllerutil.SetControllerReference(seaweedCR, ingressService, r.Scheme); err != nil { 27 | return ReconcileResult(err) 28 | } 29 | _, err := r.CreateOrUpdateIngress(ingressService) 30 | 31 | log.Info("ensure ingress " + ingressService.Name) 32 | return ReconcileResult(err) 33 | } 34 | 35 | func labelsForIngress(name string) map[string]string { 36 | return map[string]string{ 37 | label.ManagedByLabelKey: "seaweedfs-operator", 38 | label.NameLabelKey: "seaweedfs", 39 | label.ComponentLabelKey: "ingress", 40 | label.InstanceLabelKey: name, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary Build Files 2 | build/_output 3 | build/_test 4 | # Created by https://www.gitignore.io/api/go,vim,emacs,visualstudiocode 5 | ### Emacs ### 6 | # -*- mode: gitignore; -*- 7 | *~ 8 | \#*\# 9 | /.emacs.desktop 10 | /.emacs.desktop.lock 11 | *.elc 12 | auto-save-list 13 | tramp 14 | .\#* 15 | # Org-mode 16 | .org-id-locations 17 | *_archive 18 | # flymake-mode 19 | *_flymake.* 20 | # eshell files 21 | /eshell/history 22 | /eshell/lastdir 23 | # elpa packages 24 | /elpa/ 25 | # reftex files 26 | *.rel 27 | # AUCTeX auto folder 28 | /auto/ 29 | # cask packages 30 | .cask/ 31 | dist/ 32 | # Flycheck 33 | flycheck_*.el 34 | # server auth directory 35 | /server/ 36 | # projectiles files 37 | .projectile 38 | projectile-bookmarks.eld 39 | # directory configuration 40 | .dir-locals.el 41 | # saveplace 42 | places 43 | # url cache 44 | url/cache/ 45 | # cedet 46 | ede-projects.el 47 | # smex 48 | smex-items 49 | # company-statistics 50 | company-statistics-cache.el 51 | # anaconda-mode 52 | anaconda-mode/ 53 | ### Go ### 54 | # Binaries for programs and plugins 55 | *.exe 56 | *.exe~ 57 | *.dll 58 | *.so 59 | *.dylib 60 | # Test binary, build with 'go test -c' 61 | *.test 62 | # Output of the go coverage tool, specifically when used with LiteIDE 63 | *.out 64 | ### Vim ### 65 | # swap 66 | .sw[a-p] 67 | .*.sw[a-p] 68 | # session 69 | Session.vim 70 | # temporary 71 | .netrwhist 72 | # auto-generated tag files 73 | tags 74 | ### VisualStudioCode ### 75 | .vscode/* 76 | .history 77 | # End of https://www.gitignore.io/api/go,vim,emacs,visualstudiocode 78 | 79 | ### GoLand ### 80 | .idea 81 | 82 | testbin 83 | bin 84 | dist 85 | -------------------------------------------------------------------------------- /.github/workflows/make-test-e2e.yaml: -------------------------------------------------------------------------------- 1 | #name: Make run tests e2e 2 | # 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - labeled 8 | - synchronize 9 | paths: 10 | - '**/*.go' 11 | - 'go.mod' 12 | - 'go.sum' 13 | - 'Makefile' 14 | - 'Dockerfile' 15 | # 16 | jobs: 17 | test-e2e: 18 | name: test-e2e on k8s ${{ matrix.k8s.attribute }} version 19 | # Pull request has label 'ok-to-test' or the author is a member of the organization 20 | if: contains(github.event.pull_request.labels.*.name, 'ok-to-test') || contains(fromJSON('["COLLABORATOR", "MEMBER", "OWNER"]'), github.event.pull_request.author_association) 21 | strategy: 22 | matrix: 23 | k8s: 24 | - version: v1.28.0 25 | attribute: penultimate 26 | - version: v1.29.0 27 | attribute: previous 28 | - version: default 29 | attribute: latest 30 | runs-on: ubuntu-22.04 31 | steps: 32 | - uses: actions/checkout@v4.1.7 33 | - uses: actions/setup-go@v5.0.1 34 | with: 35 | go-version: '1.24' 36 | - uses: docker/setup-buildx-action@v3.3.0 37 | - uses: tale/kubectl-action@v1.4.0 38 | with: 39 | kubectl-version: v1.30.0 40 | # Empty kubeconfig file 41 | base64-kube-config: "YXBpVmVyc2lvbjogdjEKa2luZDogQ29uZmlnCnByZWZlcmVuY2VzOiB7fQo=" 42 | - run: | 43 | if [ "${{ matrix.k8s.version }}" = "default" ]; then 44 | # For latest version use default from Makefile 45 | make test-e2e 46 | else 47 | K8S_VERSION=${{ matrix.k8s.version }} make test-e2e 48 | fi 49 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | name: system 7 | --- 8 | apiVersion: apps/v1 9 | kind: Deployment 10 | metadata: 11 | name: controller-manager 12 | namespace: system 13 | labels: 14 | control-plane: controller-manager 15 | spec: 16 | selector: 17 | matchLabels: 18 | control-plane: controller-manager 19 | replicas: 1 20 | template: 21 | metadata: 22 | labels: 23 | control-plane: controller-manager 24 | spec: 25 | containers: 26 | - command: 27 | - /manager 28 | args: 29 | - --leader-elect 30 | - --health-probe-bind-address=:8081 31 | image: controller:latest 32 | imagePullPolicy: IfNotPresent 33 | env: 34 | - name: ENABLE_WEBHOOKS 35 | value: "false" 36 | name: manager 37 | livenessProbe: 38 | httpGet: 39 | path: /healthz 40 | port: 8081 41 | initialDelaySeconds: 15 42 | periodSeconds: 20 43 | readinessProbe: 44 | httpGet: 45 | path: /readyz 46 | port: 8081 47 | initialDelaySeconds: 5 48 | periodSeconds: 10 49 | # TODO(user): Configure the resources accordingly based on the project requirements. 50 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 51 | resources: 52 | limits: 53 | cpu: 500m 54 | memory: 128Mi 55 | requests: 56 | cpu: 10m 57 | memory: 64Mi 58 | terminationGracePeriodSeconds: 10 59 | -------------------------------------------------------------------------------- /internal/controller/controller_volume_servicemonitor.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | 6 | monitorv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" 7 | seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 8 | ) 9 | 10 | func (r *SeaweedReconciler) createVolumeServerServiceMonitor(m *seaweedv1.Seaweed) *monitorv1.ServiceMonitor { 11 | labels := labelsForVolumeServer(m.Name) 12 | 13 | dep := &monitorv1.ServiceMonitor{ 14 | ObjectMeta: metav1.ObjectMeta{ 15 | Name: m.Name + "-volume", 16 | Namespace: m.Namespace, 17 | Labels: labels, 18 | }, 19 | Spec: monitorv1.ServiceMonitorSpec{ 20 | Endpoints: []monitorv1.Endpoint{ 21 | { 22 | Path: "/metrics", 23 | Port: "volume-metrics", 24 | }, 25 | }, 26 | Selector: metav1.LabelSelector{ 27 | MatchLabels: labels, 28 | }, 29 | }, 30 | } 31 | 32 | return dep 33 | } 34 | 35 | func (r *SeaweedReconciler) createVolumeServerTopologyServiceMonitor(m *seaweedv1.Seaweed, topologyName string, topologySpec *seaweedv1.VolumeTopologySpec) *monitorv1.ServiceMonitor { 36 | labels := labelsForVolumeServerTopology(m.Name, topologyName) 37 | labels["seaweedfs/service-role"] = "peer" 38 | 39 | dep := &monitorv1.ServiceMonitor{ 40 | ObjectMeta: metav1.ObjectMeta{ 41 | Name: m.Name + "-volume-" + topologyName, 42 | Namespace: m.Namespace, 43 | Labels: labels, 44 | }, 45 | Spec: monitorv1.ServiceMonitorSpec{ 46 | Endpoints: []monitorv1.Endpoint{ 47 | { 48 | Path: "/metrics", 49 | Port: "volume-metrics", 50 | }, 51 | }, 52 | Selector: metav1.LabelSelector{ 53 | MatchLabels: labels, 54 | }, 55 | }, 56 | } 57 | 58 | return dep 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/image.yml: -------------------------------------------------------------------------------- 1 | name: Create and publish Docker image 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - '*' 9 | 10 | 11 | jobs: 12 | build-and-push-image: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | steps: 18 | 19 | - name: Checkout repository 20 | uses: actions/checkout@v3 21 | 22 | - name: Set up QEMU (for cross-platform builds) 23 | uses: docker/setup-qemu-action@v3 24 | 25 | - name: Set up Docker Buildx 26 | uses: docker/setup-buildx-action@v3 27 | 28 | - name: Login into GitHub Container Registry 29 | uses: docker/login-action@v2 30 | with: 31 | registry: ghcr.io 32 | username: ${{ github.actor }} 33 | password: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: Login into Docker Hub 36 | uses: docker/login-action@v2 37 | with: 38 | username: ${{ secrets.DOCKERHUB_USERNAME }} 39 | password: ${{ secrets.DOCKERHUB_TOKEN }} 40 | 41 | - name: Extract metadata (tags, labels) for Docker 42 | id: meta 43 | uses: docker/metadata-action@v4 44 | with: 45 | images: | 46 | chrislusf/seaweedfs-operator 47 | ghcr.io/seaweedfs/seaweedfs-operator 48 | tags: | 49 | type=ref,event=branch 50 | type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} 51 | type=ref,event=tag 52 | 53 | - name: Build and push Docker image 54 | uses: docker/build-push-action@v3 55 | with: 56 | context: . 57 | push: true 58 | platforms: linux/amd64, linux/arm, linux/arm64 59 | tags: ${{ steps.meta.outputs.tags }} 60 | labels: ${{ steps.meta.outputs.labels }} 61 | -------------------------------------------------------------------------------- /internal/controller/swadmin/seaweed_admin.go: -------------------------------------------------------------------------------- 1 | package swadmin 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "regexp" 8 | "strings" 9 | 10 | "google.golang.org/grpc/credentials/insecure" 11 | 12 | "github.com/seaweedfs/seaweedfs/weed/shell" 13 | "google.golang.org/grpc" 14 | ) 15 | 16 | type SeaweedAdmin struct { 17 | commandReg *regexp.Regexp 18 | commandEnv *shell.CommandEnv 19 | Output io.Writer 20 | } 21 | 22 | func NewSeaweedAdmin(masters string, output io.Writer) *SeaweedAdmin { 23 | var shellOptions shell.ShellOptions 24 | shellOptions.GrpcDialOption = grpc.WithTransportCredentials(insecure.NewCredentials()) 25 | shellOptions.Masters = &masters 26 | 27 | commandEnv := shell.NewCommandEnv(&shellOptions) 28 | reg, _ := regexp.Compile(`'.*?'|".*?"|\S+`) 29 | 30 | go commandEnv.MasterClient.KeepConnectedToMaster(context.Background()) 31 | 32 | return &SeaweedAdmin{ 33 | commandEnv: commandEnv, 34 | commandReg: reg, 35 | Output: output, 36 | } 37 | } 38 | 39 | // ProcessCommands cmds can be semi-colon separated commands 40 | func (sa *SeaweedAdmin) ProcessCommands(cmds string) error { 41 | for _, c := range strings.Split(cmds, ";") { 42 | if err := sa.ProcessCommand(c); err != nil { 43 | return err 44 | } 45 | } 46 | return nil 47 | } 48 | 49 | func (sa *SeaweedAdmin) ProcessCommand(cmd string) error { 50 | sa.commandEnv.MasterClient.WaitUntilConnected(context.Background()) 51 | cmds := sa.commandReg.FindAllString(cmd, -1) 52 | if len(cmds) == 0 { 53 | return nil 54 | } 55 | 56 | args := make([]string, len(cmds[1:])) 57 | 58 | for i := range args { 59 | args[i] = strings.Trim(string(cmds[1+i]), "\"'") 60 | } 61 | 62 | for _, c := range shell.Commands { 63 | if c.Name() == cmds[0] || c.Name() == "fs."+cmds[0] { 64 | return c.Do(args, sa.commandEnv, sa.Output) 65 | } 66 | } 67 | 68 | return fmt.Errorf("unknown command: %v", cmd) 69 | 70 | } 71 | -------------------------------------------------------------------------------- /deploy/helm/templates/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | creationTimestamp: null 7 | name: {{ include "seaweedfs-operator.fullname" . }}-manager-role 8 | rules: 9 | - apiGroups: 10 | - apps 11 | resources: 12 | - statefulsets 13 | verbs: 14 | - create 15 | - delete 16 | - get 17 | - list 18 | - patch 19 | - update 20 | - watch 21 | - apiGroups: 22 | - "" 23 | resources: 24 | - configmaps 25 | verbs: 26 | - create 27 | - delete 28 | - get 29 | - list 30 | - patch 31 | - update 32 | - watch 33 | - apiGroups: 34 | - "" 35 | resources: 36 | - pods 37 | verbs: 38 | - get 39 | - watch 40 | - list 41 | - apiGroups: 42 | - "" 43 | resources: 44 | - services 45 | verbs: 46 | - create 47 | - delete 48 | - get 49 | - list 50 | - patch 51 | - update 52 | - watch 53 | - apiGroups: 54 | - extensions 55 | resources: 56 | - ingresses 57 | verbs: 58 | - create 59 | - delete 60 | - get 61 | - list 62 | - patch 63 | - update 64 | - watch 65 | - apiGroups: 66 | - networking.k8s.io 67 | resources: 68 | - ingresses 69 | verbs: 70 | - create 71 | - delete 72 | - get 73 | - list 74 | - patch 75 | - update 76 | - watch 77 | - apiGroups: 78 | - seaweed.seaweedfs.com 79 | resources: 80 | - seaweeds 81 | verbs: 82 | - create 83 | - delete 84 | - get 85 | - list 86 | - patch 87 | - update 88 | - watch 89 | - apiGroups: 90 | - seaweed.seaweedfs.com 91 | resources: 92 | - seaweeds/status 93 | verbs: 94 | - get 95 | - patch 96 | - update 97 | {{- if .Values.serviceMonitor.enabled }} 98 | - apiGroups: 99 | - monitoring.coreos.com 100 | resources: 101 | - servicemonitors 102 | verbs: 103 | - get 104 | - list 105 | - watch 106 | - patch 107 | - update 108 | - create 109 | - delete 110 | {{- end }} 111 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v3.2.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | exclude: "^deploy/helm/(values.schema.json|README.md)$" 10 | - id: check-added-large-files 11 | - repo: local 12 | hooks: 13 | - id: make-fmt 14 | name: make-fmt 15 | entry: sh -c "make fmt" 16 | language: system 17 | - id: make-vet 18 | name: make-vet 19 | entry: sh -c "make vet" 20 | language: system 21 | require_serial: true 22 | - id: make-nilaway-lint 23 | name: make-nilaway-lint 24 | entry: sh -c "make nilaway-lint" 25 | language: system 26 | require_serial: true 27 | # - id: make-lint-fix 28 | # name: make-lint-fix 29 | # entry: sh -c "make lint-fix" 30 | # language: system 31 | # require_serial: true 32 | # - id: make-generate-docs 33 | # name: make-generate-docs 34 | # entry: sh -c "make generate-docs" 35 | # language: system 36 | # require_serial: true 37 | # - id: make-mod-tidy 38 | # name: make-mod-tidy 39 | # entry: sh -c "make mod-tidy" 40 | # language: system 41 | # require_serial: true 42 | # - id: make-helm-lint 43 | # name: make-helm-lint 44 | # entry: sh -c "make helm-lint" 45 | # language: system 46 | # require_serial: true 47 | # - id: make-helm-schema-run 48 | # name: make-helm-schema-run 49 | # entry: sh -c "make helm-schema-run" 50 | # language: system 51 | # require_serial: true 52 | # - id: make-helm-docs-run 53 | # name: make-helm-docs-run 54 | # entry: sh -c "make helm-docs-run" 55 | # language: system 56 | # require_serial: true 57 | # - id: make-helm-crd-copy 58 | # name: make-helm-crd-copy 59 | # entry: sh -c "make helm-crd-copy" 60 | # language: system 61 | # require_serial: true 62 | -------------------------------------------------------------------------------- /test/e2e/e2e_suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package e2e 18 | 19 | import ( 20 | "fmt" 21 | "os/exec" 22 | "testing" 23 | 24 | . "github.com/onsi/ginkgo/v2" 25 | . "github.com/onsi/gomega" 26 | "github.com/seaweedfs/seaweedfs-operator/test/utils" 27 | ) 28 | 29 | // Run e2e tests using the Ginkgo runner. 30 | func TestE2E(t *testing.T) { 31 | RegisterFailHandler(Fail) 32 | fmt.Fprintf(GinkgoWriter, "Starting seaweedfs-operator suite\n") 33 | RunSpecs(t, "e2e suite") 34 | } 35 | 36 | var _ = BeforeSuite(func() { 37 | By("prepare kind environment", func() { 38 | cmd := exec.Command("make", "kind-prepare") 39 | _, err := utils.Run(cmd) 40 | Expect(err).NotTo(HaveOccurred()) 41 | }) 42 | 43 | By("upload latest image to kind cluster", func() { 44 | cmd := exec.Command("make", "kind-load") 45 | _, err := utils.Run(cmd) 46 | Expect(err).NotTo(HaveOccurred()) 47 | }) 48 | 49 | By("install CRDs", func() { 50 | cmd := exec.Command("make", "install") 51 | _, err := utils.Run(cmd) 52 | Expect(err).NotTo(HaveOccurred()) 53 | }) 54 | 55 | By("deploy controller-manager", func() { 56 | cmd := exec.Command("make", "deploy") 57 | _, err := utils.Run(cmd) 58 | Expect(err).NotTo(HaveOccurred()) 59 | }) 60 | }) 61 | 62 | var _ = AfterSuite(func() { 63 | By("cleanup", func() { 64 | cmd := exec.Command("make", "undeploy") 65 | _, err := utils.Run(cmd) 66 | Expect(err).NotTo(HaveOccurred()) 67 | 68 | cmd = exec.Command("make", "uninstall") 69 | _, err = utils.Run(cmd) 70 | if err != nil { 71 | // Ignore uninstall errors - CRDs might already be gone 72 | fmt.Printf("Warning: uninstall failed (this is often harmless): %v\n", err) 73 | } 74 | }) 75 | }) 76 | -------------------------------------------------------------------------------- /config/samples/seaweed_v1_migration_example.yaml: -------------------------------------------------------------------------------- 1 | # Example: Migration from legacy volume section to volumeTopology 2 | # This shows how to disable the old volume section and use the new topology approach 3 | 4 | apiVersion: seaweed.seaweedfs.com/v1 5 | kind: Seaweed 6 | metadata: 7 | name: seaweed-migration-example 8 | namespace: default 9 | spec: 10 | image: chrislusf/seaweedfs:latest 11 | volumeServerDiskCount: 1 12 | 13 | # Master configuration 14 | master: 15 | replicas: 3 16 | volumeSizeLimitMB: 1024 17 | defaultReplication: "210" # Enable 210 replication 18 | 19 | # DISABLE legacy volume section by setting replicas to 0 20 | # This is now allowed after the validation fix 21 | volume: 22 | replicas: 0 # ✅ Now valid! Previously required minimum: 1 23 | requests: 24 | storage: 10Gi 25 | # Note: All volume server settings here are inherited by topology groups 26 | # Including resources, storageClass, volume server config, service, etc. 27 | # Topology groups only need to specify overrides for different settings 28 | 29 | # NEW: Use volumeTopology for structured deployment 30 | volumeTopology: 31 | dc1-rack1: 32 | replicas: 3 33 | rack: "rack1" 34 | dataCenter: "dc1" 35 | nodeSelector: 36 | topology.kubernetes.io/zone: "us-west-2a" 37 | seaweedfs/datacenter: "dc1" 38 | seaweedfs/rack: "rack1" 39 | requests: 40 | storage: 10Gi 41 | cpu: "1" 42 | memory: "2Gi" 43 | limits: 44 | cpu: "2" 45 | memory: "4Gi" 46 | 47 | dc1-rack2: 48 | replicas: 2 49 | rack: "rack2" 50 | dataCenter: "dc1" 51 | nodeSelector: 52 | topology.kubernetes.io/zone: "us-west-2b" 53 | seaweedfs/datacenter: "dc1" 54 | seaweedfs/rack: "rack2" 55 | requests: 56 | storage: 10Gi 57 | cpu: "500m" 58 | memory: "1.5Gi" 59 | limits: 60 | cpu: "1.5" 61 | memory: "3Gi" 62 | 63 | dc2-rack1: 64 | replicas: 3 65 | requests: 66 | storage: 15Gi # Different storage size for this datacenter 67 | rack: "rack1" 68 | dataCenter: "dc2" 69 | nodeSelector: 70 | topology.kubernetes.io/zone: "us-east-1a" 71 | seaweedfs/datacenter: "dc2" 72 | seaweedfs/rack: "rack1" 73 | storageClassName: "fast-ssd" # Use faster storage 74 | compactionMBps: 100 75 | maxVolumeCounts: 100 76 | 77 | # Filer configuration 78 | filer: 79 | replicas: 2 80 | s3: 81 | enabled: true 82 | config: | 83 | [leveldb2] 84 | enabled = true 85 | dir = "/data/filerldb2" 86 | -------------------------------------------------------------------------------- /config/samples/seaweed_v1_seaweed_topology.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: seaweed.seaweedfs.com/v1 2 | kind: Seaweed 3 | metadata: 4 | name: seaweed-topology 5 | namespace: default 6 | spec: 7 | # SeaweedFS image 8 | image: chrislusf/seaweedfs:latest 9 | volumeServerDiskCount: 1 10 | 11 | # Master configuration with 210 replication 12 | master: 13 | replicas: 3 14 | volumeSizeLimitMB: 1024 15 | # Set default replication to 210 (2 copies in different datacenters, 1 copy in different rack, 0 copies in same rack) 16 | defaultReplication: "210" 17 | 18 | # Volume server configuration with topology information 19 | volume: 20 | replicas: 6 # Deploy 6 volume servers across different racks/datacenters 21 | requests: 22 | storage: 10Gi 23 | # Topology configuration 24 | rack: "rack1" 25 | dataCenter: "dc1" 26 | # Node selector to ensure pods are placed on nodes with appropriate labels 27 | nodeSelector: 28 | seaweedfs/datacenter: "dc1" 29 | seaweedfs/rack: "rack1" 30 | 31 | # Filer configuration 32 | filer: 33 | replicas: 2 34 | s3: 35 | enabled: true 36 | config: | 37 | [leveldb2] 38 | enabled = true 39 | dir = "/data/filerldb2" 40 | 41 | --- 42 | # Additional Seaweed instances for other racks/datacenters 43 | apiVersion: seaweed.seaweedfs.com/v1 44 | kind: Seaweed 45 | metadata: 46 | name: seaweed-topology-dc1-rack2 47 | namespace: default 48 | spec: 49 | image: chrislusf/seaweedfs:latest 50 | volumeServerDiskCount: 1 51 | 52 | # Only deploy volume servers for this instance 53 | master: 54 | replicas: 0 # Don't deploy masters, use the main instance 55 | 56 | volume: 57 | replicas: 3 58 | requests: 59 | storage: 10Gi 60 | rack: "rack2" 61 | dataCenter: "dc1" 62 | nodeSelector: 63 | seaweedfs/datacenter: "dc1" 64 | seaweedfs/rack: "rack2" 65 | 66 | filer: 67 | replicas: 0 # Don't deploy filers, use the main instance 68 | 69 | --- 70 | apiVersion: seaweed.seaweedfs.com/v1 71 | kind: Seaweed 72 | metadata: 73 | name: seaweed-topology-dc2-rack1 74 | namespace: default 75 | spec: 76 | image: chrislusf/seaweedfs:latest 77 | volumeServerDiskCount: 1 78 | 79 | # Only deploy volume servers for this instance 80 | master: 81 | replicas: 0 # Don't deploy masters, use the main instance 82 | 83 | volume: 84 | replicas: 3 85 | requests: 86 | storage: 10Gi 87 | rack: "rack1" 88 | dataCenter: "dc2" 89 | nodeSelector: 90 | seaweedfs/datacenter: "dc2" 91 | seaweedfs/rack: "rack1" 92 | 93 | filer: 94 | replicas: 0 # Don't deploy filers, use the main instance 95 | -------------------------------------------------------------------------------- /internal/controller/helper_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "testing" 5 | 6 | corev1 "k8s.io/api/core/v1" 7 | "k8s.io/apimachinery/pkg/api/resource" 8 | ) 9 | 10 | func TestFilterContainerResources(t *testing.T) { 11 | // Test with various resource types 12 | input := corev1.ResourceRequirements{ 13 | Requests: corev1.ResourceList{ 14 | corev1.ResourceCPU: resource.MustParse("500m"), 15 | corev1.ResourceMemory: resource.MustParse("1Gi"), 16 | corev1.ResourceStorage: resource.MustParse("10Gi"), 17 | corev1.ResourceEphemeralStorage: resource.MustParse("1Gi"), 18 | }, 19 | Limits: corev1.ResourceList{ 20 | corev1.ResourceCPU: resource.MustParse("1000m"), 21 | corev1.ResourceMemory: resource.MustParse("2Gi"), 22 | corev1.ResourceStorage: resource.MustParse("20Gi"), 23 | corev1.ResourceEphemeralStorage: resource.MustParse("2Gi"), 24 | }, 25 | } 26 | 27 | filtered := filterContainerResources(input) 28 | 29 | // Verify storage is removed from requests 30 | if _, exists := filtered.Requests[corev1.ResourceStorage]; exists { 31 | t.Errorf("Expected storage to be filtered out from requests") 32 | } 33 | 34 | // Verify storage is removed from limits 35 | if _, exists := filtered.Limits[corev1.ResourceStorage]; exists { 36 | t.Errorf("Expected storage to be filtered out from limits") 37 | } 38 | 39 | // Verify other resources are preserved 40 | expectedResources := []corev1.ResourceName{ 41 | corev1.ResourceCPU, 42 | corev1.ResourceMemory, 43 | corev1.ResourceEphemeralStorage, 44 | } 45 | 46 | for _, resource := range expectedResources { 47 | if _, exists := filtered.Requests[resource]; !exists { 48 | t.Errorf("Expected %s to be preserved in requests", resource) 49 | } 50 | if _, exists := filtered.Limits[resource]; !exists { 51 | t.Errorf("Expected %s to be preserved in limits", resource) 52 | } 53 | } 54 | 55 | // Verify values are correct 56 | if !filtered.Requests[corev1.ResourceCPU].Equal(resource.MustParse("500m")) { 57 | t.Errorf("CPU request value mismatch") 58 | } 59 | if !filtered.Limits[corev1.ResourceMemory].Equal(resource.MustParse("2Gi")) { 60 | t.Errorf("Memory limit value mismatch") 61 | } 62 | } 63 | 64 | func TestFilterContainerResourcesEmpty(t *testing.T) { 65 | // Test with empty ResourceRequirements 66 | input := corev1.ResourceRequirements{} 67 | filtered := filterContainerResources(input) 68 | 69 | if filtered.Requests != nil { 70 | t.Errorf("Expected empty requests to remain nil") 71 | } 72 | if filtered.Limits != nil { 73 | t.Errorf("Expected empty limits to remain nil") 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: seaweedfs-operator-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: seaweedfs-operator- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | - ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 20 | # crd/kustomization.yaml 21 | #- ../webhook 22 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 23 | #- ../certmanager 24 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 25 | #- ../prometheus 26 | 27 | patchesStrategicMerge: 28 | # Protect the /metrics endpoint by putting it behind auth. 29 | # If you want your controller-manager to expose the /metrics 30 | # endpoint w/o any authn/z, please comment the following line. 31 | - manager_auth_proxy_patch.yaml 32 | 33 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 34 | # crd/kustomization.yaml 35 | #- manager_webhook_patch.yaml 36 | 37 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 38 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 39 | # 'CERTMANAGER' needs to be enabled to use ca injection 40 | #- webhookcainjection_patch.yaml 41 | 42 | # the following config is for teaching kustomize how to do var substitution 43 | vars: 44 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 45 | #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 46 | # objref: 47 | # kind: Certificate 48 | # group: cert-manager.io 49 | # version: v1alpha2 50 | # name: serving-cert # this name should match the one in certificate.yaml 51 | # fieldref: 52 | # fieldpath: metadata.namespace 53 | #- name: CERTIFICATE_NAME 54 | # objref: 55 | # kind: Certificate 56 | # group: cert-manager.io 57 | # version: v1alpha2 58 | # name: serving-cert # this name should match the one in certificate.yaml 59 | #- name: SERVICE_NAMESPACE # namespace of the service 60 | # objref: 61 | # kind: Service 62 | # version: v1 63 | # name: webhook-service 64 | # fieldref: 65 | # fieldpath: metadata.namespace 66 | #- name: SERVICE_NAME 67 | # objref: 68 | # kind: Service 69 | # version: v1 70 | # name: webhook-service 71 | -------------------------------------------------------------------------------- /test/e2e/e2e_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package e2e 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "net/http" 23 | "strconv" 24 | "time" 25 | 26 | "sigs.k8s.io/controller-runtime/pkg/client/config" 27 | 28 | "github.com/go-resty/resty/v2" 29 | . "github.com/onsi/ginkgo/v2" 30 | . "github.com/onsi/gomega" 31 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 | 33 | "github.com/seaweedfs/seaweedfs-operator/test/utils" 34 | ) 35 | 36 | const namespace = "seaweedfs-operator-system" 37 | const deploymentName = "seaweedfs-operator-controller-manager" 38 | 39 | var _ = Describe("controller", func() { 40 | 41 | Context("Operator", func() { 42 | It("should run successfully", func() { 43 | kubeconfig := config.GetConfigOrDie() 44 | clientset, err := utils.GetClientset(kubeconfig) 45 | 46 | deployment, err := clientset.AppsV1().Deployments(namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{}) 47 | if err != nil { 48 | panic(err.Error()) 49 | } 50 | 51 | // Get the pods under the deployment 52 | selector := deployment.Spec.Selector.MatchLabels 53 | labelSelector := metav1.LabelSelector{MatchLabels: selector} 54 | 55 | pods, err := clientset.CoreV1().Pods(namespace).List(context.TODO(), metav1.ListOptions{ 56 | LabelSelector: metav1.FormatLabelSelector(&labelSelector), 57 | }) 58 | 59 | if err != nil { 60 | panic(err.Error()) 61 | } 62 | 63 | stopCh := make(chan struct{}, 1) 64 | readyCh := make(chan struct{}) 65 | localPort, err := utils.GetFreePort() 66 | localPortStr := strconv.Itoa(localPort) 67 | 68 | // Call the function to run port forward 69 | err = utils.RunPortForward(kubeconfig, namespace, pods.Items[0].Name, []string{fmt.Sprintf("%s:8081", localPortStr)}, stopCh, readyCh) 70 | if err != nil { 71 | panic(err.Error()) 72 | } 73 | <-readyCh 74 | 75 | readyzURL := fmt.Sprintf("http://localhost:%s/readyz", localPortStr) 76 | client := resty.New().SetTimeout(5 * time.Second) 77 | resp, err := client.R().Get(readyzURL) 78 | if err != nil { 79 | panic(err.Error()) 80 | } 81 | 82 | Expect(resp.StatusCode()).To(Equal(http.StatusOK)) 83 | Expect(resp.Body()).To(Equal([]uint8{'o', 'k'})) 84 | 85 | close(stopCh) 86 | <-stopCh 87 | }) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /deploy/helm/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "seaweedfs-operator.fullname" . }} 5 | labels: 6 | {{- include "seaweedfs-operator.labels" . | nindent 4 }} 7 | {{- with .Values.commonLabels }} 8 | {{- toYaml . | nindent 4 }} 9 | {{- end }} 10 | {{- with .Values.commonAnnotations }} 11 | annotations: 12 | {{- toYaml . | nindent 4 }} 13 | {{- end }} 14 | spec: 15 | replicas: {{ .Values.replicaCount }} 16 | selector: 17 | matchLabels: 18 | {{- include "seaweedfs-operator.selectorLabels" . | nindent 6 }} 19 | template: 20 | metadata: 21 | labels: 22 | {{- include "seaweedfs-operator.selectorLabels" . | nindent 8 }} 23 | {{- with .Values.commonAnnotations }} 24 | annotations: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | spec: 28 | {{ if or .Values.image.credentials .Values.image.pullSecrets }} 29 | imagePullSecrets: 30 | - name: {{ include "seaweedfs-operator.pullSecretName" . }} 31 | {{ end }} 32 | serviceAccountName: {{ include "seaweedfs-operator.serviceAccountName" . }} 33 | {{- with .Values.podSecurityContext }} 34 | securityContext: 35 | {{- toYaml . | nindent 8 }} 36 | {{- end }} 37 | {{- with .Values.nodeSelector }} 38 | nodeSelector: 39 | {{- toYaml . | nindent 8 }} 40 | {{- end }} 41 | {{- with .Values.affinity }} 42 | affinity: 43 | {{- toYaml . | nindent 8 }} 44 | {{- end }} 45 | {{- with .Values.tolerations }} 46 | tolerations: 47 | {{- toYaml . | nindent 8 }} 48 | {{- end }} 49 | containers: 50 | - name: seaweedfs-operator 51 | {{- with .Values.securityContext }} 52 | securityContext: 53 | {{- toYaml . | nindent 10 }} 54 | {{- end }} 55 | image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }}" 56 | imagePullPolicy: {{ .Values.image.pullPolicy }} 57 | command: 58 | - /manager 59 | args: 60 | - --leader-elect 61 | env: 62 | {{- if eq .Values.webhook.enabled false }} 63 | - name: ENABLE_WEBHOOKS 64 | value: "false" 65 | {{- end }} 66 | ports: 67 | - name: {{ .Values.port.name }} 68 | containerPort: {{ .Values.port.number }} 69 | protocol: TCP 70 | - name: https 71 | containerPort: 443 72 | protocol: TCP 73 | {{- if .Values.resources }} 74 | resources: {{- toYaml .Values.resources | nindent 12 }} 75 | {{- end }} 76 | {{- if .Values.webhook.enabled }} 77 | volumeMounts: 78 | - mountPath: /tmp/k8s-webhook-server/serving-certs 79 | name: cert 80 | readOnly: true 81 | {{- end }} 82 | terminationGracePeriodSeconds: 10 83 | {{- if .Values.webhook.enabled }} 84 | volumes: 85 | - name: cert 86 | secret: 87 | defaultMode: 420 88 | secretName: {{ include "seaweedfs-operator.fullname" . }}-webhook-server-cert 89 | items: 90 | - key: cert 91 | path: tls.crt 92 | - key: key 93 | path: tls.key 94 | {{- end }} 95 | -------------------------------------------------------------------------------- /internal/controller/seaweed_controller_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | //import ( 4 | // "context" 5 | // "time" 6 | // 7 | // . "github.com/onsi/ginkgo" 8 | // . "github.com/onsi/gomega" 9 | // appsv1 "k8s.io/api/apps/v1" 10 | // corev1 "k8s.io/api/core/v1" 11 | // "k8s.io/apimachinery/pkg/api/resource" 12 | // metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | // "k8s.io/apimachinery/pkg/types" 14 | // 15 | // seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 16 | //) 17 | // 18 | //var ( 19 | // TrueValue = true 20 | // FalseVallue = false 21 | //) 22 | // 23 | //var _ = Describe("Seaweed Controller", func() { 24 | // Context("Basic Functionality", func() { 25 | // It("Should create StatefulSets", func() { 26 | // By("By creating a new Seaweed", func() { 27 | // const ( 28 | // namespace = "default" 29 | // name = "test-seaweed" 30 | // 31 | // timeout = time.Second * 30 32 | // interval = time.Millisecond * 250 33 | // ) 34 | // 35 | // ctx := context.Background() 36 | // seaweed := &seaweedv1.Seaweed{ 37 | // ObjectMeta: metav1.ObjectMeta{ 38 | // Namespace: namespace, 39 | // Name: name, 40 | // }, 41 | // Spec: seaweedv1.SeaweedSpec{ 42 | // Image: "chrislusf/seaweedfs:latest", 43 | // VolumeServerDiskCount: 1, 44 | // Master: &seaweedv1.MasterSpec{ 45 | // Replicas: 3, 46 | // ConcurrentStart: &TrueValue, 47 | // }, 48 | // Volume: &seaweedv1.VolumeSpec{ 49 | // Replicas: 1, 50 | // ResourceRequirements: corev1.ResourceRequirements{ 51 | // Requests: corev1.ResourceList{ 52 | // corev1.ResourceStorage: resource.MustParse("1Gi"), 53 | // }, 54 | // }, 55 | // }, 56 | // Filer: &seaweedv1.FilerSpec{ 57 | // Replicas: 2, 58 | // }, 59 | // }, 60 | // } 61 | // Expect(k8sClient.Create(ctx, seaweed)).Should(Succeed()) 62 | // 63 | // masterKey := types.NamespacedName{Name: name + "-master", Namespace: namespace} 64 | // volumeKey := types.NamespacedName{Name: name + "-volume", Namespace: namespace} 65 | // filerKey := types.NamespacedName{Name: name + "-filer", Namespace: namespace} 66 | // 67 | // masterSts := &appsv1.StatefulSet{} 68 | // volumeSts := &appsv1.StatefulSet{} 69 | // filerSts := &appsv1.StatefulSet{} 70 | // 71 | // Eventually(func() bool { 72 | // err := k8sClient.Get(ctx, masterKey, masterSts) 73 | // return err == nil 74 | // }, timeout, interval).Should(BeTrue()) 75 | // Expect(masterSts.Spec.Replicas).ShouldNot(BeNil()) 76 | // Expect(*masterSts.Spec.Replicas).Should(Equal(seaweed.Spec.Master.Replicas)) 77 | // 78 | // Eventually(func() bool { 79 | // err := k8sClient.Get(ctx, volumeKey, volumeSts) 80 | // return err == nil 81 | // }, timeout, interval).Should(BeTrue()) 82 | // Expect(volumeSts.Spec.Replicas).ShouldNot(BeNil()) 83 | // Expect(*volumeSts.Spec.Replicas).Should(Equal(seaweed.Spec.Volume.Replicas)) 84 | // 85 | // Eventually(func() bool { 86 | // err := k8sClient.Get(ctx, filerKey, filerSts) 87 | // return err == nil 88 | // }, timeout, interval).Should(BeTrue()) 89 | // Expect(filerSts.Spec.Replicas).ShouldNot(BeNil()) 90 | // Expect(*filerSts.Spec.Replicas).Should(Equal(seaweed.Spec.Filer.Replicas)) 91 | // }) 92 | // }) 93 | // }) 94 | //}) 95 | -------------------------------------------------------------------------------- /internal/controller/controller_filer_ingress.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | 6 | networkingv1 "k8s.io/api/networking/v1" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | ctrl "sigs.k8s.io/controller-runtime" 9 | 10 | seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 11 | ) 12 | 13 | func (r *SeaweedReconciler) createAllIngress(m *seaweedv1.Seaweed) *networkingv1.Ingress { 14 | log := r.Log.WithValues("sw-create-ingress", m.Name) 15 | labels := labelsForIngress(m.Name) 16 | pathType := networkingv1.PathTypePrefix 17 | 18 | dep := &networkingv1.Ingress{ 19 | ObjectMeta: metav1.ObjectMeta{ 20 | Name: m.Name + "-ingress", 21 | Namespace: m.Namespace, 22 | Labels: labels, 23 | }, 24 | Spec: networkingv1.IngressSpec{ 25 | // TLS: ingressSpec.TLS, 26 | Rules: []networkingv1.IngressRule{ 27 | { 28 | Host: "filer." + *m.Spec.HostSuffix, 29 | IngressRuleValue: networkingv1.IngressRuleValue{ 30 | HTTP: &networkingv1.HTTPIngressRuleValue{ 31 | Paths: []networkingv1.HTTPIngressPath{ 32 | { 33 | Path: "/", 34 | PathType: &pathType, 35 | Backend: networkingv1.IngressBackend{ 36 | Service: &networkingv1.IngressServiceBackend{ 37 | Name: m.Name + "-filer", 38 | Port: networkingv1.ServiceBackendPort{ 39 | Number: seaweedv1.FilerHTTPPort, 40 | }, 41 | }, 42 | }, 43 | }, 44 | }, 45 | }, 46 | }, 47 | }, 48 | { 49 | Host: "s3." + *m.Spec.HostSuffix, 50 | IngressRuleValue: networkingv1.IngressRuleValue{ 51 | HTTP: &networkingv1.HTTPIngressRuleValue{ 52 | Paths: []networkingv1.HTTPIngressPath{ 53 | { 54 | Path: "/", 55 | PathType: &pathType, 56 | Backend: networkingv1.IngressBackend{ 57 | Service: &networkingv1.IngressServiceBackend{ 58 | Name: m.Name + "-filer", 59 | Port: networkingv1.ServiceBackendPort{ 60 | Number: seaweedv1.FilerS3Port, 61 | }, 62 | }, 63 | }, 64 | }, 65 | }, 66 | }, 67 | }, 68 | }, 69 | }, 70 | }, 71 | } 72 | 73 | // add ingress for volume servers 74 | for i := 0; i < int(m.Spec.Volume.Replicas); i++ { 75 | dep.Spec.Rules = append(dep.Spec.Rules, networkingv1.IngressRule{ 76 | Host: fmt.Sprintf("%s-volume-%d.%s", m.Name, i, *m.Spec.HostSuffix), 77 | IngressRuleValue: networkingv1.IngressRuleValue{ 78 | HTTP: &networkingv1.HTTPIngressRuleValue{ 79 | Paths: []networkingv1.HTTPIngressPath{ 80 | { 81 | Path: "/", 82 | PathType: &pathType, 83 | Backend: networkingv1.IngressBackend{ 84 | Service: &networkingv1.IngressServiceBackend{ 85 | Name: fmt.Sprintf("%s-volume-%d", m.Name, i), 86 | Port: networkingv1.ServiceBackendPort{ 87 | Number: seaweedv1.VolumeHTTPPort, 88 | }, 89 | }, 90 | }, 91 | }, 92 | }, 93 | }, 94 | }, 95 | }) 96 | } 97 | 98 | // Set master instance as the owner and controller 99 | if err := ctrl.SetControllerReference(m, dep, r.Scheme); err != nil { 100 | log.Error(err, "set controller reference for Ingress failed") 101 | } 102 | return dep 103 | } 104 | -------------------------------------------------------------------------------- /internal/controller/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | //import ( 20 | // "path/filepath" 21 | // "testing" 22 | // 23 | // . "github.com/onsi/ginkgo" 24 | // . "github.com/onsi/gomega" 25 | // "k8s.io/client-go/kubernetes/scheme" 26 | // "k8s.io/client-go/rest" 27 | // ctrl "sigs.k8s.io/controller-runtime" 28 | // "sigs.k8s.io/controller-runtime/pkg/client" 29 | // "sigs.k8s.io/controller-runtime/pkg/envtest" 30 | // "sigs.k8s.io/controller-runtime/pkg/envtest/printer" 31 | // logf "sigs.k8s.io/controller-runtime/pkg/log" 32 | // "sigs.k8s.io/controller-runtime/pkg/log/zap" 33 | // 34 | // seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 35 | // // +kubebuilder:scaffold:imports 36 | //) 37 | // 38 | //// These tests use Ginkgo (BDD-style Go testing framework). Refer to 39 | //// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 40 | // 41 | //var cfg *rest.Config 42 | //var k8sClient client.Client 43 | //var testEnv *envtest.Environment 44 | // 45 | //func TestAPIs(t *testing.T) { 46 | // RegisterFailHandler(Fail) 47 | // 48 | // RunSpecsWithDefaultAndCustomReporters(t, 49 | // "Controller Suite", 50 | // []Reporter{printer.NewlineReporter{}}) 51 | //} 52 | // 53 | //var _ = BeforeSuite(func(done Done) { 54 | // logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 55 | // 56 | // By("bootstrapping test environment") 57 | // testEnv = &envtest.Environment{ 58 | // CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 59 | // } 60 | // 61 | // var err error 62 | // cfg, err = testEnv.Start() 63 | // Expect(err).ToNot(HaveOccurred()) 64 | // Expect(cfg).ToNot(BeNil()) 65 | // 66 | // err = seaweedv1.AddToScheme(scheme.Scheme) 67 | // Expect(err).NotTo(HaveOccurred()) 68 | // 69 | // // +kubebuilder:scaffold:scheme 70 | // 71 | // k8sClient, _ = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 72 | // k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ 73 | // Scheme: scheme.Scheme, 74 | // }) 75 | // Expect(err).ToNot(HaveOccurred()) 76 | // 77 | // err = (&SeaweedReconciler{ 78 | // Client: k8sManager.GetClient(), 79 | // Log: ctrl.Log.WithName("controller").WithName("Seaweed"), 80 | // Scheme: k8sManager.GetScheme(), 81 | // }).SetupWithManager(k8sManager) 82 | // Expect(err).ToNot(HaveOccurred()) 83 | // 84 | // go func() { 85 | // err = k8sManager.Start(ctrl.SetupSignalHandler()) 86 | // Expect(err).ToNot(HaveOccurred()) 87 | // }() 88 | // 89 | // Expect(err).ToNot(HaveOccurred()) 90 | // Expect(k8sClient).ToNot(BeNil()) 91 | // 92 | // close(done) 93 | //}, 60) 94 | // 95 | //var _ = AfterSuite(func() { 96 | // By("tearing down the test environment") 97 | // err := testEnv.Stop() 98 | // Expect(err).ToNot(HaveOccurred()) 99 | //}) 100 | -------------------------------------------------------------------------------- /internal/controller/controller_master_service.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | "k8s.io/apimachinery/pkg/util/intstr" 7 | 8 | seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 9 | ) 10 | 11 | func (r *SeaweedReconciler) createMasterPeerService(m *seaweedv1.Seaweed) *corev1.Service { 12 | labels := labelsForMaster(m.Name) 13 | ports := []corev1.ServicePort{ 14 | { 15 | Name: "master-http", 16 | Protocol: corev1.Protocol("TCP"), 17 | Port: seaweedv1.MasterHTTPPort, 18 | TargetPort: intstr.FromInt(seaweedv1.MasterHTTPPort), 19 | }, 20 | { 21 | Name: "master-grpc", 22 | Protocol: corev1.Protocol("TCP"), 23 | Port: seaweedv1.MasterGRPCPort, 24 | TargetPort: intstr.FromInt(seaweedv1.MasterGRPCPort), 25 | }, 26 | } 27 | if m.Spec.Master.MetricsPort != nil { 28 | ports = append(ports, corev1.ServicePort{ 29 | Name: "master-metrics", 30 | Protocol: corev1.Protocol("TCP"), 31 | Port: *m.Spec.Master.MetricsPort, 32 | TargetPort: intstr.FromInt(int(*m.Spec.Master.MetricsPort)), 33 | }) 34 | } 35 | 36 | dep := &corev1.Service{ 37 | ObjectMeta: metav1.ObjectMeta{ 38 | Name: m.Name + "-master-peer", 39 | Namespace: m.Namespace, 40 | Labels: labels, 41 | Annotations: map[string]string{ 42 | "service.alpha.kubernetes.io/tolerate-unready-endpoints": "true", 43 | }, 44 | }, 45 | Spec: corev1.ServiceSpec{ 46 | ClusterIP: "None", 47 | PublishNotReadyAddresses: true, 48 | Ports: ports, 49 | Selector: labels, 50 | }, 51 | } 52 | // Set master instance as the owner and controller 53 | // ctrl.SetControllerReference(m, dep, r.Scheme) 54 | return dep 55 | } 56 | 57 | func (r *SeaweedReconciler) createMasterService(m *seaweedv1.Seaweed) *corev1.Service { 58 | labels := labelsForMaster(m.Name) 59 | ports := []corev1.ServicePort{ 60 | { 61 | Name: "master-http", 62 | Protocol: corev1.Protocol("TCP"), 63 | Port: seaweedv1.MasterHTTPPort, 64 | TargetPort: intstr.FromInt(seaweedv1.MasterHTTPPort), 65 | }, 66 | { 67 | Name: "master-grpc", 68 | Protocol: corev1.Protocol("TCP"), 69 | Port: seaweedv1.MasterGRPCPort, 70 | TargetPort: intstr.FromInt(seaweedv1.MasterGRPCPort), 71 | }, 72 | } 73 | if m.Spec.Master.MetricsPort != nil { 74 | ports = append(ports, corev1.ServicePort{ 75 | Name: "master-metrics", 76 | Protocol: corev1.Protocol("TCP"), 77 | Port: *m.Spec.Master.MetricsPort, 78 | TargetPort: intstr.FromInt(int(*m.Spec.Master.MetricsPort)), 79 | }) 80 | } 81 | 82 | dep := &corev1.Service{ 83 | ObjectMeta: metav1.ObjectMeta{ 84 | Name: m.Name + "-master", 85 | Namespace: m.Namespace, 86 | Labels: labels, 87 | Annotations: map[string]string{ 88 | "service.alpha.kubernetes.io/tolerate-unready-endpoints": "true", 89 | }, 90 | }, 91 | Spec: corev1.ServiceSpec{ 92 | PublishNotReadyAddresses: true, 93 | Ports: ports, 94 | Selector: labels, 95 | }, 96 | } 97 | 98 | if m.Spec.Master.Service != nil { 99 | svcSpec := m.Spec.Master.Service 100 | dep.Annotations = copyAnnotations(svcSpec.Annotations) 101 | 102 | if svcSpec.Type != "" { 103 | dep.Spec.Type = svcSpec.Type 104 | } 105 | 106 | if svcSpec.ClusterIP != nil { 107 | dep.Spec.ClusterIP = *svcSpec.ClusterIP 108 | } 109 | 110 | if svcSpec.LoadBalancerIP != nil { 111 | dep.Spec.LoadBalancerIP = *svcSpec.LoadBalancerIP 112 | } 113 | } 114 | return dep 115 | } 116 | -------------------------------------------------------------------------------- /api/v1/seaweed_webhook.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1 18 | 19 | import ( 20 | "errors" 21 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission" 22 | 23 | corev1 "k8s.io/api/core/v1" 24 | "k8s.io/apimachinery/pkg/api/resource" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | utilerrors "k8s.io/apimachinery/pkg/util/errors" 27 | ctrl "sigs.k8s.io/controller-runtime" 28 | logf "sigs.k8s.io/controller-runtime/pkg/log" 29 | "sigs.k8s.io/controller-runtime/pkg/webhook" 30 | ) 31 | 32 | // log is for logging in this package. 33 | var seaweedlog = logf.Log.WithName("seaweed-resource") 34 | 35 | func (r *Seaweed) SetupWebhookWithManager(mgr ctrl.Manager) error { 36 | return ctrl.NewWebhookManagedBy(mgr). 37 | For(r). 38 | Complete() 39 | } 40 | 41 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 42 | 43 | // +kubebuilder:webhook:path=/mutate-seaweed-seaweedfs-com-v1-seaweed,mutating=true,failurePolicy=fail,sideEffects=None,groups=seaweed.seaweedfs.com,resources=seaweeds,verbs=create;update,versions=v1,name=mseaweed.kb.io,admissionReviewVersions=v1 44 | 45 | var _ webhook.Defaulter = &Seaweed{} 46 | 47 | // Default implements webhook.Defaulter so a webhook will be registered for the type 48 | func (r *Seaweed) Default() { 49 | seaweedlog.Info("default", "name", r.Name) 50 | 51 | // TODO(user): fill in your defaulting logic. 52 | } 53 | 54 | // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. 55 | // +kubebuilder:webhook:verbs=create;update,path=/validate-seaweed-seaweedfs-com-v1-seaweed,mutating=false,failurePolicy=fail,sideEffects=None,groups=seaweed.seaweedfs.com,resources=seaweeds,versions=v1,name=vseaweed.kb.io,admissionReviewVersions=v1 56 | 57 | var _ webhook.Validator = &Seaweed{} 58 | 59 | // ValidateCreate implements webhook.Validator so a webhook will be registered for the type 60 | func (r *Seaweed) ValidateCreate() (admission.Warnings, error) { 61 | seaweedlog.Info("validate create", "name", r.Name) 62 | errs := []error{} 63 | 64 | // TODO(user): fill in your validation logic upon object creation. 65 | if r.Spec.Master == nil { 66 | errs = append(errs, errors.New("missing master spec")) 67 | } 68 | 69 | if r.Spec.Volume == nil { 70 | errs = append(errs, errors.New("missing volume spec")) 71 | } else { 72 | if r.Spec.Volume.Requests[corev1.ResourceStorage].Equal(resource.MustParse("0")) { 73 | errs = append(errs, errors.New("volume storage request cannot be zero")) 74 | } 75 | } 76 | 77 | return nil, utilerrors.NewAggregate(errs) 78 | } 79 | 80 | // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type 81 | func (r *Seaweed) ValidateUpdate(old runtime.Object) (admission.Warnings, error) { 82 | seaweedlog.Info("validate update", "name", r.Name) 83 | 84 | // TODO(user): fill in your validation logic upon object update. 85 | return nil, nil 86 | } 87 | 88 | // ValidateDelete implements webhook.Validator so a webhook will be registered for the type 89 | func (r *Seaweed) ValidateDelete() (admission.Warnings, error) { 90 | seaweedlog.Info("validate delete", "name", r.Name) 91 | 92 | // TODO(user): fill in your validation logic upon object deletion. 93 | return nil, nil 94 | } 95 | -------------------------------------------------------------------------------- /test/utils/utils.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package utils 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "net" 23 | "net/http" 24 | "os" 25 | "os/exec" 26 | "strings" 27 | 28 | . "github.com/onsi/ginkgo/v2" //nolint:golint,revive 29 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | "k8s.io/client-go/kubernetes" 31 | "k8s.io/client-go/rest" 32 | "k8s.io/client-go/tools/portforward" 33 | "k8s.io/client-go/transport/spdy" 34 | ) 35 | 36 | // Run executes the provided command within this context 37 | func Run(cmd *exec.Cmd) ([]byte, error) { 38 | dir, _ := GetProjectDir() 39 | cmd.Dir = dir 40 | 41 | if err := os.Chdir(cmd.Dir); err != nil { 42 | fmt.Fprintf(GinkgoWriter, "chdir dir: %s\n", err) 43 | } 44 | 45 | cmd.Env = append(os.Environ(), "GO111MODULE=on") 46 | command := strings.Join(cmd.Args, " ") 47 | fmt.Fprintf(GinkgoWriter, "running: %s\n", command) 48 | output, err := cmd.CombinedOutput() 49 | if err != nil { 50 | return output, fmt.Errorf("%s failed with error: (%v) %s", command, err, string(output)) 51 | } 52 | 53 | return output, nil 54 | } 55 | 56 | // GetProjectDir will return the directory where the project is 57 | func GetProjectDir() (string, error) { 58 | wd, err := os.Getwd() 59 | if err != nil { 60 | return wd, err 61 | } 62 | wd = strings.Replace(wd, "/test/e2e", "", -1) 63 | return wd, nil 64 | } 65 | 66 | // GetClientset returns a kubernetes Clientset. 67 | func GetClientset(config *rest.Config) (*kubernetes.Clientset, error) { 68 | clientset, err := kubernetes.NewForConfig(config) 69 | if err != nil { 70 | panic(err) 71 | } 72 | return clientset, nil 73 | } 74 | 75 | // RunPortForward creates a port-forward for a specific pod. 76 | func RunPortForward(config *rest.Config, namespace, podName string, ports []string, stopCh, readyCh chan struct{}) error { 77 | // Get Clientset 78 | clientset, err := GetClientset(config) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | // Get the pod 84 | pod, err := clientset.CoreV1().Pods(namespace).Get(context.TODO(), podName, metav1.GetOptions{}) 85 | if err != nil { 86 | return fmt.Errorf("failed to get pod: %v", err) 87 | } 88 | 89 | // Create the port forwarder 90 | req := clientset.CoreV1().RESTClient().Post(). 91 | Resource("pods"). 92 | Namespace(pod.Namespace). 93 | Name(pod.Name). 94 | SubResource("portforward") 95 | 96 | transport, upgrader, err := spdy.RoundTripperFor(config) 97 | if err != nil { 98 | return fmt.Errorf("failed to create round tripper: %v", err) 99 | } 100 | 101 | fw, err := portforward.New( 102 | spdy.NewDialer(upgrader, &http.Client{Transport: transport}, http.MethodPost, req.URL()), 103 | ports, 104 | stopCh, 105 | readyCh, 106 | os.Stdout, 107 | os.Stderr, 108 | ) 109 | if err != nil { 110 | return fmt.Errorf("failed to create port forwarder: %v", err) 111 | } 112 | 113 | go func() { 114 | if err := fw.ForwardPorts(); err != nil { 115 | fmt.Printf("Port forwarding failed: %v\n", err) 116 | } 117 | }() 118 | 119 | return nil 120 | } 121 | 122 | // GetFreePort asks the kernel for a free open port that is ready to use. 123 | func GetFreePort() (port int, err error) { 124 | var a *net.TCPAddr 125 | if a, err = net.ResolveTCPAddr("tcp", "localhost:0"); err == nil { 126 | var l *net.TCPListener 127 | if l, err = net.ListenTCP("tcp", a); err == nil { 128 | defer l.Close() 129 | return l.Addr().(*net.TCPAddr).Port, nil 130 | } 131 | } 132 | return 133 | } 134 | -------------------------------------------------------------------------------- /IAM_SUPPORT.md: -------------------------------------------------------------------------------- 1 | # IAM Service Support in SeaweedFS Operator 2 | 3 | This document describes the IAM (Identity and Access Management) service support in the SeaweedFS Operator. 4 | 5 | ## Overview 6 | 7 | Starting from SeaweedFS version **4.03**, the IAM API is now **embedded in the S3 server by default**. This follows the pattern used by MinIO and Ceph RGW, providing a simpler deployment model where both S3 and IAM APIs are available on the same port (8333). 8 | 9 | ## Configuration 10 | 11 | ### Enabling S3 with Embedded IAM (Default) 12 | 13 | When you enable S3, IAM is automatically enabled on the same port: 14 | 15 | ```yaml 16 | apiVersion: seaweed.seaweedfs.com/v1 17 | kind: Seaweed 18 | metadata: 19 | name: seaweed-sample 20 | spec: 21 | image: chrislusf/seaweedfs:latest 22 | 23 | master: 24 | replicas: 1 25 | volume: 26 | replicas: 1 27 | filer: 28 | replicas: 1 29 | s3: 30 | enabled: true 31 | # iam: true # Default - IAM is enabled when S3 is enabled 32 | ``` 33 | 34 | The IAM API is accessible on the same port as S3 (8333). 35 | 36 | ### Disabling Embedded IAM 37 | 38 | To run S3 without IAM: 39 | 40 | ```yaml 41 | filer: 42 | replicas: 1 43 | s3: 44 | enabled: true 45 | iam: false # Explicitly disable embedded IAM 46 | ``` 47 | 48 | ## API Reference 49 | 50 | ### FilerSpec.IAM 51 | 52 | ```go 53 | // IAM enables/disables IAM API embedded in S3 server. 54 | // When S3 is enabled, IAM is enabled by default (on the same S3 port: 8333). 55 | // Set to false to explicitly disable embedded IAM. 56 | // +kubebuilder:default:=true 57 | IAM bool `json:"iam,omitempty"` 58 | ``` 59 | 60 | ## Service Discovery 61 | 62 | The IAM API is accessible through the filer S3 service: 63 | - **Internal**: `-filer..svc.cluster.local:8333` 64 | - **Port**: 8333 (same as S3) 65 | 66 | ## Examples 67 | 68 | Complete examples are available in the `config/samples/` directory: 69 | 70 | - `seaweed_v1_seaweed_with_iam_embedded.yaml`: Embedded IAM configuration 71 | 72 | ### Quick Start 73 | 74 | Deploy SeaweedFS with S3 and embedded IAM: 75 | 76 | ```bash 77 | kubectl apply -f config/samples/seaweed_v1_seaweed_with_iam_embedded.yaml 78 | ``` 79 | 80 | Verify deployment: 81 | 82 | ```bash 83 | # Check all resources 84 | kubectl get seaweed,statefulset,service,pod 85 | 86 | # Test S3/IAM endpoint (both on same port) 87 | kubectl port-forward svc/seaweed-sample-filer 8333:8333 88 | 89 | # S3 operations 90 | aws --endpoint-url http://localhost:8333 s3 ls 91 | 92 | # IAM operations (same endpoint) 93 | aws --endpoint-url http://localhost:8333 iam list-users 94 | ``` 95 | 96 | ## Architecture 97 | 98 | ``` 99 | ┌─────────────────────────────────────────────┐ 100 | │ Filer Pod │ 101 | │ ┌────────────────────────────────────────┐ │ 102 | │ │ weed filer -s3 │ │ 103 | │ │ ┌──────────────────────────────────┐ │ │ 104 | │ │ │ S3 API Server (port 8333) │ │ │ 105 | │ │ │ ├── S3 Operations (GET/PUT/...) │ │ │ 106 | │ │ │ └── IAM Operations (POST /) │ │ │ 107 | │ │ └──────────────────────────────────┘ │ │ 108 | │ └────────────────────────────────────────┘ │ 109 | └─────────────────────────────────────────────┘ 110 | ``` 111 | 112 | ## Benefits 113 | 114 | 1. **Simple deployment**: Single service, single port 115 | 2. **Reduced resource usage**: No separate IAM pods 116 | 3. **Industry standard**: Matches MinIO and Ceph RGW patterns 117 | 4. **Automatic scaling**: IAM scales with S3/filer instances 118 | 119 | ## Migration from Standalone IAM 120 | 121 | If you were previously using standalone IAM, simply: 122 | 123 | 1. Remove the `iam:` section from your Seaweed CRD 124 | 2. Ensure `filer.s3.enabled: true` 125 | 3. Update clients to use the S3 port (8333) for IAM operations 126 | 127 | ## Further Reading 128 | 129 | - [SeaweedFS IAM Documentation](https://github.com/seaweedfs/seaweedfs/wiki/IAM) 130 | - [SeaweedFS S3 API Documentation](https://github.com/seaweedfs/seaweedfs/wiki/Amazon-S3-API) 131 | -------------------------------------------------------------------------------- /internal/controller/controller_filer_service.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | corev1 "k8s.io/api/core/v1" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | "k8s.io/apimachinery/pkg/util/intstr" 7 | 8 | seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 9 | ) 10 | 11 | func (r *SeaweedReconciler) createFilerPeerService(m *seaweedv1.Seaweed) *corev1.Service { 12 | labels := labelsForFiler(m.Name) 13 | ports := []corev1.ServicePort{ 14 | { 15 | Name: "filer-http", 16 | Protocol: corev1.Protocol("TCP"), 17 | Port: seaweedv1.FilerHTTPPort, 18 | TargetPort: intstr.FromInt(seaweedv1.FilerHTTPPort), 19 | }, 20 | { 21 | Name: "filer-grpc", 22 | Protocol: corev1.Protocol("TCP"), 23 | Port: seaweedv1.FilerGRPCPort, 24 | TargetPort: intstr.FromInt(seaweedv1.FilerGRPCPort), 25 | }, 26 | } 27 | if m.Spec.Filer.S3 != nil && m.Spec.Filer.S3.Enabled { 28 | // S3 port also serves IAM API when embedded IAM is enabled (default) 29 | ports = append(ports, corev1.ServicePort{ 30 | Name: "filer-s3", 31 | Protocol: corev1.Protocol("TCP"), 32 | Port: seaweedv1.FilerS3Port, 33 | TargetPort: intstr.FromInt(seaweedv1.FilerS3Port), 34 | }) 35 | } 36 | if m.Spec.Filer.MetricsPort != nil { 37 | ports = append(ports, corev1.ServicePort{ 38 | Name: "filer-metrics", 39 | Protocol: corev1.Protocol("TCP"), 40 | Port: *m.Spec.Filer.MetricsPort, 41 | TargetPort: intstr.FromInt(int(*m.Spec.Filer.MetricsPort)), 42 | }) 43 | } 44 | 45 | dep := &corev1.Service{ 46 | ObjectMeta: metav1.ObjectMeta{ 47 | Name: m.Name + "-filer-peer", 48 | Namespace: m.Namespace, 49 | Labels: labels, 50 | Annotations: map[string]string{ 51 | "service.alpha.kubernetes.io/tolerate-unready-endpoints": "true", 52 | }, 53 | }, 54 | Spec: corev1.ServiceSpec{ 55 | ClusterIP: "None", 56 | PublishNotReadyAddresses: true, 57 | Ports: ports, 58 | Selector: labels, 59 | }, 60 | } 61 | return dep 62 | } 63 | 64 | func (r *SeaweedReconciler) createFilerService(m *seaweedv1.Seaweed) *corev1.Service { 65 | labels := labelsForFiler(m.Name) 66 | ports := []corev1.ServicePort{ 67 | { 68 | Name: "filer-http", 69 | Protocol: corev1.Protocol("TCP"), 70 | Port: seaweedv1.FilerHTTPPort, 71 | TargetPort: intstr.FromInt(seaweedv1.FilerHTTPPort), 72 | }, 73 | { 74 | Name: "filer-grpc", 75 | Protocol: corev1.Protocol("TCP"), 76 | Port: seaweedv1.FilerGRPCPort, 77 | TargetPort: intstr.FromInt(seaweedv1.FilerGRPCPort), 78 | }, 79 | } 80 | if m.Spec.Filer.S3 != nil && m.Spec.Filer.S3.Enabled { 81 | // S3 port also serves IAM API when embedded IAM is enabled (default) 82 | ports = append(ports, corev1.ServicePort{ 83 | Name: "filer-s3", 84 | Protocol: corev1.Protocol("TCP"), 85 | Port: seaweedv1.FilerS3Port, 86 | TargetPort: intstr.FromInt(seaweedv1.FilerS3Port), 87 | }) 88 | } 89 | if m.Spec.Filer.MetricsPort != nil { 90 | ports = append(ports, corev1.ServicePort{ 91 | Name: "filer-metrics", 92 | Protocol: corev1.Protocol("TCP"), 93 | Port: *m.Spec.Filer.MetricsPort, 94 | TargetPort: intstr.FromInt(int(*m.Spec.Filer.MetricsPort)), 95 | }) 96 | } 97 | 98 | dep := &corev1.Service{ 99 | ObjectMeta: metav1.ObjectMeta{ 100 | Name: m.Name + "-filer", 101 | Namespace: m.Namespace, 102 | Labels: labels, 103 | Annotations: map[string]string{ 104 | "service.alpha.kubernetes.io/tolerate-unready-endpoints": "true", 105 | }, 106 | }, 107 | Spec: corev1.ServiceSpec{ 108 | Type: corev1.ServiceTypeClusterIP, 109 | PublishNotReadyAddresses: true, 110 | Ports: ports, 111 | Selector: labels, 112 | }, 113 | } 114 | 115 | if m.Spec.Filer.Service != nil { 116 | svcSpec := m.Spec.Filer.Service 117 | dep.Annotations = copyAnnotations(svcSpec.Annotations) 118 | 119 | if svcSpec.Type != "" { 120 | dep.Spec.Type = svcSpec.Type 121 | } 122 | 123 | if svcSpec.ClusterIP != nil { 124 | dep.Spec.ClusterIP = *svcSpec.ClusterIP 125 | } 126 | 127 | if svcSpec.LoadBalancerIP != nil { 128 | dep.Spec.LoadBalancerIP = *svcSpec.LoadBalancerIP 129 | } 130 | } 131 | return dep 132 | } 133 | -------------------------------------------------------------------------------- /internal/controller/seaweed_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controller 18 | 19 | import ( 20 | "context" 21 | "time" 22 | 23 | "github.com/go-logr/logr" 24 | "k8s.io/apimachinery/pkg/api/errors" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | ctrl "sigs.k8s.io/controller-runtime" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | 29 | seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 30 | ) 31 | 32 | // SeaweedReconciler reconciles a Seaweed object 33 | type SeaweedReconciler struct { 34 | client.Client 35 | Log logr.Logger 36 | Scheme *runtime.Scheme 37 | } 38 | 39 | // +kubebuilder:rbac:groups=seaweed.seaweedfs.com,resources=seaweeds,verbs=get;list;watch;create;update;patch;delete 40 | // +kubebuilder:rbac:groups=seaweed.seaweedfs.com,resources=seaweeds/status,verbs=get;update;patch 41 | // +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete 42 | // +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete 43 | // +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;create;update;patch;delete 44 | // +kubebuilder:rbac:groups=extensions,resources=ingresses,verbs=get;list;watch;create;update;patch;delete 45 | // +kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete 46 | // +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list; 47 | 48 | // Reconcile implements the reconciliation logic 49 | func (r *SeaweedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 50 | log := r.Log.WithValues("seaweed", req.NamespacedName) 51 | 52 | log.Info("start Reconcile ...") 53 | 54 | seaweedCR, done, result, err := r.findSeaweedCustomResourceInstance(ctx, log, req) 55 | if done { 56 | return result, err 57 | } 58 | 59 | if done, result, err = r.ensureMaster(seaweedCR); done { 60 | return result, err 61 | } 62 | 63 | if done, result, err = r.ensureVolumeServers(seaweedCR); done { 64 | return result, err 65 | } 66 | 67 | if seaweedCR.Spec.Filer != nil { 68 | if done, result, err = r.ensureFilerServers(seaweedCR); done { 69 | return result, err 70 | } 71 | } 72 | 73 | // Note: Standalone IAM has been removed. IAM is now embedded in S3 by default. 74 | // Use filer.s3.enabled=true to enable S3 with embedded IAM. 75 | 76 | if done, result, err = r.ensureSeaweedIngress(seaweedCR); done { 77 | return result, err 78 | } 79 | 80 | if false { 81 | if done, result, err = r.maintenance(seaweedCR); done { 82 | return result, err 83 | } 84 | } 85 | 86 | return ctrl.Result{RequeueAfter: 5 * time.Second}, nil 87 | } 88 | 89 | func (r *SeaweedReconciler) findSeaweedCustomResourceInstance(ctx context.Context, log logr.Logger, req ctrl.Request) (*seaweedv1.Seaweed, bool, ctrl.Result, error) { 90 | // fetch the master instance 91 | seaweedCR := &seaweedv1.Seaweed{} 92 | err := r.Get(ctx, req.NamespacedName, seaweedCR) 93 | if err != nil { 94 | if errors.IsNotFound(err) { 95 | // Request object not found, could have been deleted after reconcile request. 96 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 97 | // Return and don't requeue 98 | log.Info("Seaweed CR not found. Ignoring since object must be deleted") 99 | return nil, true, ctrl.Result{RequeueAfter: time.Second * 5}, nil 100 | } 101 | // Error reading the object - requeue the request. 102 | log.Error(err, "Failed to get SeaweedCR") 103 | return nil, true, ctrl.Result{}, err 104 | } 105 | log.Info("Get master " + seaweedCR.Name) 106 | return seaweedCR, false, ctrl.Result{}, nil 107 | } 108 | 109 | func (r *SeaweedReconciler) SetupWithManager(mgr ctrl.Manager) error { 110 | return ctrl.NewControllerManagedBy(mgr). 111 | For(&seaweedv1.Seaweed{}). 112 | Complete(r) 113 | } 114 | -------------------------------------------------------------------------------- /deploy/helm/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "seaweedfs-operator.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "seaweedfs-operator.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "seaweedfs-operator.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "seaweedfs-operator.labels" -}} 37 | helm.sh/chart: {{ include "seaweedfs-operator.chart" . }} 38 | {{ include "seaweedfs-operator.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "seaweedfs-operator.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "seaweedfs-operator.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Docker registry image pull secret 55 | */}} 56 | {{- define "seaweedfs-operator.imagePullSecret" }} 57 | {{- $auth := printf "%s:%s" .username .password | b64enc -}} 58 | {{- printf "{\"auths\": {\"%s\": {\"auth\": \"%s\"}}}" .registry $auth | b64enc }} 59 | {{- end }} 60 | 61 | {{- define "seaweedfs-operator.createPullSecret" -}} 62 | {{- if and .Values.image.credentials (not .Values.image.pullSecrets) }} 63 | {{- true -}} 64 | {{- else -}} 65 | {{- end -}} 66 | {{- end -}} 67 | 68 | {{- define "seaweedfs-operator.pullSecretName" -}} 69 | {{- if .Values.image.pullSecrets -}} 70 | {{- printf "%s" (tpl .Values.image.pullSecrets .) -}} 71 | {{- else -}} 72 | {{- printf "%s-container-registry" (include "seaweedfs-operator.fullname" .) -}} 73 | {{- end -}} 74 | {{- end -}} 75 | 76 | {{/* 77 | Create the name of the service account to use 78 | */}} 79 | {{- define "seaweedfs-operator.serviceAccountName" -}} 80 | {{- if .Values.rbac.serviceAccount.create -}} 81 | {{- default (include "seaweedfs-operator.fullname" .) .Values.rbac.serviceAccount.name -}} 82 | {{- else -}} 83 | {{- default "default" .Values.rbac.serviceAccount.name -}} 84 | {{- end -}} 85 | {{- end -}} 86 | 87 | {{/* 88 | Mutating webhook path 89 | */}} 90 | {{- define "seaweedfs-operator.mutatingWebhookPath" -}}/mutate-seaweed-seaweedfs-com-v1-seaweed{{- end -}} 91 | 92 | {{/* 93 | Validating webhook path 94 | */}} 95 | {{- define "seaweedfs-operator.validatingWebhookPath" -}}/validate-seaweed-seaweedfs-com-v1-seaweed{{- end -}} 96 | 97 | {{/* 98 | Webhook Pod Security Context 99 | */}} 100 | {{- define "seaweedfs-operator.webhookPodSecurityContext" -}} 101 | {{- with .Values.webhook.podSecurityContext }} 102 | securityContext: 103 | {{- toYaml . | nindent 2 }} 104 | {{- end }} 105 | {{- end -}} 106 | 107 | {{/* 108 | Webhook Container Security Context 109 | */}} 110 | {{- define "seaweedfs-operator.webhookContainerSecurityContext" -}} 111 | {{- with .Values.webhook.securityContext }} 112 | securityContext: 113 | {{- toYaml . | nindent 2 }} 114 | {{- end }} 115 | {{- end -}} 116 | 117 | {{/* 118 | Webhook init container for waiting until webhook service is ready 119 | */}} 120 | {{- define "seaweedfs-operator.webhookWaitInitContainer" -}} 121 | - name: wait-for-webhook 122 | image: {{ .Values.webhook.initContainer.image }} 123 | {{- include "seaweedfs-operator.webhookContainerSecurityContext" . | nindent 2 }} 124 | command: ['sh', '-c', 'set -e; until curl -sk --fail --head --max-time 5 https://{{ include "seaweedfs-operator.fullname" . }}-webhook.{{ .Release.Namespace }}.svc:443{{ .webhookPath }} >/dev/null; do echo waiting for webhook; sleep 1; done;'] 125 | {{- end -}} 126 | -------------------------------------------------------------------------------- /.github/workflows/helm_chart_release.yml: -------------------------------------------------------------------------------- 1 | name: "helm: publish charts" 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | workflow_dispatch: 7 | inputs: 8 | tag: 9 | description: 'Tag to release (e.g., v1.0.5)' 10 | required: true 11 | type: string 12 | 13 | permissions: 14 | contents: write 15 | pages: write 16 | 17 | jobs: 18 | release: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Configure Git 26 | run: | 27 | git config user.name "chrislusf" 28 | git config user.email "chrislusf@users.noreply.github.com" 29 | 30 | - name: Install Helm 31 | uses: azure/setup-helm@v4 32 | 33 | - name: Determine tag 34 | id: get_tag 35 | run: | 36 | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then 37 | TAG="${{ github.event.inputs.tag }}" 38 | else 39 | TAG="${GITHUB_REF#refs/tags/}" 40 | fi 41 | echo "tag=$TAG" >> $GITHUB_OUTPUT 42 | echo "Detected tag: $TAG" 43 | 44 | - name: Set appVersion in Chart.yaml to match tag 45 | run: | 46 | TAG="${{ steps.get_tag.outputs.tag }}" 47 | echo "Setting appVersion to: $TAG" 48 | sed -i "s/appVersion: .*/appVersion: \"$TAG\"/" deploy/helm/Chart.yaml 49 | 50 | - name: Set chart version based on tag 51 | id: set_chart_version 52 | run: | 53 | # Increment the chart version for each release 54 | CURRENT_VERSION=$(grep '^version:' deploy/helm/Chart.yaml | awk '{print $2}') 55 | IFS='.' read -r major minor patch <<< "$CURRENT_VERSION" 56 | VERSION="$major.$minor.$((patch + 1))" 57 | echo "Setting chart version to: $VERSION" 58 | sed -i "s/version: .*/version: $VERSION/" deploy/helm/Chart.yaml 59 | echo "chart_version=$VERSION" >> $GITHUB_OUTPUT 60 | 61 | - name: Checkout branch 62 | run: git checkout master 63 | 64 | - name: Commit Chart.yaml changes 65 | run: | 66 | git add deploy/helm/Chart.yaml 67 | git commit -m "Update Helm chart version for release ${{ steps.get_tag.outputs.tag }}" 68 | git push 69 | 70 | - name: Regenerate CRD manifests 71 | run: | 72 | make manifests 73 | 74 | - name: Copy fixed CRD to Helm chart 75 | run: | 76 | cp config/crd/bases/seaweed.seaweedfs.com_seaweeds.yaml deploy/helm/crds/seaweed.seaweedfs.com_seaweeds.yaml 77 | 78 | - name: Debug chart structure 79 | run: | 80 | echo "Chart.yaml content:" 81 | cat deploy/helm/Chart.yaml 82 | echo -e "\nLinting chart:" 83 | helm lint deploy/helm 84 | 85 | - name: Package Helm chart 86 | run: | 87 | helm package deploy/helm -d .cr-release-packages 88 | 89 | - name: Upload chart to release 90 | uses: softprops/action-gh-release@v2 91 | with: 92 | tag_name: seaweedfs-operator-${{ steps.set_chart_version.outputs.chart_version }} 93 | files: .cr-release-packages/*.tgz 94 | 95 | - name: Update Helm repo index 96 | run: | 97 | # Checkout gh-pages branch 98 | git fetch origin gh-pages:gh-pages || true 99 | git checkout gh-pages || git checkout --orphan gh-pages 100 | 101 | # Ensure helm directory exists 102 | mkdir -p helm 103 | 104 | CHART_VERSION="${{ steps.set_chart_version.outputs.chart_version }}" 105 | RELEASE_URL="https://github.com/${{ github.repository }}/releases/download/seaweedfs-operator-${CHART_VERSION}" 106 | 107 | # Update index.yaml in the helm/ subdirectory 108 | if [ -f helm/index.yaml ]; then 109 | helm repo index .cr-release-packages --url "${RELEASE_URL}" --merge helm/index.yaml 110 | cp .cr-release-packages/index.yaml helm/index.yaml 111 | else 112 | helm repo index .cr-release-packages --url "${RELEASE_URL}" 113 | cp .cr-release-packages/index.yaml helm/index.yaml 114 | fi 115 | 116 | # Also update the root index.yaml (some users add repo without /helm suffix) 117 | cp helm/index.yaml index.yaml 118 | 119 | git add helm/index.yaml index.yaml 120 | git commit -m "Update Helm repo index for seaweedfs-operator-${CHART_VERSION}" || true 121 | git push origin gh-pages 122 | -------------------------------------------------------------------------------- /internal/controller/controller_filer.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/apimachinery/pkg/runtime" 7 | 8 | appsv1 "k8s.io/api/apps/v1" 9 | ctrl "sigs.k8s.io/controller-runtime" 10 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 11 | 12 | seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 13 | label "github.com/seaweedfs/seaweedfs-operator/internal/controller/label" 14 | ) 15 | 16 | func (r *SeaweedReconciler) ensureFilerServers(seaweedCR *seaweedv1.Seaweed) (done bool, result ctrl.Result, err error) { 17 | _ = context.Background() 18 | _ = r.Log.WithValues("seaweed", seaweedCR.Name) 19 | 20 | if done, result, err = r.ensureFilerPeerService(seaweedCR); done { 21 | return 22 | } 23 | 24 | if done, result, err = r.ensureFilerService(seaweedCR); done { 25 | return 26 | } 27 | 28 | if done, result, err = r.ensureFilerConfigMap(seaweedCR); done { 29 | return 30 | } 31 | 32 | if done, result, err = r.ensureFilerStatefulSet(seaweedCR); done { 33 | return 34 | } 35 | 36 | if seaweedCR.Spec.Filer.MetricsPort != nil { 37 | if done, result, err = r.ensureFilerServiceMonitor(seaweedCR); done { 38 | return 39 | } 40 | } 41 | 42 | return 43 | } 44 | 45 | func (r *SeaweedReconciler) ensureFilerStatefulSet(seaweedCR *seaweedv1.Seaweed) (bool, ctrl.Result, error) { 46 | log := r.Log.WithValues("sw-filer-statefulset", seaweedCR.Name) 47 | 48 | filerStatefulSet := r.createFilerStatefulSet(seaweedCR) 49 | if err := controllerutil.SetControllerReference(seaweedCR, filerStatefulSet, r.Scheme); err != nil { 50 | return ReconcileResult(err) 51 | } 52 | _, err := r.CreateOrUpdate(filerStatefulSet, func(existing, desired runtime.Object) error { 53 | existingStatefulSet := existing.(*appsv1.StatefulSet) 54 | desiredStatefulSet := desired.(*appsv1.StatefulSet) 55 | 56 | existingStatefulSet.Spec.Replicas = desiredStatefulSet.Spec.Replicas 57 | existingStatefulSet.Spec.Template.ObjectMeta = desiredStatefulSet.Spec.Template.ObjectMeta 58 | existingStatefulSet.Spec.Template.Spec = desiredStatefulSet.Spec.Template.Spec 59 | return nil 60 | }) 61 | log.Info("ensure filer stateful set " + filerStatefulSet.Name) 62 | return ReconcileResult(err) 63 | } 64 | 65 | func (r *SeaweedReconciler) ensureFilerPeerService(seaweedCR *seaweedv1.Seaweed) (bool, ctrl.Result, error) { 66 | 67 | log := r.Log.WithValues("sw-filer-peer-service", seaweedCR.Name) 68 | 69 | filerPeerService := r.createFilerPeerService(seaweedCR) 70 | if err := controllerutil.SetControllerReference(seaweedCR, filerPeerService, r.Scheme); err != nil { 71 | return ReconcileResult(err) 72 | } 73 | 74 | _, err := r.CreateOrUpdateService(filerPeerService) 75 | log.Info("ensure filer peer service " + filerPeerService.Name) 76 | 77 | return ReconcileResult(err) 78 | } 79 | 80 | func (r *SeaweedReconciler) ensureFilerService(seaweedCR *seaweedv1.Seaweed) (bool, ctrl.Result, error) { 81 | 82 | log := r.Log.WithValues("sw-filer-service", seaweedCR.Name) 83 | 84 | filerService := r.createFilerService(seaweedCR) 85 | if err := controllerutil.SetControllerReference(seaweedCR, filerService, r.Scheme); err != nil { 86 | return ReconcileResult(err) 87 | } 88 | _, err := r.CreateOrUpdateService(filerService) 89 | 90 | log.Info("ensure filer service " + filerService.Name) 91 | 92 | return ReconcileResult(err) 93 | } 94 | 95 | func (r *SeaweedReconciler) ensureFilerConfigMap(seaweedCR *seaweedv1.Seaweed) (bool, ctrl.Result, error) { 96 | log := r.Log.WithValues("sw-filer-configmap", seaweedCR.Name) 97 | 98 | filerConfigMap := r.createFilerConfigMap(seaweedCR) 99 | if err := controllerutil.SetControllerReference(seaweedCR, filerConfigMap, r.Scheme); err != nil { 100 | return ReconcileResult(err) 101 | } 102 | _, err := r.CreateOrUpdateConfigMap(filerConfigMap) 103 | 104 | log.Info("Get filer ConfigMap " + filerConfigMap.Name) 105 | return ReconcileResult(err) 106 | } 107 | 108 | func (r *SeaweedReconciler) ensureFilerServiceMonitor(seaweedCR *seaweedv1.Seaweed) (bool, ctrl.Result, error) { 109 | log := r.Log.WithValues("sw-filer-servicemonitor", seaweedCR.Name) 110 | 111 | filerServiceMonitor := r.createFilerServiceMonitor(seaweedCR) 112 | if err := controllerutil.SetControllerReference(seaweedCR, filerServiceMonitor, r.Scheme); err != nil { 113 | return ReconcileResult(err) 114 | } 115 | _, err := r.CreateOrUpdateServiceMonitor(filerServiceMonitor) 116 | 117 | log.Info("Get filer service monitor " + filerServiceMonitor.Name) 118 | return ReconcileResult(err) 119 | } 120 | 121 | func labelsForFiler(name string) map[string]string { 122 | return map[string]string{ 123 | label.ManagedByLabelKey: "seaweedfs-operator", 124 | label.NameLabelKey: "seaweedfs", 125 | label.ComponentLabelKey: "filer", 126 | label.InstanceLabelKey: name, 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /config/samples/seaweed_v1_seaweed_tree_topology.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: seaweed.seaweedfs.com/v1 2 | kind: Seaweed 3 | metadata: 4 | name: seaweed-tree-topology 5 | namespace: default 6 | spec: 7 | # SeaweedFS image 8 | image: chrislusf/seaweedfs:latest 9 | volumeServerDiskCount: 1 10 | 11 | # Master configuration with 210 replication 12 | master: 13 | replicas: 3 14 | volumeSizeLimitMB: 1024 15 | # Set default replication to 210 (2 copies in different datacenters, 1 copy in different rack, 0 copies in same rack) 16 | defaultReplication: "210" 17 | 18 | # Default volume server configuration. Settings here are inherited by all topology groups. 19 | # Set replicas to 0 to disable the simple volume group and only use volumeTopology. 20 | volume: 21 | replicas: 0 22 | requests: 23 | storage: "5Gi" # Default storage size, can be overridden per group 24 | cpu: "500m" # Default CPU request 25 | memory: "1Gi" # Default memory request 26 | limits: 27 | cpu: "1" # Default CPU limit 28 | memory: "2Gi" # Default memory limit 29 | metricsPort: 9323 # Default metrics port, can be overridden per group 30 | # Default environment variables that all topology groups will inherit 31 | env: 32 | - name: WEED_VOLUME_MAX_VOLUMES 33 | value: "50" 34 | 35 | # Tree-structured topology configuration 36 | # This creates a hierarchical deployment across multiple datacenters and racks 37 | volumeTopology: 38 | # Datacenter 1, Rack 1 39 | dc1-rack1: 40 | replicas: 3 41 | rack: "rack1" 42 | dataCenter: "dc1" 43 | # Use node selector to ensure placement on appropriate nodes 44 | nodeSelector: 45 | topology.kubernetes.io/zone: "us-west-2a" 46 | seaweedfs/datacenter: "dc1" 47 | seaweedfs/rack: "rack1" 48 | # Optional: Add specific tolerations for this topology group 49 | tolerations: 50 | - key: "datacenter" 51 | operator: "Equal" 52 | value: "dc1" 53 | effect: "NoSchedule" 54 | # Resource requirements specific to this topology group 55 | requests: 56 | storage: 10Gi 57 | cpu: "1" 58 | memory: "2Gi" 59 | limits: 60 | cpu: "2" 61 | memory: "4Gi" 62 | metricsPort: 9324 63 | 64 | # Datacenter 1, Rack 2 65 | # This group inherits most settings from spec.volume but overrides storage size and adds nodeSelector 66 | dc1-rack2: 67 | replicas: 2 68 | rack: "rack2" 69 | dataCenter: "dc1" 70 | nodeSelector: 71 | topology.kubernetes.io/zone: "us-west-2b" 72 | seaweedfs/datacenter: "dc1" 73 | seaweedfs/rack: "rack2" 74 | tolerations: 75 | - key: "datacenter" 76 | operator: "Equal" 77 | value: "dc1" 78 | effect: "NoSchedule" 79 | # Override only storage size, other resource settings inherit from spec.volume 80 | requests: 81 | storage: 8Gi 82 | metricsPort: 9325 83 | 84 | # Datacenter 2, Rack 1 85 | dc2-rack1: 86 | replicas: 3 87 | rack: "rack1" 88 | dataCenter: "dc2" 89 | nodeSelector: 90 | topology.kubernetes.io/zone: "us-east-1a" 91 | seaweedfs/datacenter: "dc2" 92 | seaweedfs/rack: "rack1" 93 | tolerations: 94 | - key: "datacenter" 95 | operator: "Equal" 96 | value: "dc2" 97 | effect: "NoSchedule" 98 | requests: 99 | storage: 15Gi 100 | cpu: "1" 101 | memory: "2Gi" 102 | limits: 103 | cpu: "2" 104 | memory: "4Gi" 105 | # Use different storage class for this topology group 106 | storageClassName: "fast-ssd" 107 | metricsPort: 9326 108 | # Volume-specific settings for this topology 109 | compactionMBps: 100 110 | maxVolumeCounts: 100 111 | 112 | # Datacenter 2, Rack 2 113 | dc2-rack2: 114 | replicas: 2 115 | rack: "rack2" 116 | dataCenter: "dc2" 117 | nodeSelector: 118 | topology.kubernetes.io/zone: "us-east-1b" 119 | seaweedfs/datacenter: "dc2" 120 | seaweedfs/rack: "rack2" 121 | tolerations: 122 | - key: "datacenter" 123 | operator: "Equal" 124 | value: "dc2" 125 | effect: "NoSchedule" 126 | requests: 127 | storage: 15Gi 128 | cpu: "500m" 129 | memory: "1.5Gi" 130 | limits: 131 | cpu: "1.5" 132 | memory: "3Gi" 133 | storageClassName: "fast-ssd" 134 | metricsPort: 9327 135 | compactionMBps: 80 136 | maxVolumeCounts: 80 137 | 138 | # Filer configuration 139 | filer: 140 | replicas: 2 141 | s3: 142 | enabled: true 143 | config: | 144 | [leveldb2] 145 | enabled = true 146 | dir = "/data/filerldb2" 147 | 148 | # Enable cross-datacenter replication 149 | [replication] 150 | dc1 = "dc1" 151 | dc2 = "dc2" 152 | --- 153 | # ServiceMonitor for monitoring all topology groups 154 | apiVersion: monitoring.coreos.com/v1 155 | kind: ServiceMonitor 156 | metadata: 157 | name: seaweed-volume-topology 158 | namespace: default 159 | labels: 160 | app: seaweedfs 161 | spec: 162 | selector: 163 | matchExpressions: 164 | - key: seaweedfs/topology 165 | operator: Exists 166 | endpoints: 167 | - port: volume-metrics 168 | path: /metrics 169 | -------------------------------------------------------------------------------- /internal/controller/controller_master_statefulset.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | appsv1 "k8s.io/api/apps/v1" 8 | corev1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/util/intstr" 11 | 12 | seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 13 | ) 14 | 15 | func buildMasterStartupScript(m *seaweedv1.Seaweed) string { 16 | command := []string{"weed", "-logtostderr=true", "master"} 17 | spec := m.Spec.Master 18 | if spec.VolumePreallocate != nil && *spec.VolumePreallocate { 19 | command = append(command, "-volumePreallocate") 20 | } 21 | 22 | if spec.VolumeSizeLimitMB != nil { 23 | command = append(command, fmt.Sprintf("-volumeSizeLimitMB=%d", *spec.VolumeSizeLimitMB)) 24 | } 25 | 26 | if spec.GarbageThreshold != nil { 27 | command = append(command, fmt.Sprintf("-garbageThreshold=%s", *spec.GarbageThreshold)) 28 | } 29 | 30 | if spec.PulseSeconds != nil { 31 | command = append(command, fmt.Sprintf("-pulseSeconds=%d", *spec.PulseSeconds)) 32 | } 33 | 34 | if spec.DefaultReplication != nil { 35 | command = append(command, fmt.Sprintf("-defaultReplication=%s", *spec.DefaultReplication)) 36 | } 37 | 38 | if m.Spec.Master.MetricsPort != nil { 39 | command = append(command, fmt.Sprintf("-metricsPort=%d", *m.Spec.Master.MetricsPort)) 40 | } 41 | 42 | command = append(command, fmt.Sprintf("-ip=$(POD_NAME).%s-master-peer.%s", m.Name, m.Namespace)) 43 | command = append(command, fmt.Sprintf("-peers=%s", getMasterPeersString(m))) 44 | return strings.Join(command, " ") 45 | } 46 | 47 | func (r *SeaweedReconciler) createMasterStatefulSet(m *seaweedv1.Seaweed) *appsv1.StatefulSet { 48 | labels := labelsForMaster(m.Name) 49 | annotations := m.Spec.Master.Annotations 50 | ports := []corev1.ContainerPort{ 51 | { 52 | ContainerPort: seaweedv1.MasterHTTPPort, 53 | Name: "master-http", 54 | }, 55 | { 56 | ContainerPort: seaweedv1.MasterGRPCPort, 57 | Name: "master-grpc", 58 | }, 59 | } 60 | if m.Spec.Master.MetricsPort != nil { 61 | ports = append(ports, corev1.ContainerPort{ 62 | ContainerPort: *m.Spec.Master.MetricsPort, 63 | Name: "master-metrics", 64 | }) 65 | } 66 | replicas := m.Spec.Master.Replicas 67 | rollingUpdatePartition := int32(0) 68 | enableServiceLinks := false 69 | 70 | masterPodSpec := m.BaseMasterSpec().BuildPodSpec() 71 | masterPodSpec.Volumes = []corev1.Volume{ 72 | { 73 | Name: "master-config", 74 | VolumeSource: corev1.VolumeSource{ 75 | ConfigMap: &corev1.ConfigMapVolumeSource{ 76 | LocalObjectReference: corev1.LocalObjectReference{ 77 | Name: m.Name + "-master", 78 | }, 79 | }, 80 | }, 81 | }, 82 | } 83 | masterPodSpec.EnableServiceLinks = &enableServiceLinks 84 | masterPodSpec.Containers = []corev1.Container{{ 85 | Name: "master", 86 | Image: m.Spec.Image, 87 | ImagePullPolicy: m.BaseMasterSpec().ImagePullPolicy(), 88 | Env: append(m.BaseMasterSpec().Env(), kubernetesEnvVars...), 89 | Resources: filterContainerResources(m.Spec.Master.ResourceRequirements), 90 | VolumeMounts: []corev1.VolumeMount{ 91 | { 92 | Name: "master-config", 93 | ReadOnly: true, 94 | MountPath: "/etc/seaweedfs", 95 | }, 96 | }, 97 | Command: []string{ 98 | "/bin/sh", 99 | "-ec", 100 | buildMasterStartupScript(m), 101 | }, 102 | Ports: ports, 103 | ReadinessProbe: &corev1.Probe{ 104 | ProbeHandler: corev1.ProbeHandler{ 105 | HTTPGet: &corev1.HTTPGetAction{ 106 | Path: "/cluster/status", 107 | Port: intstr.FromInt(seaweedv1.MasterHTTPPort), 108 | Scheme: corev1.URISchemeHTTP, 109 | }, 110 | }, 111 | InitialDelaySeconds: 5, 112 | TimeoutSeconds: 15, 113 | PeriodSeconds: 15, 114 | SuccessThreshold: 2, 115 | FailureThreshold: 100, 116 | }, 117 | LivenessProbe: &corev1.Probe{ 118 | ProbeHandler: corev1.ProbeHandler{ 119 | HTTPGet: &corev1.HTTPGetAction{ 120 | Path: "/cluster/status", 121 | Port: intstr.FromInt(seaweedv1.MasterHTTPPort), 122 | Scheme: corev1.URISchemeHTTP, 123 | }, 124 | }, 125 | InitialDelaySeconds: 15, 126 | TimeoutSeconds: 15, 127 | PeriodSeconds: 15, 128 | SuccessThreshold: 1, 129 | FailureThreshold: 6, 130 | }, 131 | }} 132 | 133 | dep := &appsv1.StatefulSet{ 134 | ObjectMeta: metav1.ObjectMeta{ 135 | Name: m.Name + "-master", 136 | Namespace: m.Namespace, 137 | }, 138 | Spec: appsv1.StatefulSetSpec{ 139 | ServiceName: m.Name + "-master-peer", 140 | PodManagementPolicy: appsv1.ParallelPodManagement, 141 | Replicas: &replicas, 142 | UpdateStrategy: appsv1.StatefulSetUpdateStrategy{ 143 | Type: appsv1.RollingUpdateStatefulSetStrategyType, 144 | RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{ 145 | Partition: &rollingUpdatePartition, 146 | }, 147 | }, 148 | Selector: &metav1.LabelSelector{ 149 | MatchLabels: labels, 150 | }, 151 | Template: corev1.PodTemplateSpec{ 152 | ObjectMeta: metav1.ObjectMeta{ 153 | Labels: labels, 154 | Annotations: annotations, 155 | }, 156 | Spec: masterPodSpec, 157 | }, 158 | }, 159 | } 160 | // Set master instance as the owner and controller 161 | // ctrl.SetControllerReference(m, dep, r.Scheme) 162 | return dep 163 | } 164 | -------------------------------------------------------------------------------- /deploy/helm/values.yaml: -------------------------------------------------------------------------------- 1 | # -- Global Docker image parameters 2 | # Please, note that this will override the image parameters, including dependencies, configured to use the global value 3 | # Current available global Docker image parameters: imageRegistry 4 | global: 5 | imageRegistry: "chrislusf" 6 | 7 | # -- String to partially override common.names.fullname template (will maintain the release name) 8 | nameOverride: "" 9 | 10 | # -- String to fully override common.names.fullname template 11 | fullnameOverride: "" 12 | 13 | # -- Annotations for all the deployed objects 14 | commonAnnotations: {} 15 | 16 | # -- Labels for all the deployed objects 17 | commonLabels: {} 18 | 19 | ## Configure Kubernetes Rbac parameters 20 | rbac: 21 | serviceAccount: 22 | # -- Specifies whether a service account should be created 23 | create: true 24 | # -- Annotations to add to the service account 25 | annotations: {} 26 | # -- The name of the service account to use. 27 | # If not set and create is true, a name is generated using the fullname template 28 | # If set to "default", no ServiceAccount will be created and the default one will be used 29 | name: "" 30 | # -- Automount service account token for the server service account 31 | automount: true 32 | 33 | image: 34 | registry: chrislusf 35 | repository: seaweedfs-operator 36 | # -- tag of image to use. Defaults to appVersion in Chart.yaml 37 | tag: "" 38 | # -- Specify a imagePullPolicy 39 | ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' 40 | ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images 41 | pullPolicy: IfNotPresent 42 | 43 | ## Specify credentials to authorize in docker registry or set existing secrets in pullSecrets 44 | # credentials: 45 | # registry: private-registry 46 | # username: username 47 | # password: password 48 | 49 | ## Optionally specify imagePullSecret. 50 | ## Secrets must be manually created in the namespace. 51 | ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ 52 | ## 53 | # pullSecrets: myRegistryKeySecretName 54 | 55 | # -- Set number of pod replicas 56 | replicaCount: 1 57 | 58 | ## Configure container port 59 | port: 60 | # -- name of the container port to use for the Kubernete service and ingress 61 | name: http 62 | # -- container port number to use for the Kubernete service and ingress 63 | number: 8080 64 | 65 | ## Configure Service 66 | service: 67 | # -- name of the port to use for Kubernetes service 68 | portName: http 69 | # -- port to use for Kubernetes service 70 | port: 8080 71 | 72 | grafanaDashboard: 73 | # -- Enable or disable Grafana Dashboard configmap 74 | enabled: true 75 | 76 | serviceMonitor: 77 | # -- Enable or disable ServiceMonitor for prometheus metrics 78 | enabled: false 79 | # -- Specify the interval at which metrics should be scraped 80 | interval: 10s 81 | # -- Specify the timeout after which the scrape is ended 82 | scrapeTimeout: 10s 83 | ## Specify Metric Relabellings to add to the scrape endpoint 84 | # -- Specify honorLabels parameter to add the scrape endpoint 85 | honorLabels: true 86 | ## Specify the release for ServiceMonitor. Sometimes it should be custom for prometheus operator to work 87 | # release: "" 88 | ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#prometheusspec 89 | # -- Used to pass Labels that are used by the Prometheus installed in your cluster to select Service Monitors to work with 90 | additionalLabels: {} 91 | 92 | webhook: 93 | # -- Enable or disable webhooks 94 | enabled: true 95 | # -- Configuration for webhook certificate jobs 96 | initContainer: 97 | # -- Image for webhook readiness check init container 98 | image: curlimages/curl:8.8.0 99 | # -- Pod security context for webhook jobs 100 | # ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ 101 | podSecurityContext: 102 | runAsNonRoot: true 103 | runAsUser: 65532 104 | fsGroup: 65532 105 | seccompProfile: 106 | type: RuntimeDefault 107 | # -- Container security context for webhook jobs 108 | securityContext: 109 | allowPrivilegeEscalation: false 110 | capabilities: 111 | drop: 112 | - ALL 113 | readOnlyRootFilesystem: true 114 | runAsNonRoot: true 115 | 116 | ## seaweedfs-operator containers' resource requests and limits. 117 | ## ref: http://kubernetes.io/docs/user-guide/compute-resources/ 118 | resources: 119 | limits: 120 | # -- seaweedfs-operator containers' cpu limit (maximum allowed CPU) 121 | cpu: 500m 122 | # -- seaweedfs-operator containers' memory limit (maximum allowed memory) 123 | memory: 500Mi 124 | requests: 125 | # -- seaweedfs-operator containers' cpu request (how much is requested by default) 126 | cpu: 100m 127 | # -- seaweedfs-operator containers' memory request (how much is requested by default) 128 | memory: 50Mi 129 | 130 | ## Security context for pods 131 | ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ 132 | podSecurityContext: 133 | runAsNonRoot: true 134 | runAsUser: 65532 135 | fsGroup: 65532 136 | 137 | ## Security context for containers 138 | ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ 139 | securityContext: 140 | allowPrivilegeEscalation: false 141 | capabilities: 142 | drop: 143 | - ALL 144 | readOnlyRootFilesystem: true 145 | runAsNonRoot: true 146 | 147 | ## Node labels for pod assignment 148 | ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/ 149 | nodeSelector: {} 150 | 151 | ## Tolerations for pod assignment 152 | ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/ 153 | tolerations: [] 154 | 155 | ## Affinity for pod assignment 156 | ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#affinity-and-anti-affinity 157 | affinity: {} 158 | -------------------------------------------------------------------------------- /test/INTEGRATION_TESTING.md: -------------------------------------------------------------------------------- 1 | # Integration Testing 2 | 3 | This document describes how to run integration tests for the SeaweedFS Operator, particularly the resource requirements testing that ensures the fixes for [Issue #131](https://github.com/seaweedfs/seaweedfs-operator/issues/131) and [Issue #132](https://github.com/seaweedfs/seaweedfs-operator/issues/132) work correctly. 4 | 5 | ## Test Overview 6 | 7 | The integration tests verify that: 8 | 9 | 1. **Resource requirements are properly applied** to all container specs (master, volume, filer) 10 | 2. **Storage resources are correctly filtered** out of container specs but used for PVC sizing 11 | 3. **The operator deploys successfully** in a real Kubernetes environment 12 | 4. **Resource requests/limits work in constrained environments** like GKE Autopilot 13 | 14 | ## Running Tests Locally 15 | 16 | ### Prerequisites 17 | 18 | - Go 1.22+ 19 | - Docker 20 | - kubectl 21 | - Make 22 | 23 | ### Quick Test Run 24 | 25 | ```bash 26 | # Run just the unit tests for resource filtering 27 | make test 28 | 29 | # Run the specific resource filtering tests 30 | go test ./internal/controller/ -run TestFilterContainerResources -v 31 | ``` 32 | 33 | ### Full Integration Test with Kind 34 | 35 | ```bash 36 | # Create a Kind cluster and run all e2e tests 37 | make test-e2e 38 | 39 | # Or run manually with specific Kubernetes version 40 | K8S_VERSION=v1.30.0 make kind-prepare 41 | make docker-build kind-load deploy 42 | go test ./test/e2e/ -v -ginkgo.v -timeout 20m 43 | ``` 44 | 45 | ### Step-by-Step Manual Testing 46 | 47 | 1. **Set up Kind cluster:** 48 | ```bash 49 | make kind-prepare 50 | ``` 51 | 52 | 2. **Build and load operator image:** 53 | ```bash 54 | make docker-build kind-load 55 | ``` 56 | 57 | 3. **Deploy the operator:** 58 | ```bash 59 | make deploy 60 | kubectl wait deployment.apps/seaweedfs-operator-controller-manager \ 61 | --for condition=Available \ 62 | --namespace seaweedfs-operator-system \ 63 | --timeout 5m 64 | ``` 65 | 66 | 4. **Run integration tests:** 67 | ```bash 68 | go test ./test/e2e/ -v -ginkgo.v -ginkgo.progress 69 | ``` 70 | 71 | 5. **Clean up:** 72 | ```bash 73 | make undeploy kind-delete 74 | ``` 75 | 76 | ## Test Structure 77 | 78 | ### Unit Tests (`internal/controller/helper_test.go`) 79 | 80 | - `TestFilterContainerResources`: Verifies the `filterContainerResources()` function correctly removes storage resources while preserving other resources 81 | - `TestFilterContainerResourcesEmpty`: Tests edge cases with empty resource specifications 82 | 83 | ### Integration Tests (`test/e2e/resource_integration_test.go`) 84 | 85 | The integration tests create actual Seaweed resources with comprehensive resource specifications and verify: 86 | 87 | #### Master Container Resources 88 | - CPU requests/limits are applied correctly 89 | - Memory requests/limits are applied correctly 90 | - No storage resources leak into container specs 91 | 92 | #### Volume Container Resources 93 | - CPU, memory, and ephemeral-storage resources are applied 94 | - **Critical**: `storage` resources are filtered out of container specs 95 | - Storage resources are used for PVC templates in StatefulSets 96 | 97 | #### Filer Container Resources 98 | - CPU and memory resources are applied correctly 99 | - No unintended resource types are included 100 | 101 | ### GitHub Actions Workflow 102 | 103 | The `.github/workflows/integration-test.yml` workflow runs automatically on: 104 | 105 | - Pull requests (with proper labels or from maintainers) 106 | - Pushes to main/master branches 107 | 108 | The workflow includes: 109 | - **Multi-version testing**: Kubernetes v1.29, v1.30, v1.31 110 | - **Resource validation**: Specific tests for storage filtering 111 | - **Build verification**: Ensures code compiles and Docker images build 112 | - **Comprehensive logging**: Collects operator logs, pod status, and events on failure 113 | 114 | ## Key Test Scenarios 115 | 116 | ### Resource Filtering Test 117 | ```yaml 118 | volume: 119 | requests: 120 | cpu: "250m" 121 | memory: "512Mi" 122 | storage: "10Gi" # Should NOT appear in container 123 | ephemeral-storage: "1Gi" # Should appear in container 124 | ``` 125 | 126 | **Expected Result:** 127 | - Container spec contains: `cpu`, `memory`, `ephemeral-storage` 128 | - Container spec does NOT contain: `storage` 129 | - PVC template contains: `storage: "10Gi"` 130 | 131 | ### GKE Autopilot Compatibility 132 | The tests ensure that resource specifications work correctly in constrained environments like GKE Autopilot, where: 133 | - Missing resource requests cause pod failures 134 | - Invalid resource types (like `storage` in containers) are rejected 135 | 136 | ## Troubleshooting 137 | 138 | ### Test Failures 139 | 140 | 1. **Check operator logs:** 141 | ```bash 142 | kubectl logs -n seaweedfs-operator-system deployment/seaweedfs-operator-controller-manager 143 | ``` 144 | 145 | 2. **Verify StatefulSet creation:** 146 | ```bash 147 | kubectl get statefulsets --all-namespaces 148 | kubectl describe statefulset -n test-resources test-seaweed-resources-volume 149 | ``` 150 | 151 | 3. **Check resource specifications:** 152 | ```bash 153 | kubectl get statefulset test-seaweed-resources-volume -o yaml | grep -A 20 resources: 154 | ``` 155 | 156 | ### Common Issues 157 | 158 | - **Kind cluster not starting**: Check Docker is running and has sufficient resources 159 | - **Image pull failures**: Ensure `make kind-load` completed successfully 160 | - **Timeout errors**: Increase test timeouts or check cluster resources 161 | 162 | ## Contributing 163 | 164 | When modifying resource handling: 165 | 166 | 1. **Update unit tests** in `helper_test.go` for new resource types 167 | 2. **Extend integration tests** in `resource_integration_test.go` for new scenarios 168 | 3. **Test locally** with `make test-e2e` before submitting PRs 169 | 4. **Verify multi-version compatibility** by testing with different Kubernetes versions 170 | 171 | The integration tests serve as both verification of fixes and regression prevention for future changes. 172 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "crypto/tls" 21 | "flag" 22 | monitorv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" 23 | "os" 24 | 25 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 26 | // to ensure that exec-entrypoint and run can make use of them. 27 | _ "k8s.io/client-go/plugin/pkg/client/auth" 28 | 29 | "k8s.io/apimachinery/pkg/runtime" 30 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 31 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 32 | ctrl "sigs.k8s.io/controller-runtime" 33 | "sigs.k8s.io/controller-runtime/pkg/healthz" 34 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 35 | metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" 36 | "sigs.k8s.io/controller-runtime/pkg/webhook" 37 | 38 | seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 39 | "github.com/seaweedfs/seaweedfs-operator/internal/controller" 40 | // +kubebuilder:scaffold:imports 41 | ) 42 | 43 | var ( 44 | scheme = runtime.NewScheme() 45 | setupLog = ctrl.Log.WithName("setup") 46 | ) 47 | 48 | func init() { 49 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 50 | 51 | utilruntime.Must(seaweedv1.AddToScheme(scheme)) 52 | utilruntime.Must(monitorv1.AddToScheme(scheme)) 53 | // +kubebuilder:scaffold:scheme 54 | } 55 | 56 | func main() { 57 | var metricsAddr string 58 | var enableLeaderElection bool 59 | var probeAddr string 60 | var secureMetrics bool 61 | var enableHTTP2 bool 62 | flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metric endpoint binds to. "+ 63 | "Use the port :8080. If not set, it will be 0 in order to disable the metrics server") 64 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 65 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 66 | "Enable leader election for controller manager. "+ 67 | "Enabling this will ensure there is only one active controller manager.") 68 | flag.BoolVar(&secureMetrics, "metrics-secure", false, 69 | "If set the metrics endpoint is served securely") 70 | flag.BoolVar(&enableHTTP2, "enable-http2", false, 71 | "If set, HTTP/2 will be enabled for the metrics and webhook servers") 72 | opts := zap.Options{ 73 | Development: true, 74 | } 75 | opts.BindFlags(flag.CommandLine) 76 | flag.Parse() 77 | 78 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 79 | 80 | // if the enable-http2 flag is false (the default), http/2 should be disabled 81 | // due to its vulnerabilities. More specifically, disabling http/2 will 82 | // prevent from being vulnerable to the HTTP/2 Stream Cancellation and 83 | // Rapid Reset CVEs. For more information see: 84 | // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 85 | // - https://github.com/advisories/GHSA-4374-p667-p6c8 86 | disableHTTP2 := func(c *tls.Config) { 87 | setupLog.Info("disabling http/2") 88 | c.NextProtos = []string{"http/1.1"} 89 | } 90 | 91 | tlsOpts := []func(*tls.Config){} 92 | if !enableHTTP2 { 93 | tlsOpts = append(tlsOpts, disableHTTP2) 94 | } 95 | 96 | webhookServer := webhook.NewServer(webhook.Options{ 97 | TLSOpts: tlsOpts, 98 | }) 99 | 100 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 101 | Scheme: scheme, 102 | Metrics: metricsserver.Options{ 103 | BindAddress: metricsAddr, 104 | SecureServing: secureMetrics, 105 | TLSOpts: tlsOpts, 106 | }, 107 | WebhookServer: webhookServer, 108 | HealthProbeBindAddress: probeAddr, 109 | LeaderElection: enableLeaderElection, 110 | LeaderElectionID: "674006ec.seaweedfs.com", 111 | // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily 112 | // when the Manager ends. This requires the binary to immediately end when the 113 | // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly 114 | // speeds up voluntary leader transitions as the new leader don't have to wait 115 | // LeaseDuration time first. 116 | // 117 | // In the default scaffold provided, the program ends immediately after 118 | // the manager stops, so would be fine to enable this option. However, 119 | // if you are doing or is intended to do any operation such as perform cleanups 120 | // after the manager stops then its usage might be unsafe. 121 | // LeaderElectionReleaseOnCancel: true, 122 | }) 123 | if err != nil { 124 | setupLog.Error(err, "unable to start manager") 125 | os.Exit(1) 126 | } 127 | 128 | if err = (&controller.SeaweedReconciler{ 129 | Client: mgr.GetClient(), 130 | Log: ctrl.Log.WithName("controller").WithName("Seaweed"), 131 | Scheme: mgr.GetScheme(), 132 | }).SetupWithManager(mgr); err != nil { 133 | setupLog.Error(err, "unable to create controller", "controller", "Seaweed") 134 | os.Exit(1) 135 | } 136 | 137 | if os.Getenv("ENABLE_WEBHOOKS") != "false" { 138 | if err = (&seaweedv1.Seaweed{}).SetupWebhookWithManager(mgr); err != nil { 139 | setupLog.Error(err, "unable to create webhook", "webhook", "Seaweed") 140 | os.Exit(1) 141 | } 142 | } 143 | // +kubebuilder:scaffold:builder 144 | 145 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 146 | setupLog.Error(err, "unable to set up health check") 147 | os.Exit(1) 148 | } 149 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 150 | setupLog.Error(err, "unable to set up ready check") 151 | os.Exit(1) 152 | } 153 | 154 | setupLog.Info("starting manager") 155 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 156 | setupLog.Error(err, "problem running manager") 157 | os.Exit(1) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /api/v1/component_accessor.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | corev1 "k8s.io/api/core/v1" 6 | ) 7 | 8 | // ComponentAccessor is the interface to access component details, which respects the cluster-level properties 9 | // and component-level overrides 10 | // +kubebuilder:object:root=false 11 | // +kubebuilder:object:generate=false 12 | type ComponentAccessor interface { 13 | ImagePullPolicy() corev1.PullPolicy 14 | ImagePullSecrets() []corev1.LocalObjectReference 15 | HostNetwork() bool 16 | Affinity() *corev1.Affinity 17 | PriorityClassName() *string 18 | NodeSelector() map[string]string 19 | Annotations() map[string]string 20 | Tolerations() []corev1.Toleration 21 | SchedulerName() string 22 | DNSPolicy() corev1.DNSPolicy 23 | BuildPodSpec() corev1.PodSpec 24 | Env() []corev1.EnvVar 25 | TerminationGracePeriodSeconds() *int64 26 | StatefulSetUpdateStrategy() appsv1.StatefulSetUpdateStrategyType 27 | } 28 | 29 | type componentAccessorImpl struct { 30 | imagePullPolicy corev1.PullPolicy 31 | imagePullSecrets []corev1.LocalObjectReference 32 | hostNetwork *bool 33 | affinity *corev1.Affinity 34 | priorityClassName *string 35 | schedulerName string 36 | clusterNodeSelector map[string]string 37 | clusterAnnotations map[string]string 38 | tolerations []corev1.Toleration 39 | statefulSetUpdateStrategy appsv1.StatefulSetUpdateStrategyType 40 | 41 | // ComponentSpec is the Component Spec 42 | ComponentSpec *ComponentSpec 43 | } 44 | 45 | func (a *componentAccessorImpl) StatefulSetUpdateStrategy() appsv1.StatefulSetUpdateStrategyType { 46 | strategy := a.ComponentSpec.StatefulSetUpdateStrategy 47 | if len(strategy) != 0 { 48 | return strategy 49 | } 50 | 51 | strategy = a.statefulSetUpdateStrategy 52 | if len(strategy) != 0 { 53 | return strategy 54 | } 55 | 56 | return appsv1.RollingUpdateStatefulSetStrategyType 57 | } 58 | 59 | func (a *componentAccessorImpl) ImagePullPolicy() corev1.PullPolicy { 60 | pp := a.ComponentSpec.ImagePullPolicy 61 | if pp == nil { 62 | return a.imagePullPolicy 63 | } 64 | return *pp 65 | } 66 | 67 | func (a *componentAccessorImpl) ImagePullSecrets() []corev1.LocalObjectReference { 68 | ips := a.ComponentSpec.ImagePullSecrets 69 | if ips == nil { 70 | return a.imagePullSecrets 71 | } 72 | return ips 73 | } 74 | 75 | func (a *componentAccessorImpl) HostNetwork() bool { 76 | hostNetwork := a.ComponentSpec.HostNetwork 77 | if hostNetwork == nil { 78 | hostNetwork = a.hostNetwork 79 | } 80 | if hostNetwork == nil { 81 | return false 82 | } 83 | return *hostNetwork 84 | } 85 | 86 | func (a *componentAccessorImpl) Affinity() *corev1.Affinity { 87 | affi := a.ComponentSpec.Affinity 88 | if affi == nil { 89 | affi = a.affinity 90 | } 91 | return affi 92 | } 93 | 94 | func (a *componentAccessorImpl) PriorityClassName() *string { 95 | pcn := a.ComponentSpec.PriorityClassName 96 | if pcn == nil { 97 | pcn = a.priorityClassName 98 | } 99 | return pcn 100 | } 101 | 102 | func (a *componentAccessorImpl) SchedulerName() string { 103 | pcn := a.ComponentSpec.SchedulerName 104 | if pcn == nil { 105 | pcn = &a.schedulerName 106 | } 107 | return *pcn 108 | } 109 | 110 | func (a *componentAccessorImpl) NodeSelector() map[string]string { 111 | sel := map[string]string{} 112 | for k, v := range a.clusterNodeSelector { 113 | sel[k] = v 114 | } 115 | for k, v := range a.ComponentSpec.NodeSelector { 116 | sel[k] = v 117 | } 118 | return sel 119 | } 120 | 121 | func (a *componentAccessorImpl) Annotations() map[string]string { 122 | anno := map[string]string{} 123 | for k, v := range a.clusterAnnotations { 124 | anno[k] = v 125 | } 126 | for k, v := range a.ComponentSpec.Annotations { 127 | anno[k] = v 128 | } 129 | return anno 130 | } 131 | 132 | func (a *componentAccessorImpl) Tolerations() []corev1.Toleration { 133 | tols := a.ComponentSpec.Tolerations 134 | if len(tols) == 0 { 135 | tols = a.tolerations 136 | } 137 | return tols 138 | } 139 | 140 | func (a *componentAccessorImpl) DNSPolicy() corev1.DNSPolicy { 141 | dnsPolicy := corev1.DNSClusterFirst // same as kubernetes default 142 | if a.HostNetwork() { 143 | dnsPolicy = corev1.DNSClusterFirstWithHostNet 144 | } 145 | return dnsPolicy 146 | } 147 | 148 | func (a *componentAccessorImpl) BuildPodSpec() corev1.PodSpec { 149 | spec := corev1.PodSpec{ 150 | SchedulerName: a.SchedulerName(), 151 | Affinity: a.Affinity(), 152 | NodeSelector: a.NodeSelector(), 153 | HostNetwork: a.HostNetwork(), 154 | RestartPolicy: corev1.RestartPolicyAlways, 155 | Tolerations: a.Tolerations(), 156 | } 157 | if a.PriorityClassName() != nil { 158 | spec.PriorityClassName = *a.PriorityClassName() 159 | } 160 | if a.ImagePullSecrets() != nil { 161 | spec.ImagePullSecrets = a.ImagePullSecrets() 162 | } 163 | if a.TerminationGracePeriodSeconds() != nil { 164 | spec.TerminationGracePeriodSeconds = a.TerminationGracePeriodSeconds() 165 | } 166 | return spec 167 | } 168 | 169 | func (a *componentAccessorImpl) Env() []corev1.EnvVar { 170 | return a.ComponentSpec.Env 171 | } 172 | 173 | func (a *componentAccessorImpl) TerminationGracePeriodSeconds() *int64 { 174 | return a.ComponentSpec.TerminationGracePeriodSeconds 175 | } 176 | 177 | func buildSeaweedComponentAccessor(spec *SeaweedSpec, componentSpec *ComponentSpec) ComponentAccessor { 178 | return &componentAccessorImpl{ 179 | imagePullPolicy: spec.ImagePullPolicy, 180 | imagePullSecrets: spec.ImagePullSecrets, 181 | hostNetwork: spec.HostNetwork, 182 | affinity: spec.Affinity, 183 | schedulerName: spec.SchedulerName, 184 | clusterNodeSelector: spec.NodeSelector, 185 | clusterAnnotations: spec.Annotations, 186 | tolerations: spec.Tolerations, 187 | statefulSetUpdateStrategy: spec.StatefulSetUpdateStrategy, 188 | 189 | ComponentSpec: componentSpec, 190 | } 191 | } 192 | 193 | // BaseMasterSpec provides merged spec of masters 194 | func (s *Seaweed) BaseMasterSpec() ComponentAccessor { 195 | return buildSeaweedComponentAccessor(&s.Spec, &s.Spec.Master.ComponentSpec) 196 | } 197 | 198 | // BaseFilerSpec provides merged spec of filers 199 | func (s *Seaweed) BaseFilerSpec() ComponentAccessor { 200 | return buildSeaweedComponentAccessor(&s.Spec, &s.Spec.Filer.ComponentSpec) 201 | } 202 | 203 | // BaseVolumeSpec provides merged spec of volumes 204 | func (s *Seaweed) BaseVolumeSpec() ComponentAccessor { 205 | return buildSeaweedComponentAccessor(&s.Spec, &s.Spec.Volume.ComponentSpec) 206 | } 207 | -------------------------------------------------------------------------------- /internal/controller/controller_master.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | appsv1 "k8s.io/api/apps/v1" 8 | corev1 "k8s.io/api/core/v1" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | ctrl "sigs.k8s.io/controller-runtime" 11 | "sigs.k8s.io/controller-runtime/pkg/client" 12 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 13 | 14 | seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 15 | "github.com/seaweedfs/seaweedfs-operator/internal/controller/label" 16 | ) 17 | 18 | func (r *SeaweedReconciler) ensureMaster(seaweedCR *seaweedv1.Seaweed) (done bool, result ctrl.Result, err error) { 19 | _ = context.Background() 20 | _ = r.Log.WithValues("seaweed", seaweedCR.Name) 21 | 22 | if done, result, err = r.ensureMasterPeerService(seaweedCR); done { 23 | return 24 | } 25 | 26 | if done, result, err = r.ensureMasterService(seaweedCR); done { 27 | return 28 | } 29 | 30 | if done, result, err = r.ensureMasterConfigMap(seaweedCR); done { 31 | return 32 | } 33 | 34 | if done, result, err = r.ensureMasterStatefulSet(seaweedCR); done { 35 | return 36 | } 37 | 38 | if seaweedCR.Spec.Master.ConcurrentStart == nil || !*seaweedCR.Spec.Master.ConcurrentStart { 39 | if done, result, err = r.waitForMasterStatefulSet(seaweedCR); done { 40 | return 41 | } 42 | } 43 | 44 | if seaweedCR.Spec.Master.MetricsPort != nil { 45 | if done, result, err = r.ensureMasterServiceMonitor(seaweedCR); done { 46 | return 47 | } 48 | } 49 | 50 | return 51 | } 52 | 53 | func (r *SeaweedReconciler) waitForMasterStatefulSet(seaweedCR *seaweedv1.Seaweed) (bool, ctrl.Result, error) { 54 | log := r.Log.WithValues("sw-master-statefulset", seaweedCR.Name) 55 | 56 | podList := &corev1.PodList{} 57 | listOpts := []client.ListOption{ 58 | client.InNamespace(seaweedCR.Namespace), 59 | client.MatchingLabels(labelsForMaster(seaweedCR.Name)), 60 | } 61 | if err := r.List(context.Background(), podList, listOpts...); err != nil { 62 | log.Error(err, "Failed to list master pods", "namespace", seaweedCR.Namespace, "name", seaweedCR.Name) 63 | return true, ctrl.Result{RequeueAfter: 3 * time.Second}, nil 64 | } 65 | 66 | log.Info("pods", "count", len(podList.Items)) 67 | runningCounter := 0 68 | for _, pod := range podList.Items { 69 | if pod.Status.Phase == corev1.PodRunning { 70 | for _, containerStatus := range pod.Status.ContainerStatuses { 71 | if containerStatus.Ready { 72 | runningCounter++ 73 | } 74 | log.Info("pod", "name", pod.Name, "containerStatus", containerStatus) 75 | } 76 | } else { 77 | log.Info("pod", "name", pod.Name, "status", pod.Status) 78 | } 79 | } 80 | 81 | if runningCounter < int(seaweedCR.Spec.Master.Replicas)/2+1 { 82 | log.Info("some masters are not ready", "missing", int(seaweedCR.Spec.Master.Replicas)-runningCounter) 83 | return true, ctrl.Result{RequeueAfter: 3 * time.Second}, nil 84 | } 85 | 86 | log.Info("masters are ready") 87 | return ReconcileResult(nil) 88 | 89 | } 90 | 91 | func (r *SeaweedReconciler) ensureMasterStatefulSet(seaweedCR *seaweedv1.Seaweed) (bool, ctrl.Result, error) { 92 | log := r.Log.WithValues("sw-master-statefulset", seaweedCR.Name) 93 | 94 | masterStatefulSet := r.createMasterStatefulSet(seaweedCR) 95 | if err := controllerutil.SetControllerReference(seaweedCR, masterStatefulSet, r.Scheme); err != nil { 96 | return ReconcileResult(err) 97 | } 98 | _, err := r.CreateOrUpdate(masterStatefulSet, func(existing, desired runtime.Object) error { 99 | existingStatefulSet := existing.(*appsv1.StatefulSet) 100 | desiredStatefulSet := desired.(*appsv1.StatefulSet) 101 | 102 | existingStatefulSet.Spec.Replicas = desiredStatefulSet.Spec.Replicas 103 | existingStatefulSet.Spec.Template.ObjectMeta = desiredStatefulSet.Spec.Template.ObjectMeta 104 | existingStatefulSet.Spec.Template.Spec = desiredStatefulSet.Spec.Template.Spec 105 | return nil 106 | }) 107 | log.Info("ensure master stateful set " + masterStatefulSet.Name) 108 | return ReconcileResult(err) 109 | } 110 | 111 | func (r *SeaweedReconciler) ensureMasterConfigMap(seaweedCR *seaweedv1.Seaweed) (bool, ctrl.Result, error) { 112 | log := r.Log.WithValues("sw-master-configmap", seaweedCR.Name) 113 | 114 | masterConfigMap := r.createMasterConfigMap(seaweedCR) 115 | if err := controllerutil.SetControllerReference(seaweedCR, masterConfigMap, r.Scheme); err != nil { 116 | return ReconcileResult(err) 117 | } 118 | _, err := r.CreateOrUpdateConfigMap(masterConfigMap) 119 | 120 | log.Info("Get master ConfigMap " + masterConfigMap.Name) 121 | return ReconcileResult(err) 122 | } 123 | 124 | func (r *SeaweedReconciler) ensureMasterService(seaweedCR *seaweedv1.Seaweed) (bool, ctrl.Result, error) { 125 | log := r.Log.WithValues("sw-master-service", seaweedCR.Name) 126 | 127 | masterService := r.createMasterService(seaweedCR) 128 | if err := controllerutil.SetControllerReference(seaweedCR, masterService, r.Scheme); err != nil { 129 | return ReconcileResult(err) 130 | } 131 | _, err := r.CreateOrUpdateService(masterService) 132 | 133 | log.Info("Get master service " + masterService.Name) 134 | return ReconcileResult(err) 135 | } 136 | 137 | func (r *SeaweedReconciler) ensureMasterPeerService(seaweedCR *seaweedv1.Seaweed) (bool, ctrl.Result, error) { 138 | log := r.Log.WithValues("sw-master-peer-service", seaweedCR.Name) 139 | 140 | masterPeerService := r.createMasterPeerService(seaweedCR) 141 | if err := controllerutil.SetControllerReference(seaweedCR, masterPeerService, r.Scheme); err != nil { 142 | return ReconcileResult(err) 143 | } 144 | _, err := r.CreateOrUpdateService(masterPeerService) 145 | 146 | log.Info("Get master peer service " + masterPeerService.Name) 147 | return ReconcileResult(err) 148 | } 149 | 150 | func (r *SeaweedReconciler) ensureMasterServiceMonitor(seaweedCR *seaweedv1.Seaweed) (bool, ctrl.Result, error) { 151 | log := r.Log.WithValues("sw-master-servicemonitor", seaweedCR.Name) 152 | 153 | masterServiceMonitor := r.createMasterServiceMonitor(seaweedCR) 154 | if err := controllerutil.SetControllerReference(seaweedCR, masterServiceMonitor, r.Scheme); err != nil { 155 | return ReconcileResult(err) 156 | } 157 | _, err := r.CreateOrUpdateServiceMonitor(masterServiceMonitor) 158 | 159 | log.Info("Get master service monitor " + masterServiceMonitor.Name) 160 | return ReconcileResult(err) 161 | } 162 | 163 | func labelsForMaster(name string) map[string]string { 164 | return map[string]string{ 165 | label.ManagedByLabelKey: "seaweedfs-operator", 166 | label.NameLabelKey: "seaweedfs", 167 | label.ComponentLabelKey: "master", 168 | label.InstanceLabelKey: name, 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /internal/controller/helper.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | corev1 "k8s.io/api/core/v1" 8 | ctrl "sigs.k8s.io/controller-runtime" 9 | 10 | seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 11 | ) 12 | 13 | const ( 14 | masterPeerAddressPattern = "%s-master-%d.%s-master-peer.%s:9333" 15 | ) 16 | 17 | var ( 18 | kubernetesEnvVars = []corev1.EnvVar{ 19 | { 20 | Name: "POD_IP", 21 | ValueFrom: &corev1.EnvVarSource{ 22 | FieldRef: &corev1.ObjectFieldSelector{ 23 | FieldPath: "status.podIP", 24 | }, 25 | }, 26 | }, 27 | { 28 | Name: "POD_NAME", 29 | ValueFrom: &corev1.EnvVarSource{ 30 | FieldRef: &corev1.ObjectFieldSelector{ 31 | FieldPath: "metadata.name", 32 | }, 33 | }, 34 | }, 35 | { 36 | Name: "NAMESPACE", 37 | ValueFrom: &corev1.EnvVarSource{ 38 | FieldRef: &corev1.ObjectFieldSelector{ 39 | FieldPath: "metadata.namespace", 40 | }, 41 | }, 42 | }, 43 | } 44 | ) 45 | 46 | func ReconcileResult(err error) (bool, ctrl.Result, error) { 47 | if err != nil { 48 | return true, ctrl.Result{}, err 49 | } 50 | return false, ctrl.Result{}, nil 51 | } 52 | 53 | func getMasterAddresses(namespace string, name string, replicas int32) []string { 54 | peersAddresses := make([]string, 0, replicas) 55 | for i := int32(0); i < replicas; i++ { 56 | peersAddresses = append(peersAddresses, fmt.Sprintf(masterPeerAddressPattern, name, i, name, namespace)) 57 | } 58 | return peersAddresses 59 | } 60 | 61 | func getMasterPeersString(m *seaweedv1.Seaweed) string { 62 | return strings.Join(getMasterAddresses(m.Namespace, m.Name, m.Spec.Master.Replicas), ",") 63 | } 64 | 65 | // Note: IAM is now embedded in S3 by default (on the same port as S3: FilerS3Port). 66 | // The getIAMPort function has been removed since standalone IAM is no longer supported. 67 | 68 | func copyAnnotations(src map[string]string) map[string]string { 69 | if src == nil { 70 | return nil 71 | } 72 | dst := map[string]string{} 73 | for k, v := range src { 74 | dst[k] = v 75 | } 76 | return dst 77 | } 78 | 79 | // mergeAnnotations merges cluster-level annotations with component-level annotations 80 | // Component-level annotations take precedence over cluster-level ones 81 | func mergeAnnotations(clusterAnnotations, componentAnnotations map[string]string) map[string]string { 82 | if clusterAnnotations == nil && componentAnnotations == nil { 83 | return nil 84 | } 85 | 86 | merged := map[string]string{} 87 | 88 | // Add cluster-level annotations first 89 | for k, v := range clusterAnnotations { 90 | merged[k] = v 91 | } 92 | 93 | // Override with component-level annotations 94 | for k, v := range componentAnnotations { 95 | merged[k] = v 96 | } 97 | 98 | return merged 99 | } 100 | 101 | // mergeNodeSelector merges cluster-level nodeSelector with component-level nodeSelector 102 | // Component-level nodeSelector takes precedence over cluster-level ones 103 | func mergeNodeSelector(clusterNodeSelector, componentNodeSelector map[string]string) map[string]string { 104 | if clusterNodeSelector == nil && componentNodeSelector == nil { 105 | return nil 106 | } 107 | 108 | merged := map[string]string{} 109 | 110 | // Add cluster-level nodeSelector first 111 | for k, v := range clusterNodeSelector { 112 | merged[k] = v 113 | } 114 | 115 | // Override with component-level nodeSelector 116 | for k, v := range componentNodeSelector { 117 | merged[k] = v 118 | } 119 | 120 | return merged 121 | } 122 | 123 | // filterContainerResources removes storage resources that are not valid for container specifications 124 | // while keeping resources like ephemeral-storage that are valid for containers 125 | func filterContainerResources(resources corev1.ResourceRequirements) corev1.ResourceRequirements { 126 | filtered := corev1.ResourceRequirements{} 127 | 128 | if resources.Requests != nil { 129 | filtered.Requests = corev1.ResourceList{} 130 | for resource, quantity := range resources.Requests { 131 | // Exclude storage resources that are only valid for PVCs 132 | if resource != corev1.ResourceStorage { 133 | filtered.Requests[resource] = quantity 134 | } 135 | } 136 | } 137 | 138 | if resources.Limits != nil { 139 | filtered.Limits = corev1.ResourceList{} 140 | for resource, quantity := range resources.Limits { 141 | // Exclude storage resources that are only valid for PVCs 142 | if resource != corev1.ResourceStorage { 143 | filtered.Limits[resource] = quantity 144 | } 145 | } 146 | } 147 | 148 | return filtered 149 | } 150 | 151 | // getStorageClassName returns the storage class name with fallback logic 152 | func getStorageClassName(m *seaweedv1.Seaweed, topologySpec *seaweedv1.VolumeTopologySpec) *string { 153 | if topologySpec != nil && topologySpec.StorageClassName != nil { 154 | return topologySpec.StorageClassName 155 | } 156 | if m.Spec.Volume != nil && m.Spec.Volume.StorageClassName != nil { 157 | return m.Spec.Volume.StorageClassName 158 | } 159 | return nil 160 | } 161 | 162 | // getResourceRequirements returns the resource requirements with fallback logic 163 | func getResourceRequirements(m *seaweedv1.Seaweed, topologySpec *seaweedv1.VolumeTopologySpec) corev1.ResourceRequirements { 164 | // Start with base resources from spec.volume, if available 165 | resources := corev1.ResourceRequirements{} 166 | if m.Spec.Volume != nil { 167 | resources = m.Spec.Volume.ResourceRequirements 168 | } 169 | 170 | // If no topology spec, return base 171 | if topologySpec == nil { 172 | return resources 173 | } 174 | 175 | // Override with topology-specific resources if they are provided 176 | if len(topologySpec.ResourceRequirements.Requests) > 0 { 177 | resources.Requests = topologySpec.ResourceRequirements.Requests 178 | } 179 | if len(topologySpec.ResourceRequirements.Limits) > 0 { 180 | resources.Limits = topologySpec.ResourceRequirements.Limits 181 | } 182 | 183 | return resources 184 | } 185 | 186 | // getMetricsPort returns the metrics port with fallback logic 187 | func getMetricsPort(m *seaweedv1.Seaweed, topologySpec *seaweedv1.VolumeTopologySpec) *int32 { 188 | if topologySpec != nil && topologySpec.MetricsPort != nil { 189 | return topologySpec.MetricsPort 190 | } 191 | if m.Spec.Volume != nil && m.Spec.Volume.MetricsPort != nil { 192 | return m.Spec.Volume.MetricsPort 193 | } 194 | return nil 195 | } 196 | 197 | // getServiceSpec returns the service spec with fallback logic 198 | func getServiceSpec(m *seaweedv1.Seaweed, topologySpec *seaweedv1.VolumeTopologySpec) *seaweedv1.ServiceSpec { 199 | if topologySpec != nil && topologySpec.Service != nil { 200 | return topologySpec.Service 201 | } 202 | if m.Spec.Volume != nil && m.Spec.Volume.Service != nil { 203 | return m.Spec.Volume.Service 204 | } 205 | return nil 206 | } 207 | 208 | // getVolumeServerConfigValue returns volume server config values with fallback logic 209 | func getVolumeServerConfigValue[T any](topologyValue, volumeValue *T) *T { 210 | if topologyValue != nil { 211 | return topologyValue 212 | } 213 | return volumeValue 214 | } 215 | -------------------------------------------------------------------------------- /deploy/helm/templates/webhook/job-update-webhook-certificates.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.webhook.enabled -}} 2 | --- 3 | apiVersion: batch/v1 4 | kind: Job 5 | metadata: 6 | name: {{ include "seaweedfs-operator.fullname" . }}-create-webhook-certificates 7 | namespace: {{ .Release.Namespace }} 8 | annotations: 9 | "helm.sh/hook": pre-install,pre-upgrade 10 | "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded 11 | spec: 12 | template: 13 | spec: 14 | serviceAccountName: {{ include "seaweedfs-operator.fullname" . }}-update-webhook-certificates 15 | {{- include "seaweedfs-operator.webhookPodSecurityContext" . | nindent 6 }} 16 | containers: 17 | - name: certgen 18 | image: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v20231011-8b53cabe0 19 | {{- include "seaweedfs-operator.webhookContainerSecurityContext" . | nindent 8 }} 20 | args: 21 | - create 22 | - --host={{ include "seaweedfs-operator.fullname" . }}-webhook,{{ include "seaweedfs-operator.fullname" . }}-webhook.{{ .Release.Namespace }}.svc 23 | - --namespace=$(POD_NAMESPACE) 24 | - --secret-name={{ include "seaweedfs-operator.fullname" . }}-webhook-server-cert 25 | env: 26 | - name: POD_NAMESPACE 27 | valueFrom: 28 | fieldRef: 29 | fieldPath: metadata.namespace 30 | restartPolicy: OnFailure 31 | 32 | --- 33 | apiVersion: batch/v1 34 | kind: Job 35 | metadata: 36 | name: {{ include "seaweedfs-operator.fullname" . }}-patch-mutating-webhook 37 | namespace: {{ .Release.Namespace }} 38 | annotations: 39 | "helm.sh/hook": post-install,post-upgrade 40 | "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded 41 | spec: 42 | template: 43 | spec: 44 | serviceAccountName: {{ include "seaweedfs-operator.fullname" . }}-update-webhook-certificates 45 | {{- include "seaweedfs-operator.webhookPodSecurityContext" . | nindent 6 }} 46 | initContainers: 47 | {{- include "seaweedfs-operator.webhookWaitInitContainer" (dict "Chart" .Chart "Values" .Values "Release" .Release "webhookPath" (include "seaweedfs-operator.mutatingWebhookPath" .)) | nindent 8 }} 48 | containers: 49 | - name: certgen 50 | image: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v20231011-8b53cabe0 51 | {{- include "seaweedfs-operator.webhookContainerSecurityContext" . | nindent 8 }} 52 | args: 53 | - patch 54 | - --webhook-name=mutating-webhook-configuration 55 | - --namespace=$(POD_NAMESPACE) 56 | - --patch-mutating=true 57 | - --patch-validating=false 58 | - --secret-name={{ include "seaweedfs-operator.fullname" . }}-webhook-server-cert 59 | - --patch-failure-policy=Fail 60 | env: 61 | - name: POD_NAMESPACE 62 | valueFrom: 63 | fieldRef: 64 | fieldPath: metadata.namespace 65 | restartPolicy: OnFailure 66 | 67 | --- 68 | apiVersion: batch/v1 69 | kind: Job 70 | metadata: 71 | name: {{ include "seaweedfs-operator.fullname" . }}-patch-validating-webhook 72 | namespace: {{ .Release.Namespace }} 73 | annotations: 74 | "helm.sh/hook": post-install,post-upgrade 75 | "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded 76 | spec: 77 | template: 78 | spec: 79 | serviceAccountName: {{ include "seaweedfs-operator.fullname" . }}-update-webhook-certificates 80 | {{- include "seaweedfs-operator.webhookPodSecurityContext" . | nindent 6 }} 81 | initContainers: 82 | {{- include "seaweedfs-operator.webhookWaitInitContainer" (dict "Chart" .Chart "Values" .Values "Release" .Release "webhookPath" (include "seaweedfs-operator.validatingWebhookPath" .)) | nindent 8 }} 83 | containers: 84 | - name: certgen 85 | image: registry.k8s.io/ingress-nginx/kube-webhook-certgen:v20231011-8b53cabe0 86 | {{- include "seaweedfs-operator.webhookContainerSecurityContext" . | nindent 8 }} 87 | args: 88 | - patch 89 | - --webhook-name=validating-webhook-configuration 90 | - --namespace=$(POD_NAMESPACE) 91 | - --patch-mutating=false 92 | - --patch-validating=true 93 | - --secret-name={{ include "seaweedfs-operator.fullname" . }}-webhook-server-cert 94 | - --patch-failure-policy=Fail 95 | env: 96 | - name: POD_NAMESPACE 97 | valueFrom: 98 | fieldRef: 99 | fieldPath: metadata.namespace 100 | restartPolicy: OnFailure 101 | --- 102 | apiVersion: v1 103 | kind: ServiceAccount 104 | metadata: 105 | name: {{ include "seaweedfs-operator.fullname" . }}-update-webhook-certificates 106 | namespace: {{ .Release.Namespace }} 107 | annotations: 108 | "helm.sh/hook": pre-install,pre-upgrade 109 | "helm.sh/hook-weight": "-10" 110 | --- 111 | apiVersion: rbac.authorization.k8s.io/v1 112 | kind: Role 113 | metadata: 114 | name: {{ include "seaweedfs-operator.fullname" . }}-update-webhook-certificates 115 | annotations: 116 | "helm.sh/hook": pre-install,pre-upgrade 117 | "helm.sh/hook-weight": "-10" 118 | rules: 119 | - apiGroups: 120 | - "" 121 | resources: 122 | - secrets 123 | verbs: 124 | - get 125 | - create 126 | - update 127 | --- 128 | apiVersion: rbac.authorization.k8s.io/v1 129 | kind: RoleBinding 130 | metadata: 131 | name: {{ include "seaweedfs-operator.fullname" . }}-update-webhook-certificates 132 | annotations: 133 | "helm.sh/hook": pre-install,pre-upgrade 134 | "helm.sh/hook-weight": "-9" 135 | roleRef: 136 | apiGroup: rbac.authorization.k8s.io 137 | kind: Role 138 | name: {{ include "seaweedfs-operator.fullname" . }}-update-webhook-certificates 139 | subjects: 140 | - kind: ServiceAccount 141 | name: {{ include "seaweedfs-operator.fullname" . }}-update-webhook-certificates 142 | namespace: {{ .Release.Namespace }} 143 | --- 144 | apiVersion: rbac.authorization.k8s.io/v1 145 | kind: ClusterRole 146 | metadata: 147 | name: {{ include "seaweedfs-operator.fullname" . }}-update-webhook-certificates 148 | annotations: 149 | "helm.sh/hook": pre-install,pre-upgrade 150 | "helm.sh/hook-weight": "-10" 151 | rules: 152 | - apiGroups: 153 | - admissionregistration.k8s.io 154 | resources: 155 | - validatingwebhookconfigurations 156 | - mutatingwebhookconfigurations 157 | verbs: 158 | - get 159 | - update 160 | --- 161 | apiVersion: rbac.authorization.k8s.io/v1 162 | kind: ClusterRoleBinding 163 | metadata: 164 | name: {{ include "seaweedfs-operator.fullname" . }}-update-webhook-certificates 165 | annotations: 166 | "helm.sh/hook": pre-install,pre-upgrade 167 | "helm.sh/hook-weight": "-9" 168 | roleRef: 169 | apiGroup: rbac.authorization.k8s.io 170 | kind: ClusterRole 171 | name: {{ include "seaweedfs-operator.fullname" . }}-update-webhook-certificates 172 | subjects: 173 | - kind: ServiceAccount 174 | name: {{ include "seaweedfs-operator.fullname" . }}-update-webhook-certificates 175 | namespace: {{ .Release.Namespace }} 176 | {{- end -}} 177 | -------------------------------------------------------------------------------- /internal/controller/controller_volume_service.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/seaweedfs/seaweedfs-operator/internal/controller/label" 7 | corev1 "k8s.io/api/core/v1" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/util/intstr" 10 | 11 | seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 12 | ) 13 | 14 | func (r *SeaweedReconciler) createVolumeServerPeerService(m *seaweedv1.Seaweed) *corev1.Service { 15 | labels := labelsForVolumeServer(m.Name) 16 | ports := []corev1.ServicePort{ 17 | { 18 | Name: "volume-http", 19 | Protocol: corev1.Protocol("TCP"), 20 | Port: seaweedv1.VolumeHTTPPort, 21 | TargetPort: intstr.FromInt(seaweedv1.VolumeHTTPPort), 22 | }, 23 | { 24 | Name: "volume-grpc", 25 | Protocol: corev1.Protocol("TCP"), 26 | Port: seaweedv1.VolumeGRPCPort, 27 | TargetPort: intstr.FromInt(seaweedv1.VolumeGRPCPort), 28 | }, 29 | } 30 | if m.Spec.Volume.MetricsPort != nil { 31 | ports = append(ports, corev1.ServicePort{ 32 | Name: "volume-metrics", 33 | Protocol: corev1.Protocol("TCP"), 34 | Port: *m.Spec.Volume.MetricsPort, 35 | TargetPort: intstr.FromInt(int(*m.Spec.Volume.MetricsPort)), 36 | }) 37 | } 38 | 39 | dep := &corev1.Service{ 40 | ObjectMeta: metav1.ObjectMeta{ 41 | Name: m.Name + "-volume-peer", 42 | Namespace: m.Namespace, 43 | Labels: labels, 44 | Annotations: map[string]string{ 45 | "service.alpha.kubernetes.io/tolerate-unready-endpoints": "true", 46 | }, 47 | }, 48 | Spec: corev1.ServiceSpec{ 49 | ClusterIP: "None", 50 | PublishNotReadyAddresses: true, 51 | Ports: ports, 52 | Selector: labels, 53 | }, 54 | } 55 | return dep 56 | } 57 | func (r *SeaweedReconciler) createVolumeServerService(m *seaweedv1.Seaweed, i int) *corev1.Service { 58 | labels := labelsForVolumeServer(m.Name) 59 | serviceName := fmt.Sprintf("%s-volume-%d", m.Name, i) 60 | labels[label.PodName] = serviceName 61 | ports := []corev1.ServicePort{ 62 | { 63 | Name: "volume-http", 64 | Protocol: corev1.Protocol("TCP"), 65 | Port: seaweedv1.VolumeHTTPPort, 66 | TargetPort: intstr.FromInt(seaweedv1.VolumeHTTPPort), 67 | }, 68 | { 69 | Name: "volume-grpc", 70 | Protocol: corev1.Protocol("TCP"), 71 | Port: seaweedv1.VolumeGRPCPort, 72 | TargetPort: intstr.FromInt(seaweedv1.VolumeGRPCPort), 73 | }, 74 | } 75 | if m.Spec.Volume.MetricsPort != nil { 76 | ports = append(ports, corev1.ServicePort{ 77 | Name: "volume-metrics", 78 | Protocol: corev1.Protocol("TCP"), 79 | Port: *m.Spec.Volume.MetricsPort, 80 | TargetPort: intstr.FromInt(int(*m.Spec.Volume.MetricsPort)), 81 | }) 82 | } 83 | 84 | dep := &corev1.Service{ 85 | ObjectMeta: metav1.ObjectMeta{ 86 | Name: serviceName, 87 | Namespace: m.Namespace, 88 | Labels: labels, 89 | Annotations: map[string]string{ 90 | "service.alpha.kubernetes.io/tolerate-unready-endpoints": "true", 91 | }, 92 | }, 93 | Spec: corev1.ServiceSpec{ 94 | PublishNotReadyAddresses: true, 95 | Ports: ports, 96 | Selector: labels, 97 | }, 98 | } 99 | 100 | if m.Spec.Volume.Service != nil { 101 | svcSpec := m.Spec.Volume.Service 102 | dep.Annotations = copyAnnotations(svcSpec.Annotations) 103 | 104 | if svcSpec.Type != "" { 105 | dep.Spec.Type = svcSpec.Type 106 | } 107 | 108 | if svcSpec.ClusterIP != nil { 109 | dep.Spec.ClusterIP = *svcSpec.ClusterIP 110 | } 111 | 112 | if svcSpec.LoadBalancerIP != nil { 113 | dep.Spec.LoadBalancerIP = *svcSpec.LoadBalancerIP 114 | } 115 | } 116 | 117 | return dep 118 | } 119 | 120 | func (r *SeaweedReconciler) createVolumeServerTopologyPeerService(m *seaweedv1.Seaweed, topologyName string) *corev1.Service { 121 | labels := labelsForVolumeServerTopology(m.Name, topologyName) 122 | labels["seaweedfs/service-role"] = "peer" 123 | ports := []corev1.ServicePort{ 124 | { 125 | Name: "volume-http", 126 | Protocol: corev1.Protocol("TCP"), 127 | Port: seaweedv1.VolumeHTTPPort, 128 | TargetPort: intstr.FromInt(seaweedv1.VolumeHTTPPort), 129 | }, 130 | { 131 | Name: "volume-grpc", 132 | Protocol: corev1.Protocol("TCP"), 133 | Port: seaweedv1.VolumeGRPCPort, 134 | TargetPort: intstr.FromInt(seaweedv1.VolumeGRPCPort), 135 | }, 136 | } 137 | 138 | // Get metrics port from topology spec 139 | topologySpec := m.Spec.VolumeTopology[topologyName] 140 | if topologySpec.MetricsPort != nil { 141 | ports = append(ports, corev1.ServicePort{ 142 | Name: "volume-metrics", 143 | Protocol: corev1.Protocol("TCP"), 144 | Port: *topologySpec.MetricsPort, 145 | TargetPort: intstr.FromInt(int(*topologySpec.MetricsPort)), 146 | }) 147 | } 148 | 149 | dep := &corev1.Service{ 150 | ObjectMeta: metav1.ObjectMeta{ 151 | Name: fmt.Sprintf("%s-volume-%s-peer", m.Name, topologyName), 152 | Namespace: m.Namespace, 153 | Labels: labels, 154 | Annotations: map[string]string{ 155 | "service.alpha.kubernetes.io/tolerate-unready-endpoints": "true", 156 | }, 157 | }, 158 | Spec: corev1.ServiceSpec{ 159 | ClusterIP: "None", 160 | PublishNotReadyAddresses: true, 161 | Ports: ports, 162 | Selector: labels, 163 | }, 164 | } 165 | return dep 166 | } 167 | 168 | func (r *SeaweedReconciler) createVolumeServerTopologyService(m *seaweedv1.Seaweed, topologyName string, i int) *corev1.Service { 169 | labels := labelsForVolumeServerTopology(m.Name, topologyName) 170 | serviceName := fmt.Sprintf("%s-volume-%s-%d", m.Name, topologyName, i) 171 | ports := []corev1.ServicePort{ 172 | { 173 | Name: "volume-http", 174 | Protocol: corev1.Protocol("TCP"), 175 | Port: seaweedv1.VolumeHTTPPort, 176 | TargetPort: intstr.FromInt(seaweedv1.VolumeHTTPPort), 177 | }, 178 | { 179 | Name: "volume-grpc", 180 | Protocol: corev1.Protocol("TCP"), 181 | Port: seaweedv1.VolumeGRPCPort, 182 | TargetPort: intstr.FromInt(seaweedv1.VolumeGRPCPort), 183 | }, 184 | } 185 | 186 | // Get metrics port from topology spec 187 | topologySpec := m.Spec.VolumeTopology[topologyName] 188 | if topologySpec.MetricsPort != nil { 189 | ports = append(ports, corev1.ServicePort{ 190 | Name: "volume-metrics", 191 | Protocol: corev1.Protocol("TCP"), 192 | Port: *topologySpec.MetricsPort, 193 | TargetPort: intstr.FromInt(int(*topologySpec.MetricsPort)), 194 | }) 195 | } 196 | 197 | dep := &corev1.Service{ 198 | ObjectMeta: metav1.ObjectMeta{ 199 | Name: serviceName, 200 | Namespace: m.Namespace, 201 | Labels: labels, 202 | Annotations: map[string]string{ 203 | "service.alpha.kubernetes.io/tolerate-unready-endpoints": "true", 204 | }, 205 | }, 206 | Spec: corev1.ServiceSpec{ 207 | PublishNotReadyAddresses: true, 208 | Ports: ports, 209 | // Use StatefulSet pod name label to select the specific pod 210 | Selector: map[string]string{ 211 | "statefulset.kubernetes.io/pod-name": serviceName, 212 | }, 213 | }, 214 | } 215 | 216 | // Apply service specification with fallback logic 217 | svcSpec := getServiceSpec(m, topologySpec) 218 | if svcSpec != nil { 219 | dep.Annotations = copyAnnotations(svcSpec.Annotations) 220 | 221 | if svcSpec.Type != "" { 222 | dep.Spec.Type = svcSpec.Type 223 | } 224 | 225 | if svcSpec.ClusterIP != nil { 226 | dep.Spec.ClusterIP = *svcSpec.ClusterIP 227 | } 228 | 229 | if svcSpec.LoadBalancerIP != nil { 230 | dep.Spec.LoadBalancerIP = *svcSpec.LoadBalancerIP 231 | } 232 | } 233 | 234 | return dep 235 | } 236 | -------------------------------------------------------------------------------- /internal/controller/controller_filer_statefulset.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | appsv1 "k8s.io/api/apps/v1" 8 | corev1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/util/intstr" 11 | 12 | seaweedv1 "github.com/seaweedfs/seaweedfs-operator/api/v1" 13 | ) 14 | 15 | func buildFilerStartupScript(m *seaweedv1.Seaweed) string { 16 | commands := []string{"weed", "-logtostderr=true", "filer"} 17 | commands = append(commands, fmt.Sprintf("-port=%d", seaweedv1.FilerHTTPPort)) 18 | commands = append(commands, fmt.Sprintf("-ip=$(POD_NAME).%s-filer-peer.%s", m.Name, m.Namespace)) 19 | commands = append(commands, fmt.Sprintf("-master=%s", getMasterPeersString(m))) 20 | if s3Config := m.Spec.Filer.S3; s3Config != nil && s3Config.Enabled { 21 | commands = append(commands, "-s3") 22 | if s3Config.ConfigSecret != nil && s3Config.ConfigSecret.Name != "" { 23 | commands = append(commands, "-s3.config=/etc/sw/"+s3Config.ConfigSecret.Key) 24 | } 25 | // IAM is now embedded in S3 by default (enabled with -iam=true, which is the default) 26 | // Only add -iam=false if explicitly disabled 27 | if !m.Spec.Filer.IAM { 28 | commands = append(commands, "-iam=false") 29 | } 30 | // Note: IAM API is available on the same port as S3 (FilerS3Port) when embedded 31 | } 32 | if m.Spec.Filer.MetricsPort != nil { 33 | commands = append(commands, fmt.Sprintf("-metricsPort=%d", *m.Spec.Filer.MetricsPort)) 34 | } 35 | 36 | return strings.Join(commands, " ") 37 | } 38 | 39 | func (r *SeaweedReconciler) createFilerStatefulSet(m *seaweedv1.Seaweed) *appsv1.StatefulSet { 40 | labels := labelsForFiler(m.Name) 41 | annotations := m.Spec.Filer.Annotations 42 | ports := []corev1.ContainerPort{ 43 | { 44 | ContainerPort: seaweedv1.FilerHTTPPort, 45 | Name: "filer-http", 46 | }, 47 | { 48 | ContainerPort: seaweedv1.FilerGRPCPort, 49 | Name: "filer-grpc", 50 | }, 51 | } 52 | if m.Spec.Filer.S3 != nil && m.Spec.Filer.S3.Enabled { 53 | ports = append(ports, corev1.ContainerPort{ 54 | ContainerPort: seaweedv1.FilerS3Port, 55 | Name: "filer-s3", 56 | }) 57 | // Note: IAM is now embedded in S3 by default (same port as S3) 58 | // No separate IAM port needed when using embedded IAM 59 | } 60 | if m.Spec.Filer.MetricsPort != nil { 61 | ports = append(ports, corev1.ContainerPort{ 62 | ContainerPort: *m.Spec.Filer.MetricsPort, 63 | Name: "filer-metrics", 64 | }) 65 | } 66 | replicas := int32(m.Spec.Filer.Replicas) 67 | rollingUpdatePartition := int32(0) 68 | enableServiceLinks := false 69 | 70 | filerPodSpec := m.BaseFilerSpec().BuildPodSpec() 71 | filerPodSpec.Volumes = []corev1.Volume{ 72 | { 73 | Name: "filer-config", 74 | VolumeSource: corev1.VolumeSource{ 75 | ConfigMap: &corev1.ConfigMapVolumeSource{ 76 | LocalObjectReference: corev1.LocalObjectReference{ 77 | Name: m.Name + "-filer", 78 | }, 79 | }, 80 | }, 81 | }, 82 | } 83 | volumeMounts := []corev1.VolumeMount{ 84 | { 85 | Name: "filer-config", 86 | ReadOnly: true, 87 | MountPath: "/etc/seaweedfs", 88 | }, 89 | } 90 | 91 | if m.Spec.Filer.S3 != nil && m.Spec.Filer.S3.Enabled && m.Spec.Filer.S3.ConfigSecret != nil && m.Spec.Filer.S3.ConfigSecret.Name != "" { 92 | volumeMounts = append(volumeMounts, corev1.VolumeMount{ 93 | Name: "config-users", 94 | ReadOnly: true, 95 | MountPath: "/etc/sw", 96 | }) 97 | filerPodSpec.Volumes = append(filerPodSpec.Volumes, 98 | corev1.Volume{ 99 | Name: "config-users", 100 | VolumeSource: corev1.VolumeSource{ 101 | Secret: &corev1.SecretVolumeSource{ 102 | SecretName: m.Spec.Filer.S3.ConfigSecret.Name, 103 | }, 104 | }, 105 | }) 106 | } 107 | 108 | var persistentVolumeClaims []corev1.PersistentVolumeClaim 109 | if m.Spec.Filer.Persistence != nil && m.Spec.Filer.Persistence.Enabled { 110 | claimName := m.Name + "-filer" 111 | if m.Spec.Filer.Persistence.ExistingClaim != nil { 112 | claimName = *m.Spec.Filer.Persistence.ExistingClaim 113 | } 114 | if m.Spec.Filer.Persistence.ExistingClaim == nil { 115 | persistentVolumeClaims = append(persistentVolumeClaims, corev1.PersistentVolumeClaim{ 116 | ObjectMeta: metav1.ObjectMeta{ 117 | Name: claimName, 118 | }, 119 | Spec: corev1.PersistentVolumeClaimSpec{ 120 | AccessModes: m.Spec.Filer.Persistence.AccessModes, 121 | Resources: m.Spec.Filer.Persistence.Resources, 122 | StorageClassName: m.Spec.Filer.Persistence.StorageClassName, 123 | Selector: m.Spec.Filer.Persistence.Selector, 124 | VolumeName: m.Spec.Filer.Persistence.VolumeName, 125 | VolumeMode: m.Spec.Filer.Persistence.VolumeMode, 126 | DataSource: m.Spec.Filer.Persistence.DataSource, 127 | }, 128 | }) 129 | } 130 | filerPodSpec.Volumes = append(filerPodSpec.Volumes, corev1.Volume{ 131 | Name: claimName, 132 | VolumeSource: corev1.VolumeSource{ 133 | PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ 134 | ClaimName: claimName, 135 | ReadOnly: false, 136 | }, 137 | }, 138 | }) 139 | volumeMounts = append(volumeMounts, corev1.VolumeMount{ 140 | Name: claimName, 141 | ReadOnly: false, 142 | MountPath: *m.Spec.Filer.Persistence.MountPath, 143 | SubPath: *m.Spec.Filer.Persistence.SubPath, 144 | }) 145 | } 146 | filerPodSpec.EnableServiceLinks = &enableServiceLinks 147 | filerPodSpec.Containers = []corev1.Container{{ 148 | Name: "filer", 149 | Image: m.Spec.Image, 150 | ImagePullPolicy: m.BaseFilerSpec().ImagePullPolicy(), 151 | Env: append(m.BaseFilerSpec().Env(), kubernetesEnvVars...), 152 | Resources: filterContainerResources(m.Spec.Filer.ResourceRequirements), 153 | VolumeMounts: volumeMounts, 154 | Command: []string{ 155 | "/bin/sh", 156 | "-ec", 157 | buildFilerStartupScript(m), 158 | }, 159 | Ports: ports, 160 | ReadinessProbe: &corev1.Probe{ 161 | ProbeHandler: corev1.ProbeHandler{ 162 | HTTPGet: &corev1.HTTPGetAction{ 163 | Path: "/", 164 | Port: intstr.FromInt(seaweedv1.FilerHTTPPort), 165 | Scheme: corev1.URISchemeHTTP, 166 | }, 167 | }, 168 | InitialDelaySeconds: 10, 169 | TimeoutSeconds: 3, 170 | PeriodSeconds: 15, 171 | SuccessThreshold: 1, 172 | FailureThreshold: 100, 173 | }, 174 | LivenessProbe: &corev1.Probe{ 175 | ProbeHandler: corev1.ProbeHandler{ 176 | HTTPGet: &corev1.HTTPGetAction{ 177 | Path: "/", 178 | Port: intstr.FromInt(seaweedv1.FilerHTTPPort), 179 | Scheme: corev1.URISchemeHTTP, 180 | }, 181 | }, 182 | InitialDelaySeconds: 20, 183 | TimeoutSeconds: 3, 184 | PeriodSeconds: 30, 185 | SuccessThreshold: 1, 186 | FailureThreshold: 6, 187 | }, 188 | }} 189 | 190 | dep := &appsv1.StatefulSet{ 191 | ObjectMeta: metav1.ObjectMeta{ 192 | Name: m.Name + "-filer", 193 | Namespace: m.Namespace, 194 | }, 195 | Spec: appsv1.StatefulSetSpec{ 196 | ServiceName: m.Name + "-filer-peer", 197 | PodManagementPolicy: appsv1.ParallelPodManagement, 198 | Replicas: &replicas, 199 | UpdateStrategy: appsv1.StatefulSetUpdateStrategy{ 200 | Type: appsv1.RollingUpdateStatefulSetStrategyType, 201 | RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{ 202 | Partition: &rollingUpdatePartition, 203 | }, 204 | }, 205 | Selector: &metav1.LabelSelector{ 206 | MatchLabels: labels, 207 | }, 208 | Template: corev1.PodTemplateSpec{ 209 | ObjectMeta: metav1.ObjectMeta{ 210 | Labels: labels, 211 | Annotations: annotations, 212 | }, 213 | Spec: filerPodSpec, 214 | }, 215 | VolumeClaimTemplates: persistentVolumeClaims, 216 | }, 217 | } 218 | return dep 219 | } 220 | -------------------------------------------------------------------------------- /.github/workflows/integration-test.yml: -------------------------------------------------------------------------------- 1 | name: Integration Tests 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - labeled 8 | - synchronize 9 | paths: 10 | - '**/*.go' 11 | - 'go.mod' 12 | - 'go.sum' 13 | - 'Makefile' 14 | - '.github/workflows/integration-test.yml' 15 | - 'config/**' 16 | push: 17 | branches: 18 | - main 19 | - master 20 | paths: 21 | - '**/*.go' 22 | - 'go.mod' 23 | - 'go.sum' 24 | - 'Makefile' 25 | - '.github/workflows/integration-test.yml' 26 | - 'config/**' 27 | 28 | jobs: 29 | determine-versions: 30 | runs-on: ubuntu-22.04 31 | outputs: 32 | matrix: ${{ steps.versions.outputs.matrix }} 33 | steps: 34 | - name: Install jq 35 | run: sudo apt-get update && sudo apt-get install -y jq 36 | 37 | - name: Determine latest K8s versions 38 | id: versions 39 | run: | 40 | set -euo pipefail 41 | set -x 42 | 43 | # Try to fetch tags (tags endpoint tends to be smaller and contains semantic tags) 44 | PER_PAGE=200 45 | API_URL="https://api.github.com/repos/kubernetes/kubernetes/tags?per_page=${PER_PAGE}" 46 | VERSIONS_RAW=$(curl -sS --fail "$API_URL" || true) 47 | 48 | if [ -z "${VERSIONS_RAW:-}" ]; then 49 | echo "Warning: failed to fetch Kubernetes tags; using fallback versions" >&2 50 | MATRIX='[{"version":"v1.31.0","attribute":"latest"},{"version":"v1.30.0","attribute":"current"},{"version":"v1.29.0","attribute":"previous"}]' 51 | echo "matrix=$MATRIX" >> $GITHUB_OUTPUT 52 | exit 0 53 | fi 54 | 55 | # Extract candidate tag names. Prefer jq; fall back to Python parsing if jq missing. 56 | if command -v jq >/dev/null 2>&1; then 57 | # Only accept full semver tags like v1.X.Y (exclude alpha/beta/rc) 58 | CANDIDATES=$(printf '%s' "$VERSIONS_RAW" | jq -r '.[].name' | grep -E '^v1\.[0-9]+\.[0-9]+$' || true) 59 | else 60 | # python fallback also restricts to v1.X.Y 61 | CANDIDATES=$(printf '%s' "$VERSIONS_RAW" | python3 -c 'import sys,json,re;data=json.load(sys.stdin);out=[item.get("name") for item in data if isinstance(item,dict) and item.get("name") and re.match(r"^v1\.[0-9]+\.[0-9]+$", item.get("name"))]; print("\n".join(out))') 62 | fi 63 | 64 | # Compute latest patch per minor (e.g., pick v1.34.2 for minor 1.34), 65 | # then pick the latest 3 minor versions (1.34,1.33,1.32) 66 | NUM=3 67 | # LATEST_PER_MINOR: for each minor (1.X) keep the highest patch 68 | LATEST_PER_MINOR=$(printf '%s' "$CANDIDATES" | sed 's/^v//' | sort -V | awk -F. '{ minor=$1"."$2; versions[minor]=$0 } END { for (m in versions) print versions[m] }') 69 | # Now sort by semver desc (latest minor first) and pick top NUM 70 | VERSIONS=$(printf '%s' "$LATEST_PER_MINOR" | sort -V -r | head -n "$NUM" | sed 's/^/v/') 71 | 72 | # Build matrix as array of objects {version, attribute} 73 | MATRIX='[]' 74 | ATTRIBUTES=("latest" "current" "previous") 75 | 76 | # read into array preserving newlines (VERSIONS already latest-first) 77 | mapfile -t arr <<<"${VERSIONS:-}" 78 | for i in "${!arr[@]}"; do 79 | v=${arr[i]} 80 | a=${ATTRIBUTES[i]:-latest} 81 | MATRIX=$(echo "$MATRIX" | jq --arg v "$v" --arg a "$a" '. += [{"version": $v, "attribute": $a}]') 82 | done 83 | 84 | # If parsing produced empty matrix, use fallback defaults 85 | if [ "$(echo "$MATRIX" | jq length)" -eq 0 ]; then 86 | MATRIX='[{"version":"v1.31.0","attribute":"latest"},{"version":"v1.30.0","attribute":"current"},{"version":"v1.29.0","attribute":"previous"}]' 87 | fi 88 | 89 | # Compact JSON (no newlines) so it can be safely written to GITHUB_OUTPUT 90 | MATRIX=$(echo "$MATRIX" | jq -c '.') 91 | echo "matrix=$MATRIX" >> $GITHUB_OUTPUT 92 | 93 | integration-test: 94 | name: Integration Tests on k8s ${{ matrix.k8s.version }} 95 | needs: determine-versions 96 | # Pull request has label 'ok-to-test' or the author is a member of the organization, or it's a push to main/master 97 | if: | 98 | github.event_name == 'push' || 99 | contains(github.event.pull_request.labels.*.name, 'ok-to-test') || 100 | contains(fromJSON('["COLLABORATOR", "MEMBER", "OWNER"]'), github.event.pull_request.author_association) 101 | 102 | strategy: 103 | fail-fast: false 104 | matrix: 105 | k8s: ${{ fromJson(needs.determine-versions.outputs.matrix) }} 106 | 107 | runs-on: ubuntu-22.04 108 | timeout-minutes: 30 109 | 110 | steps: 111 | - name: Checkout code 112 | uses: actions/checkout@v4 113 | 114 | - name: Set up Go 115 | uses: actions/setup-go@v5 116 | with: 117 | go-version: '1.24' 118 | cache: true 119 | 120 | - name: Set up Docker Buildx 121 | uses: docker/setup-buildx-action@v3 122 | 123 | - name: Install kubectl 124 | uses: azure/setup-kubectl@v4 125 | with: 126 | version: 'v1.30.0' 127 | 128 | - name: Run unit tests 129 | run: make test 130 | 131 | - name: Build manager binary 132 | run: make build 133 | 134 | - name: Set up Kind cluster 135 | run: | 136 | # Use specific Kubernetes version 137 | K8S_VERSION=${{ matrix.k8s.version }} make kind-prepare 138 | env: 139 | KIND_CLUSTER_NAME: seaweedfs-operator-kind-${{ github.run_id }} 140 | 141 | - name: Build and load Docker image 142 | run: | 143 | make docker-build 144 | make kind-load 145 | env: 146 | KIND_CLUSTER_NAME: seaweedfs-operator-kind-${{ github.run_id }} 147 | IMG: ghcr.io/seaweedfs/seaweedfs-operator:test-${{ github.run_id }} 148 | 149 | - name: Deploy operator 150 | run: | 151 | make deploy 152 | # Wait for the operator to be ready 153 | kubectl wait deployment.apps/seaweedfs-operator-controller-manager \ 154 | --for condition=Available \ 155 | --namespace seaweedfs-operator-system \ 156 | --timeout 300s 157 | env: 158 | KIND_CLUSTER_NAME: seaweedfs-operator-kind-${{ github.run_id }} 159 | IMG: ghcr.io/seaweedfs/seaweedfs-operator:test-${{ github.run_id }} 160 | 161 | - name: Run integration tests 162 | run: | 163 | # Run all e2e tests including the new resource integration tests 164 | go test ./test/e2e/ -v -ginkgo.v -ginkgo.progress -timeout 20m 165 | env: 166 | KIND_CLUSTER_NAME: seaweedfs-operator-kind-${{ github.run_id }} 167 | 168 | - name: Collect operator logs on failure 169 | if: failure() 170 | run: | 171 | echo "=== Operator Manager Logs ===" 172 | kubectl logs -n seaweedfs-operator-system deployment/seaweedfs-operator-controller-manager --tail=100 || true 173 | 174 | echo "=== All Pods in operator namespace ===" 175 | kubectl get pods -n seaweedfs-operator-system || true 176 | 177 | echo "=== All Pods in test namespace ===" 178 | kubectl get pods -n test-resources || true 179 | 180 | echo "=== Events ===" 181 | kubectl get events --all-namespaces --sort-by='.lastTimestamp' --tail=50 || true 182 | 183 | echo "=== StatefulSets ===" 184 | kubectl get statefulsets --all-namespaces || true 185 | 186 | echo "=== Seaweed Resources ===" 187 | kubectl get seaweed --all-namespaces -o yaml || true 188 | 189 | - name: Cleanup 190 | if: always() 191 | run: | 192 | # Cleanup the Kind cluster 193 | make kind-delete || true 194 | env: 195 | KIND_CLUSTER_NAME: seaweedfs-operator-kind-${{ github.run_id }} 196 | 197 | resource-validation-test: 198 | name: Resource Validation Test 199 | runs-on: ubuntu-22.04 200 | timeout-minutes: 15 201 | 202 | steps: 203 | - name: Checkout code 204 | uses: actions/checkout@v4 205 | 206 | - name: Set up Go 207 | uses: actions/setup-go@v5 208 | with: 209 | go-version: '1.24' 210 | cache: true 211 | 212 | - name: Run resource filtering unit tests 213 | run: | 214 | go test ./internal/controller/ -run TestFilterContainerResources -v 215 | 216 | - name: Validate helper function behavior 217 | run: | 218 | echo "Testing that storage resources are properly filtered..." 219 | go test ./internal/controller/ -run TestFilterContainerResources -v 220 | 221 | echo "Verifying filter function exists and compiles..." 222 | go build -o /dev/null ./internal/controller/ 223 | 224 | build-check: 225 | name: Build Check 226 | runs-on: ubuntu-22.04 227 | 228 | steps: 229 | - name: Checkout code 230 | uses: actions/checkout@v4 231 | 232 | - name: Set up Go 233 | uses: actions/setup-go@v5 234 | with: 235 | go-version: '1.24' 236 | cache: true 237 | 238 | - name: Check if code compiles 239 | run: | 240 | go mod tidy 241 | make build 242 | 243 | - name: Check if Docker image builds 244 | run: | 245 | make docker-build 246 | env: 247 | IMG: test-image:latest 248 | -------------------------------------------------------------------------------- /deploy/helm/README.md: -------------------------------------------------------------------------------- 1 | # seaweedfs-operator 2 | 3 | ![Version: 0.1.2](https://img.shields.io/badge/Version-0.1.2-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.0.4](https://img.shields.io/badge/AppVersion-1.0.4-informational?style=flat-square) 4 | 5 | A Helm chart for the seaweedfs-operator 6 | 7 | ## Maintainers 8 | 9 | | Name | Email | Url | 10 | | ---- | ------ | --- | 11 | | chrislusf | | | 12 | 13 | ## Values 14 | 15 | | Key | Type | Default | Description | 16 | |---------------------------------|--------|---------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 17 | | commonAnnotations | object | `{}` | Annotations for all the deployed objects | 18 | | commonLabels | object | `{}` | Labels for all the deployed objects | 19 | | fullnameOverride | string | `""` | String to fully override common.names.fullname template | 20 | | global | object | `{"imageRegistry":"chrislusf"}` | Global Docker image parameters Please, note that this will override the image parameters, including dependencies, configured to use the global value Current available global Docker image parameters: imageRegistry | 21 | | grafanaDashboard.enabled | bool | `true` | Enable or disable Grafana Dashboard configmap | 22 | | image.pullPolicy | string | `"Always"` | Specify a imagePullPolicy # Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' # ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images | 23 | | image.registry | string | `"chrislusf"` | | 24 | | image.repository | string | `"seaweedfs-operator"` | | 25 | | image.tag | string | `""` | tag of image to use. Defaults to appVersion in Chart.yaml | 26 | | nameOverride | string | `""` | String to partially override common.names.fullname template (will maintain the release name) | 27 | | port.name | string | `"http"` | name of the container port to use for the Kubernete service and ingress | 28 | | port.number | int | `8080` | container port number to use for the Kubernete service and ingress | 29 | | rbac.serviceAccount.annotations | object | `{}` | Annotations to add to the service account | 30 | | rbac.serviceAccount.automount | bool | `true` | Automount service account token for the server service account | 31 | | rbac.serviceAccount.create | bool | `true` | Specifies whether a service account should be created | 32 | | rbac.serviceAccount.name | string | `""` | The name of the service account to use. If not set and create is true, a name is generated using the fullname template. If set to "default", no ServiceAccount will be created and the default one will be used | 33 | | replicaCount | int | `1` | Set number of pod replicas | 34 | | resources.limits.cpu | string | `"500m"` | seaweedfs-operator containers' cpu limit (maximum allowed CPU) | 35 | | resources.limits.memory | string | `"500Mi"` | seaweedfs-operator containers' memory limit (maximum allowed memory) | 36 | | resources.requests.cpu | string | `"100m"` | seaweedfs-operator containers' cpu request (how much is requested by default) | 37 | | resources.requests.memory | string | `"50Mi"` | seaweedfs-operator containers' memory request (how much is requested by default) | 38 | | service.port | int | `8080` | port to use for Kubernetes service | 39 | | service.portName | string | `"http"` | name of the port to use for Kubernetes service | 40 | | serviceMonitor.additionalLabels | object | `{}` | Used to pass Labels that are used by the Prometheus installed in your cluster to select Service Monitors to work with | 41 | | serviceMonitor.enabled | bool | `false` | Enable or disable ServiceMonitor for prometheus metrics | 42 | | serviceMonitor.honorLabels | bool | `true` | Specify honorLabels parameter to add the scrape endpoint | 43 | | serviceMonitor.interval | string | `"10s"` | Specify the interval at which metrics should be scraped | 44 | | serviceMonitor.scrapeTimeout | string | `"10s"` | Specify the timeout after which the scrape is ended | 45 | | webhook.enabled | bool | `true` | Enable or disable webhooks | 46 | | webhook.initContainer.image | string | `"curlimages/curl:8.8.0"` | Image used by the webhook readiness init container when patching certificates | 47 | 48 | ---------------------------------------------- 49 | Autogenerated from chart metadata using [helm-docs v1.11.3](https://github.com/norwoodj/helm-docs/releases/v1.11.3) 50 | --------------------------------------------------------------------------------