├── 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 |   
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 |
--------------------------------------------------------------------------------