mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-14 10:35:51 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c159bf5052 | ||
|
|
6ac8dc16a0 | ||
|
|
62d8315b36 | ||
|
|
9ff96235c7 | ||
|
|
4a5243886a | ||
|
|
e1e4c9437a | ||
|
|
f950a941e3 | ||
|
|
ac579c1746 | ||
|
|
0a5c5ed4ce | ||
|
|
0a2dac1e56 | ||
|
|
737116e83c | ||
|
|
b20eae697e | ||
|
|
07f3f5eec5 | ||
|
|
47cad59c70 | ||
|
|
b17e2b411a | ||
|
|
4c88fbfd5e | ||
|
|
d4d2e511ed |
@@ -23,14 +23,13 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@a5f9b05d2d216f63e13859e0d847461041025775 # v2
|
||||
with:
|
||||
go-version: ^1.13
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v2
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
+15
-18
@@ -19,13 +19,12 @@ jobs:
|
||||
name: Go Vet
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@a5f9b05d2d216f63e13859e0d847461041025775 # v2
|
||||
with:
|
||||
go-version: ^1.13
|
||||
id: go
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v2
|
||||
uses: actions/checkout@v6
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
cd weed; go get -v -t -d ./...
|
||||
@@ -42,13 +41,12 @@ jobs:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@a5f9b05d2d216f63e13859e0d847461041025775 # v2
|
||||
with:
|
||||
go-version: ^1.13
|
||||
id: go
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v2
|
||||
uses: actions/checkout@v6
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- name: Build
|
||||
run: cd weed; go build -tags "elastic gocdk sqlite ydb tarantool tikv rclone" -v .
|
||||
|
||||
@@ -56,12 +54,11 @@ jobs:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go 1.x
|
||||
uses: actions/setup-go@a5f9b05d2d216f63e13859e0d847461041025775 # v2
|
||||
with:
|
||||
go-version: ^1.13
|
||||
id: go
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v2
|
||||
uses: actions/checkout@v6
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- name: Test
|
||||
run: cd weed; go test -tags "elastic gocdk sqlite ydb tarantool tikv rclone" -v ./...
|
||||
|
||||
@@ -30,7 +30,6 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
GO_VERSION: '1.24'
|
||||
TEST_TIMEOUT: '10m'
|
||||
|
||||
jobs:
|
||||
@@ -43,10 +42,10 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go ${{ env.GO_VERSION }}
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Build SeaweedFS
|
||||
run: |
|
||||
|
||||
@@ -24,7 +24,6 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
GO_VERSION: '1.24'
|
||||
TEST_TIMEOUT: '15m'
|
||||
|
||||
jobs:
|
||||
@@ -37,10 +36,10 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go ${{ env.GO_VERSION }}
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
|
||||
@@ -28,7 +28,6 @@ permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
GO_VERSION: '1.24'
|
||||
TEST_TIMEOUT: '30m'
|
||||
|
||||
jobs:
|
||||
@@ -46,10 +45,10 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go ${{ env.GO_VERSION }}
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Build SeaweedFS binary
|
||||
run: |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
apiVersion: v1
|
||||
description: SeaweedFS
|
||||
name: seaweedfs
|
||||
appVersion: "4.16"
|
||||
appVersion: "4.17"
|
||||
# Dev note: Trigger a helm chart release by `git tag -a helm-<version>`
|
||||
version: 4.16.0
|
||||
version: 4.17.0
|
||||
|
||||
@@ -64,9 +64,11 @@ spec:
|
||||
{{- if .Values.filer.podSecurityContext.enabled }}
|
||||
securityContext: {{- omit .Values.filer.podSecurityContext "enabled" | toYaml | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- include "seaweedfs.imagePullSecrets" $ | nindent 6 }}
|
||||
containers:
|
||||
- name: post-install-job
|
||||
image: {{ template "master.image" . }}
|
||||
imagePullPolicy: {{ $.Values.global.imagePullPolicy | default "IfNotPresent" }}
|
||||
env:
|
||||
- name: WEED_CLUSTER_DEFAULT
|
||||
value: "sw"
|
||||
@@ -187,6 +189,10 @@ spec:
|
||||
{{- end }}
|
||||
- containerPort: {{ .Values.master.grpcPort }}
|
||||
#name: swfs-master-grpc
|
||||
{{- with coalesce .Values.allInOne.s3.createBucketsHook.resources .Values.s3.createBucketsHook.resources .Values.filer.s3.createBucketsHook.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 10 }}
|
||||
{{- end }}
|
||||
{{- if .Values.filer.containerSecurityContext.enabled }}
|
||||
securityContext: {{- omit .Values.filer.containerSecurityContext "enabled" | toYaml | nindent 12 }}
|
||||
{{- end }}
|
||||
|
||||
@@ -909,6 +909,10 @@ filer:
|
||||
# versioning: Enabled
|
||||
# - name: bucket-b
|
||||
# anonymousRead: false
|
||||
# Resource requests/limits for the post-install bucket creation hook job.
|
||||
# Should map directly to the value of the resources field for a PodSpec.
|
||||
createBucketsHook:
|
||||
resources: {}
|
||||
|
||||
s3:
|
||||
enabled: false
|
||||
@@ -1076,6 +1080,11 @@ s3:
|
||||
failureThreshold: 100
|
||||
timeoutSeconds: 10
|
||||
|
||||
# Resource requests/limits for the post-install bucket creation hook job.
|
||||
# Should map directly to the value of the resources field for a PodSpec.
|
||||
createBucketsHook:
|
||||
resources: {}
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
className: ""
|
||||
@@ -1474,6 +1483,10 @@ allInOne:
|
||||
# versioning: Enabled
|
||||
# - name: bucket-b
|
||||
# anonymousRead: false
|
||||
# Resource requests/limits for the post-install bucket creation hook job.
|
||||
# Should map directly to the value of the resources field for a PodSpec.
|
||||
createBucketsHook:
|
||||
resources: {}
|
||||
|
||||
# SFTP server configuration
|
||||
# Note: Most parameters below default to null, which means they inherit from
|
||||
|
||||
@@ -32,6 +32,7 @@ const (
|
||||
defaultClusterContextTimeout = 10 * time.Second
|
||||
defaultWaitingBacklogFloor = 8
|
||||
defaultWaitingBacklogMultiplier = 4
|
||||
maxEstimatedRuntimeCap = 8 * time.Hour
|
||||
)
|
||||
|
||||
type schedulerPolicy struct {
|
||||
@@ -293,6 +294,26 @@ func (r *Plugin) runJobTypeIteration(jobType string, policy schedulerPolicy) boo
|
||||
|
||||
r.setSchedulerLoopState(jobType, "executing")
|
||||
|
||||
// Scan proposals for the maximum estimated_runtime_seconds so the
|
||||
// execution phase gets enough time for large jobs (e.g. vacuum on
|
||||
// big volumes). If any proposal needs more time than the remaining
|
||||
// JobTypeMaxRuntime, extend the execution context accordingly.
|
||||
var maxEstimatedRuntime time.Duration
|
||||
for _, p := range filtered {
|
||||
if p.Parameters != nil {
|
||||
if est, ok := p.Parameters["estimated_runtime_seconds"]; ok {
|
||||
if v := est.GetInt64Value(); v > 0 {
|
||||
if d := time.Duration(v) * time.Second; d > maxEstimatedRuntime {
|
||||
maxEstimatedRuntime = d
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if maxEstimatedRuntime > maxEstimatedRuntimeCap {
|
||||
maxEstimatedRuntime = maxEstimatedRuntimeCap
|
||||
}
|
||||
|
||||
remaining = time.Until(start.Add(maxRuntime))
|
||||
if remaining <= 0 {
|
||||
r.appendActivity(JobActivity{
|
||||
@@ -306,6 +327,17 @@ func (r *Plugin) runJobTypeIteration(jobType string, policy schedulerPolicy) boo
|
||||
return detected
|
||||
}
|
||||
|
||||
// If the longest estimated job exceeds the remaining JobTypeMaxRuntime,
|
||||
// create a new execution context with enough headroom instead of using
|
||||
// jobCtx which would cancel too early.
|
||||
execCtx := jobCtx
|
||||
execCancel := context.CancelFunc(func() {})
|
||||
if maxEstimatedRuntime > 0 && maxEstimatedRuntime > remaining {
|
||||
execCtx, execCancel = context.WithTimeout(context.Background(), maxEstimatedRuntime)
|
||||
remaining = maxEstimatedRuntime
|
||||
}
|
||||
defer execCancel()
|
||||
|
||||
execPolicy := policy
|
||||
if execPolicy.ExecutionTimeout <= 0 {
|
||||
execPolicy.ExecutionTimeout = defaultScheduledExecutionTimeout
|
||||
@@ -314,10 +346,10 @@ func (r *Plugin) runJobTypeIteration(jobType string, policy schedulerPolicy) boo
|
||||
execPolicy.ExecutionTimeout = remaining
|
||||
}
|
||||
|
||||
successCount, errorCount, canceledCount := r.dispatchScheduledProposals(jobCtx, jobType, filtered, clusterContext, execPolicy)
|
||||
successCount, errorCount, canceledCount := r.dispatchScheduledProposals(execCtx, jobType, filtered, clusterContext, execPolicy)
|
||||
|
||||
status := "success"
|
||||
if jobCtx.Err() != nil {
|
||||
if execCtx.Err() != nil {
|
||||
status = "timeout"
|
||||
} else if errorCount > 0 || canceledCount > 0 {
|
||||
status = "error"
|
||||
@@ -937,7 +969,24 @@ func (r *Plugin) executeScheduledJobWithExecutor(
|
||||
if parent == nil {
|
||||
parent = context.Background()
|
||||
}
|
||||
execCtx, cancel := context.WithTimeout(parent, policy.ExecutionTimeout)
|
||||
// Use the job's estimated runtime if provided and larger than the
|
||||
// default execution timeout. This lets handlers like vacuum scale
|
||||
// the timeout based on volume size so large volumes are not killed.
|
||||
timeout := policy.ExecutionTimeout
|
||||
if job.Parameters != nil {
|
||||
if est, ok := job.Parameters["estimated_runtime_seconds"]; ok {
|
||||
if v := est.GetInt64Value(); v > 0 {
|
||||
estimated := time.Duration(v) * time.Second
|
||||
if estimated > maxEstimatedRuntimeCap {
|
||||
estimated = maxEstimatedRuntimeCap
|
||||
}
|
||||
if estimated > timeout {
|
||||
timeout = estimated
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
execCtx, cancel := context.WithTimeout(parent, timeout)
|
||||
_, err := r.executeJobWithExecutor(execCtx, executor, job, clusterContext, int32(attempt))
|
||||
cancel()
|
||||
if err == nil {
|
||||
|
||||
@@ -239,7 +239,7 @@ templ Plugin(page string) {
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 mb-3">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0"><i class="fas fa-cogs me-2"></i>Job Scheduling Settings</h5>
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -269,50 +269,6 @@ templ Layout(view ViewContext, content templ.Component) {
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
if currentPath == "/plugin/detection" {
|
||||
<a class="nav-link active" href="/plugin/detection">
|
||||
<i class="fas fa-search me-2"></i>Job Detection
|
||||
</a>
|
||||
} else {
|
||||
<a class="nav-link" href="/plugin/detection">
|
||||
<i class="fas fa-search me-2"></i>Job Detection
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
if currentPath == "/plugin/queue" {
|
||||
<a class="nav-link active" href="/plugin/queue">
|
||||
<i class="fas fa-list me-2"></i>Job Queue
|
||||
</a>
|
||||
} else {
|
||||
<a class="nav-link" href="/plugin/queue">
|
||||
<i class="fas fa-list me-2"></i>Job Queue
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
if currentPath == "/plugin/execution" {
|
||||
<a class="nav-link active" href="/plugin/execution">
|
||||
<i class="fas fa-tasks me-2"></i>Job Execution
|
||||
</a>
|
||||
} else {
|
||||
<a class="nav-link" href="/plugin/execution">
|
||||
<i class="fas fa-tasks me-2"></i>Job Execution
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
if currentPath == "/plugin/configuration" {
|
||||
<a class="nav-link active" href="/plugin/configuration">
|
||||
<i class="fas fa-sliders-h me-2"></i>Configuration
|
||||
</a>
|
||||
} else {
|
||||
<a class="nav-link" href="/plugin/configuration">
|
||||
<i class="fas fa-sliders-h me-2"></i>Configuration
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -269,67 +269,7 @@ func Layout(view ViewContext, content templ.Component) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</li><li class=\"nav-item\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if currentPath == "/plugin/detection" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<a class=\"nav-link active\" href=\"/plugin/detection\"><i class=\"fas fa-search me-2\"></i>Job Detection</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<a class=\"nav-link\" href=\"/plugin/detection\"><i class=\"fas fa-search me-2\"></i>Job Detection</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</li><li class=\"nav-item\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if currentPath == "/plugin/queue" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<a class=\"nav-link active\" href=\"/plugin/queue\"><i class=\"fas fa-list me-2\"></i>Job Queue</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<a class=\"nav-link\" href=\"/plugin/queue\"><i class=\"fas fa-list me-2\"></i>Job Queue</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</li><li class=\"nav-item\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if currentPath == "/plugin/execution" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<a class=\"nav-link active\" href=\"/plugin/execution\"><i class=\"fas fa-tasks me-2\"></i>Job Execution</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<a class=\"nav-link\" href=\"/plugin/execution\"><i class=\"fas fa-tasks me-2\"></i>Job Execution</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</li><li class=\"nav-item\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if currentPath == "/plugin/configuration" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<a class=\"nav-link active\" href=\"/plugin/configuration\"><i class=\"fas fa-sliders-h me-2\"></i>Configuration</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<a class=\"nav-link\" href=\"/plugin/configuration\"><i class=\"fas fa-sliders-h me-2\"></i>Configuration</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</li></ul></div></div><!-- Sidebar backdrop for mobile --><div class=\"sidebar-backdrop\" id=\"sidebarBackdrop\"></div><!-- Main content --><main class=\"col-md-9 ms-sm-auto col-lg-10 px-3 px-md-4\"><div class=\"pt-3\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</li></ul></div></div><!-- Sidebar backdrop for mobile --><div class=\"sidebar-backdrop\" id=\"sidebarBackdrop\"></div><!-- Main content --><main class=\"col-md-9 ms-sm-auto col-lg-10 px-3 px-md-4\"><div class=\"pt-3\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -337,43 +277,43 @@ func Layout(view ViewContext, content templ.Component) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</div></main></div></div><!-- Footer --><footer class=\"footer mt-auto py-3 bg-light\"><div class=\"container-fluid text-center\"><small class=\"text-muted\">© ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</div></main></div></div><!-- Footer --><footer class=\"footer mt-auto py-3 bg-light\"><div class=\"container-fluid text-center\"><small class=\"text-muted\">© ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", time.Now().Year()))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 336, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 292, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, " SeaweedFS Admin v")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, " SeaweedFS Admin v")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(version.VERSION_NUMBER)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 336, Col: 102}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 292, Col: 102}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !strings.Contains(version.VERSION, "enterprise") {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<span class=\"mx-2\">•</span> <a href=\"https://seaweedfs.com\" target=\"_blank\" class=\"text-decoration-none\"><i class=\"fas fa-star me-1\"></i>Enterprise Version Available</a>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<span class=\"mx-2\">•</span> <a href=\"https://seaweedfs.com\" target=\"_blank\" class=\"text-decoration-none\"><i class=\"fas fa-star me-1\"></i>Enterprise Version Available</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</small></div></footer><!-- Bootstrap JS --><script src=\"/static/js/bootstrap.bundle.min.js\"></script><!-- Modal Alerts JS (replaces native alert/confirm) --><script src=\"/static/js/modal-alerts.js\"></script><!-- Custom JS --><script src=\"/static/js/admin.js\"></script><script src=\"/static/js/iam-utils.js\"></script><script src=\"/static/js/s3tables.js\"></script></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</small></div></footer><!-- Bootstrap JS --><script src=\"/static/js/bootstrap.bundle.min.js\"></script><!-- Modal Alerts JS (replaces native alert/confirm) --><script src=\"/static/js/modal-alerts.js\"></script><!-- Custom JS --><script src=\"/static/js/admin.js\"></script><script src=\"/static/js/iam-utils.js\"></script><script src=\"/static/js/s3tables.js\"></script></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -402,69 +342,69 @@ func LoginForm(title string, errorMessage string, csrfToken string) templ.Compon
|
||||
templ_7745c5c3_Var16 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 364, Col: 17}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 320, Col: 17}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, " - Login</title><link rel=\"icon\" href=\"/static/favicon.ico\" type=\"image/x-icon\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link href=\"/static/css/bootstrap.min.css\" rel=\"stylesheet\"><link href=\"/static/css/fontawesome.min.css\" rel=\"stylesheet\"></head><body class=\"bg-light\"><div class=\"container\"><div class=\"row justify-content-center min-vh-100 align-items-center\"><div class=\"col-md-6 col-lg-4\"><div class=\"card shadow\"><div class=\"card-body p-5\"><div class=\"text-center mb-4\"><i class=\"fas fa-server fa-3x text-primary mb-3\"></i><h4 class=\"card-title\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " - Login</title><link rel=\"icon\" href=\"/static/favicon.ico\" type=\"image/x-icon\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><link href=\"/static/css/bootstrap.min.css\" rel=\"stylesheet\"><link href=\"/static/css/fontawesome.min.css\" rel=\"stylesheet\"></head><body class=\"bg-light\"><div class=\"container\"><div class=\"row justify-content-center min-vh-100 align-items-center\"><div class=\"col-md-6 col-lg-4\"><div class=\"card shadow\"><div class=\"card-body p-5\"><div class=\"text-center mb-4\"><i class=\"fas fa-server fa-3x text-primary mb-3\"></i><h4 class=\"card-title\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 378, Col: 57}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 334, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</h4><p class=\"text-muted\">Please sign in to continue</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</h4><p class=\"text-muted\">Please sign in to continue</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if errorMessage != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<div class=\"alert alert-danger\" role=\"alert\"><i class=\"fas fa-exclamation-triangle me-2\"></i> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<div class=\"alert alert-danger\" role=\"alert\"><i class=\"fas fa-exclamation-triangle me-2\"></i> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 385, Col: 45}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 341, Col: 45}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<form method=\"POST\" action=\"/login\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<form method=\"POST\" action=\"/login\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(csrfToken)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 390, Col: 84}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 346, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "\"><div class=\"mb-3\"><label for=\"username\" class=\"form-label\">Username</label><div class=\"input-group\"><span class=\"input-group-text\"><i class=\"fas fa-user\"></i></span> <input type=\"text\" class=\"form-control\" id=\"username\" name=\"username\" required></div></div><div class=\"mb-4\"><label for=\"password\" class=\"form-label\">Password</label><div class=\"input-group\"><span class=\"input-group-text\"><i class=\"fas fa-lock\"></i></span> <input type=\"password\" class=\"form-control\" id=\"password\" name=\"password\" required></div></div><button type=\"submit\" class=\"btn btn-primary w-100\"><i class=\"fas fa-sign-in-alt me-2\"></i>Sign In</button></form></div></div></div></div></div><script src=\"/static/js/bootstrap.bundle.min.js\"></script></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\"><div class=\"mb-3\"><label for=\"username\" class=\"form-label\">Username</label><div class=\"input-group\"><span class=\"input-group-text\"><i class=\"fas fa-user\"></i></span> <input type=\"text\" class=\"form-control\" id=\"username\" name=\"username\" required></div></div><div class=\"mb-4\"><label for=\"password\" class=\"form-label\">Password</label><div class=\"input-group\"><span class=\"input-group-text\"><i class=\"fas fa-lock\"></i></span> <input type=\"password\" class=\"form-control\" id=\"password\" name=\"password\" required></div></div><button type=\"submit\" class=\"btn btn-primary w-100\"><i class=\"fas fa-sign-in-alt me-2\"></i>Sign In</button></form></div></div></div></div></div><script src=\"/static/js/bootstrap.bundle.min.js\"></script></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
+21
-7
@@ -114,7 +114,7 @@ Admin UI (http://localhost:23646) to manage users and policies.
|
||||
|
||||
var (
|
||||
miniIp = cmdMini.Flag.String("ip", util.DetectedHostAddress(), "ip or server name, also used as identifier")
|
||||
miniBindIp = cmdMini.Flag.String("ip.bind", "", "ip address to bind to. If empty, default to same as -ip option.")
|
||||
miniBindIp = cmdMini.Flag.String("ip.bind", "0.0.0.0", "ip address to bind to. If empty, default to same as -ip option.")
|
||||
miniTimeout = cmdMini.Flag.Int("idleTimeout", 30, "connection idle seconds")
|
||||
miniDataCenter = cmdMini.Flag.String("dataCenter", "", "current volume server's data center name")
|
||||
miniRack = cmdMini.Flag.String("rack", "", "current volume server's rack name")
|
||||
@@ -555,9 +555,23 @@ func ensureAllPortsAvailableOnIP(bindIp string) error {
|
||||
// If a gRPC port is 0, it will be set to httpPort + GrpcPortOffset
|
||||
// This must be called after HTTP ports are finalized and before services start
|
||||
func initializeGrpcPortsOnIP(bindIp string) {
|
||||
// Track gRPC ports allocated during this function to prevent collisions between services
|
||||
// when multiple services need fallback port allocation
|
||||
allocatedGrpcPorts := make(map[int]bool)
|
||||
// Track all ports allocated (both HTTP and gRPC) to prevent collisions.
|
||||
// We must reserve HTTP ports so that gRPC fallback allocation never picks
|
||||
// a port already assigned to an HTTP service (which hasn't bound yet).
|
||||
allocatedPorts := make(map[int]bool)
|
||||
|
||||
// Reserve all HTTP ports first
|
||||
allocatedPorts[*miniMasterOptions.port] = true
|
||||
allocatedPorts[*miniFilerOptions.port] = true
|
||||
allocatedPorts[*miniOptions.v.port] = true
|
||||
allocatedPorts[*miniWebDavOptions.port] = true
|
||||
allocatedPorts[*miniAdminOptions.port] = true
|
||||
if *miniEnableS3 {
|
||||
allocatedPorts[*miniS3Options.port] = true
|
||||
if miniS3Options.portIceberg != nil && *miniS3Options.portIceberg > 0 {
|
||||
allocatedPorts[*miniS3Options.portIceberg] = true
|
||||
}
|
||||
}
|
||||
|
||||
grpcConfigs := []struct {
|
||||
httpPort *int
|
||||
@@ -593,10 +607,10 @@ func initializeGrpcPortsOnIP(bindIp string) {
|
||||
|
||||
// Verify the gRPC port is available (whether calculated or explicitly set)
|
||||
// Check on both specific IP and all interfaces, and check against already allocated ports
|
||||
if !isPortOpenOnIP(bindIp, *config.grpcPort) || !isPortAvailable(*config.grpcPort) || allocatedGrpcPorts[*config.grpcPort] {
|
||||
if !isPortOpenOnIP(bindIp, *config.grpcPort) || !isPortAvailable(*config.grpcPort) || allocatedPorts[*config.grpcPort] {
|
||||
glog.Warningf("gRPC port %d for %s is not available, finding alternative...", *config.grpcPort, config.name)
|
||||
originalPort := *config.grpcPort
|
||||
newPort := findAvailablePortOnIP(bindIp, originalPort+1, 100, allocatedGrpcPorts)
|
||||
newPort := findAvailablePortOnIP(bindIp, originalPort+1, 100, allocatedPorts)
|
||||
if newPort == 0 {
|
||||
glog.Errorf("Could not find available gRPC port for %s starting from %d, will use %d and fail on binding", config.name, originalPort+1, originalPort)
|
||||
} else {
|
||||
@@ -604,7 +618,7 @@ func initializeGrpcPortsOnIP(bindIp string) {
|
||||
*config.grpcPort = newPort
|
||||
}
|
||||
}
|
||||
allocatedGrpcPorts[*config.grpcPort] = true
|
||||
allocatedPorts[*config.grpcPort] = true
|
||||
glog.V(1).Infof("%s gRPC port set to %d", config.name, *config.grpcPort)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,18 @@ func (m *IAMManager) ValidateTrustPolicyForPrincipal(ctx context.Context, roleAr
|
||||
return fmt.Errorf("role has no trust policy")
|
||||
}
|
||||
|
||||
// Create evaluation context
|
||||
// Create evaluation context with RequestContext populated so that
|
||||
// principal matching works for specific (non-wildcard) principals.
|
||||
// Without this, evaluatePrincipalValue cannot look up "aws:PrincipalArn"
|
||||
// and always returns false for non-wildcard trust policy principals.
|
||||
evalCtx := &policy.EvaluationContext{
|
||||
Principal: principalArn,
|
||||
Action: "sts:AssumeRole",
|
||||
Resource: roleArn,
|
||||
RequestContext: map[string]interface{}{
|
||||
"principal": principalArn,
|
||||
"aws:PrincipalArn": principalArn,
|
||||
},
|
||||
}
|
||||
|
||||
// Evaluate the trust policy
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/iam/policy"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestTrustPolicyAWSUserPrincipal tests that trust policies with specific AWS user
|
||||
// principals work correctly with ValidateTrustPolicyForPrincipal.
|
||||
// This is a regression test for the case where setting a specific user principal like:
|
||||
//
|
||||
// "Principal": {"AWS": "arn:aws:iam::000000000000:user/backend"}
|
||||
//
|
||||
// would fail with "trust policy denies access to principal" even though the
|
||||
// caller's principal ARN matched.
|
||||
func TestTrustPolicyAWSUserPrincipal(t *testing.T) {
|
||||
iamManager := setupIntegratedIAMSystem(t)
|
||||
ctx := context.Background()
|
||||
|
||||
const (
|
||||
accountID = "000000000000"
|
||||
backendUser = "backend"
|
||||
backendArn = "arn:aws:iam::" + accountID + ":user/" + backendUser
|
||||
otherUser = "other"
|
||||
otherArn = "arn:aws:iam::" + accountID + ":user/" + otherUser
|
||||
clientRoleN = "ClientRole"
|
||||
clientRoleA = "arn:aws:iam::role/" + clientRoleN
|
||||
)
|
||||
|
||||
// Create role with trust policy restricted to a specific AWS user principal
|
||||
// (the exact scenario from discussion #8588)
|
||||
err := iamManager.CreateRole(ctx, "", clientRoleN, &RoleDefinition{
|
||||
RoleName: clientRoleN,
|
||||
TrustPolicy: &policy.PolicyDocument{
|
||||
Version: "2012-10-17",
|
||||
Statement: []policy.Statement{
|
||||
{
|
||||
Effect: "Allow",
|
||||
Principal: map[string]interface{}{
|
||||
"AWS": backendArn,
|
||||
},
|
||||
Action: []string{"sts:AssumeRole"},
|
||||
},
|
||||
},
|
||||
},
|
||||
AttachedPolicies: []string{"S3ReadOnlyPolicy"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
principalArn string
|
||||
expectErr bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "matching user principal should be allowed",
|
||||
principalArn: backendArn,
|
||||
expectErr: false,
|
||||
description: "Backend user ARN matches the trust policy principal exactly",
|
||||
},
|
||||
{
|
||||
name: "non-matching user principal should be denied",
|
||||
principalArn: otherArn,
|
||||
expectErr: true,
|
||||
description: "Other user ARN does not match the trust policy principal",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := iamManager.ValidateTrustPolicyForPrincipal(ctx, clientRoleA, tt.principalArn)
|
||||
if tt.expectErr {
|
||||
assert.Error(t, err, tt.description)
|
||||
assert.Contains(t, err.Error(), "trust policy denies access")
|
||||
} else {
|
||||
assert.NoError(t, err, tt.description)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTrustPolicyAWSWildcardPrincipal tests that wildcard AWS principals
|
||||
// continue to work correctly (this already works, serves as a regression guard).
|
||||
func TestTrustPolicyAWSWildcardPrincipal(t *testing.T) {
|
||||
iamManager := setupIntegratedIAMSystem(t)
|
||||
ctx := context.Background()
|
||||
|
||||
const roleName = "WildcardAWSRole"
|
||||
const roleArn = "arn:aws:iam::role/" + roleName
|
||||
|
||||
err := iamManager.CreateRole(ctx, "", roleName, &RoleDefinition{
|
||||
RoleName: roleName,
|
||||
TrustPolicy: &policy.PolicyDocument{
|
||||
Version: "2012-10-17",
|
||||
Statement: []policy.Statement{
|
||||
{
|
||||
Effect: "Allow",
|
||||
Principal: map[string]interface{}{
|
||||
"AWS": "*",
|
||||
},
|
||||
Action: []string{"sts:AssumeRole"},
|
||||
},
|
||||
},
|
||||
},
|
||||
AttachedPolicies: []string{"S3ReadOnlyPolicy"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Any user should be able to assume a role with wildcard AWS principal
|
||||
err = iamManager.ValidateTrustPolicyForPrincipal(ctx, roleArn, "arn:aws:iam::000000000000:user/anyuser")
|
||||
assert.NoError(t, err, "Wildcard AWS principal should allow any user")
|
||||
}
|
||||
|
||||
// TestTrustPolicyAWSUserArrayPrincipal tests trust policies with an array of
|
||||
// AWS user principals.
|
||||
func TestTrustPolicyAWSUserArrayPrincipal(t *testing.T) {
|
||||
iamManager := setupIntegratedIAMSystem(t)
|
||||
ctx := context.Background()
|
||||
|
||||
const (
|
||||
accountID = "000000000000"
|
||||
roleName = "MultiUserRole"
|
||||
roleArn = "arn:aws:iam::role/" + roleName
|
||||
user1Arn = "arn:aws:iam::" + accountID + ":user/user1"
|
||||
user2Arn = "arn:aws:iam::" + accountID + ":user/user2"
|
||||
user3Arn = "arn:aws:iam::" + accountID + ":user/user3"
|
||||
)
|
||||
|
||||
// Create role with multiple allowed user principals
|
||||
err := iamManager.CreateRole(ctx, "", roleName, &RoleDefinition{
|
||||
RoleName: roleName,
|
||||
TrustPolicy: &policy.PolicyDocument{
|
||||
Version: "2012-10-17",
|
||||
Statement: []policy.Statement{
|
||||
{
|
||||
Effect: "Allow",
|
||||
Principal: map[string]interface{}{
|
||||
"AWS": []interface{}{user1Arn, user2Arn},
|
||||
},
|
||||
Action: []string{"sts:AssumeRole"},
|
||||
},
|
||||
},
|
||||
},
|
||||
AttachedPolicies: []string{"S3ReadOnlyPolicy"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
principalArn string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "first listed user is allowed",
|
||||
principalArn: user1Arn,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "second listed user is allowed",
|
||||
principalArn: user2Arn,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "unlisted user is denied",
|
||||
principalArn: user3Arn,
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := iamManager.ValidateTrustPolicyForPrincipal(ctx, roleArn, tt.principalArn)
|
||||
if tt.expectErr {
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "trust policy denies access")
|
||||
}
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestTrustPolicyPlainStringPrincipal tests trust policies with a plain string
|
||||
// principal (not wrapped in {"AWS": ...}).
|
||||
func TestTrustPolicyPlainStringPrincipal(t *testing.T) {
|
||||
iamManager := setupIntegratedIAMSystem(t)
|
||||
ctx := context.Background()
|
||||
|
||||
const (
|
||||
roleName = "PlainStringPrincipalRole"
|
||||
roleArn = "arn:aws:iam::role/" + roleName
|
||||
userArn = "arn:aws:iam::000000000000:user/backend"
|
||||
)
|
||||
|
||||
// Plain string wildcard - should work
|
||||
err := iamManager.CreateRole(ctx, "", roleName, &RoleDefinition{
|
||||
RoleName: roleName,
|
||||
TrustPolicy: &policy.PolicyDocument{
|
||||
Version: "2012-10-17",
|
||||
Statement: []policy.Statement{
|
||||
{
|
||||
Effect: "Allow",
|
||||
Principal: "*",
|
||||
Action: []string{"sts:AssumeRole"},
|
||||
},
|
||||
},
|
||||
},
|
||||
AttachedPolicies: []string{"S3ReadOnlyPolicy"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = iamManager.ValidateTrustPolicyForPrincipal(ctx, roleArn, userArn)
|
||||
assert.NoError(t, err, "Plain wildcard principal should allow any user")
|
||||
|
||||
// Plain string with specific ARN
|
||||
const roleName2 = "PlainStringSpecificRole"
|
||||
const roleArn2 = "arn:aws:iam::role/" + roleName2
|
||||
|
||||
err = iamManager.CreateRole(ctx, "", roleName2, &RoleDefinition{
|
||||
RoleName: roleName2,
|
||||
TrustPolicy: &policy.PolicyDocument{
|
||||
Version: "2012-10-17",
|
||||
Statement: []policy.Statement{
|
||||
{
|
||||
Effect: "Allow",
|
||||
Principal: userArn,
|
||||
Action: []string{"sts:AssumeRole"},
|
||||
},
|
||||
},
|
||||
},
|
||||
AttachedPolicies: []string{"S3ReadOnlyPolicy"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = iamManager.ValidateTrustPolicyForPrincipal(ctx, roleArn2, userArn)
|
||||
assert.NoError(t, err, "Matching plain string principal should allow user")
|
||||
|
||||
err = iamManager.ValidateTrustPolicyForPrincipal(ctx, roleArn2, "arn:aws:iam::000000000000:user/other")
|
||||
if assert.Error(t, err, "Non-matching plain string principal should deny user") {
|
||||
assert.Contains(t, err.Error(), "trust policy denies access")
|
||||
}
|
||||
}
|
||||
@@ -544,6 +544,10 @@ func buildVacuumProposal(result *workertypes.TaskDetectionResult) (*plugin_pb.Jo
|
||||
summary = summary + " on " + result.Server
|
||||
}
|
||||
|
||||
// Estimate runtime: 5 min/GB (compact + commit + overhead)
|
||||
volumeSizeGB := int64(result.TypedParams.VolumeSize/1024/1024/1024) + 1
|
||||
estimatedRuntimeSeconds := volumeSizeGB * 5 * 60
|
||||
|
||||
return &plugin_pb.JobProposal{
|
||||
ProposalId: proposalID,
|
||||
DedupeKey: dedupeKey,
|
||||
@@ -564,6 +568,9 @@ func buildVacuumProposal(result *workertypes.TaskDetectionResult) (*plugin_pb.Jo
|
||||
"collection": {
|
||||
Kind: &plugin_pb.ConfigValue_StringValue{StringValue: result.Collection},
|
||||
},
|
||||
"estimated_runtime_seconds": {
|
||||
Kind: &plugin_pb.ConfigValue_Int64Value{Int64Value: estimatedRuntimeSeconds},
|
||||
},
|
||||
},
|
||||
Labels: map[string]string{
|
||||
"task_type": "vacuum",
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
stats_collect "github.com/seaweedfs/seaweedfs/weed/stats"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/needle"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/super_block"
|
||||
|
||||
@@ -396,8 +397,9 @@ func (s3a *S3ApiServer) DeleteBucketHandler(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
// Clean up bucket-related caches and locks after successful deletion
|
||||
// Clean up bucket-related caches, locks, and metrics after successful deletion
|
||||
s3a.invalidateBucketConfigCache(bucket)
|
||||
stats_collect.DeleteBucketMetrics(bucket)
|
||||
|
||||
s3err.WriteEmptyResponse(w, r, http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -632,6 +632,10 @@ func (s3a *S3ApiServer) doListFilerEntries(client filer_pb.SeaweedFilerClient, d
|
||||
|
||||
// Set nextMarker only when we have quota to process this entry
|
||||
nextMarker = entry.Name
|
||||
// Track whether this entry is the exact directory targeted by a trailing-slash prefix
|
||||
// (e.g., prefix "foo" from original prefix "foo/"). After recursing into this directory,
|
||||
// we must stop processing siblings to avoid matching unrelated entries like "foo1000".
|
||||
matchedPrefixDir := cursor.prefixEndsOnDelimiter && entry.Name == prefix && entry.IsDirectory
|
||||
if cursor.prefixEndsOnDelimiter {
|
||||
if entry.Name == prefix && entry.IsDirectory {
|
||||
if delimiter != "/" {
|
||||
@@ -692,6 +696,9 @@ func (s3a *S3ApiServer) doListFilerEntries(client filer_pb.SeaweedFilerClient, d
|
||||
if cursor.isTruncated {
|
||||
return
|
||||
}
|
||||
if matchedPrefixDir {
|
||||
return
|
||||
}
|
||||
// println("doListFilerEntries2 nextMarker", nextMarker)
|
||||
} else {
|
||||
eachEntryFn(dir, entry)
|
||||
|
||||
@@ -814,3 +814,74 @@ func TestListObjectsV2_Regression_Sorting(t *testing.T) {
|
||||
// With fix, it sees both and processes "reports"
|
||||
assert.Contains(t, results, "file1", "Should return the nested file even if 'reports' directory is not the first match")
|
||||
}
|
||||
|
||||
func TestListObjectsV2_PrefixEndingWithSlash_DoesNotMatchSiblings(t *testing.T) {
|
||||
// Regression test: listing with prefix "1/" should only return objects under
|
||||
// directory "1", not objects under "1000" or any other sibling whose name
|
||||
// shares the same prefix string.
|
||||
|
||||
s3a := &S3ApiServer{}
|
||||
client := &testFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/buckets/bucket/path/to/list": {
|
||||
{Name: "1", IsDirectory: true, Attributes: &filer_pb.FuseAttributes{}},
|
||||
{Name: "1000", IsDirectory: true, Attributes: &filer_pb.FuseAttributes{}},
|
||||
{Name: "2500", IsDirectory: true, Attributes: &filer_pb.FuseAttributes{}},
|
||||
},
|
||||
"/buckets/bucket/path/to/list/1": {
|
||||
{Name: "fileA", IsDirectory: false, Attributes: &filer_pb.FuseAttributes{}},
|
||||
},
|
||||
"/buckets/bucket/path/to/list/1000": {
|
||||
{Name: "fileB", IsDirectory: false, Attributes: &filer_pb.FuseAttributes{}},
|
||||
{Name: "fileC", IsDirectory: false, Attributes: &filer_pb.FuseAttributes{}},
|
||||
},
|
||||
"/buckets/bucket/path/to/list/2500": {
|
||||
{Name: "fileD", IsDirectory: false, Attributes: &filer_pb.FuseAttributes{}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Simulate listing with prefix "path/to/list/1/" (no delimiter).
|
||||
// normalizePrefixMarker("path/to/list/1/", "") returns dir="path/to/list", prefix="1"
|
||||
cursor := &ListingCursor{maxKeys: 1000, prefixEndsOnDelimiter: true}
|
||||
var results []string
|
||||
|
||||
_, err := s3a.doListFilerEntries(client, "/buckets/bucket/path/to/list", "1", cursor, "", "", false, "bucket", func(dir string, entry *filer_pb.Entry) {
|
||||
if !entry.IsDirectory {
|
||||
results = append(results, entry.Name)
|
||||
}
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"fileA"}, results, "Should only return files under directory '1', not '1000' or '2500'")
|
||||
}
|
||||
|
||||
func TestListObjectsV2_PrefixEndingWithSlash_WithDelimiter(t *testing.T) {
|
||||
// Same scenario but with delimiter="/", verifying the fix works for both cases.
|
||||
|
||||
s3a := &S3ApiServer{}
|
||||
client := &testFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/buckets/bucket/path/to/list": {
|
||||
{Name: "1", IsDirectory: true, Attributes: &filer_pb.FuseAttributes{}},
|
||||
{Name: "1000", IsDirectory: true, Attributes: &filer_pb.FuseAttributes{}},
|
||||
},
|
||||
"/buckets/bucket/path/to/list/1": {
|
||||
{Name: "fileA", IsDirectory: false, Attributes: &filer_pb.FuseAttributes{}},
|
||||
},
|
||||
"/buckets/bucket/path/to/list/1000": {
|
||||
{Name: "fileB", IsDirectory: false, Attributes: &filer_pb.FuseAttributes{}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cursor := &ListingCursor{maxKeys: 1000, prefixEndsOnDelimiter: true}
|
||||
var results []string
|
||||
|
||||
_, err := s3a.doListFilerEntries(client, "/buckets/bucket/path/to/list", "1", cursor, "", "/", false, "bucket", func(dir string, entry *filer_pb.Entry) {
|
||||
results = append(results, entry.Name)
|
||||
})
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"fileA"}, results, "Should only return files under directory '1', not '1000'")
|
||||
}
|
||||
|
||||
+39
-14
@@ -573,6 +573,26 @@ func RecordBucketActiveTime(bucket string) {
|
||||
bucketLastActiveLock.Unlock()
|
||||
}
|
||||
|
||||
func DeleteBucketMetrics(bucket string) {
|
||||
bucketLastActiveLock.Lock()
|
||||
delete(bucketLastActiveTsNs, bucket)
|
||||
bucketLastActiveLock.Unlock()
|
||||
|
||||
labels := prometheus.Labels{"bucket": bucket}
|
||||
c := S3RequestCounter.DeletePartialMatch(labels)
|
||||
c += S3RequestHistogram.DeletePartialMatch(labels)
|
||||
c += S3TimeToFirstByteHistogram.DeletePartialMatch(labels)
|
||||
c += S3BucketTrafficReceivedBytesCounter.DeletePartialMatch(labels)
|
||||
c += S3BucketTrafficSentBytesCounter.DeletePartialMatch(labels)
|
||||
c += S3DeletedObjectsCounter.DeletePartialMatch(labels)
|
||||
c += S3UploadedObjectsCounter.DeletePartialMatch(labels)
|
||||
c += S3BucketSizeBytesGauge.DeletePartialMatch(labels)
|
||||
c += S3BucketPhysicalSizeBytesGauge.DeletePartialMatch(labels)
|
||||
c += S3BucketObjectCountGauge.DeletePartialMatch(labels)
|
||||
|
||||
glog.V(0).Infof("delete bucket metrics, %s: %d", bucket, c)
|
||||
}
|
||||
|
||||
func DeleteCollectionMetrics(collection string) {
|
||||
labels := prometheus.Labels{"collection": collection}
|
||||
c := MasterReplicaPlacementMismatch.DeletePartialMatch(labels)
|
||||
@@ -590,27 +610,32 @@ func bucketMetricTTLControl() {
|
||||
for {
|
||||
now := time.Now().UnixNano()
|
||||
|
||||
// Collect expired buckets under the lock, then release before
|
||||
// doing the expensive Prometheus DeletePartialMatch calls.
|
||||
// This prevents blocking RecordBucketActiveTime during cleanup.
|
||||
bucketLastActiveLock.Lock()
|
||||
var expiredBuckets []string
|
||||
for bucket, ts := range bucketLastActiveTsNs {
|
||||
if (now - ts) > ttlNs {
|
||||
expiredBuckets = append(expiredBuckets, bucket)
|
||||
delete(bucketLastActiveTsNs, bucket)
|
||||
|
||||
labels := prometheus.Labels{"bucket": bucket}
|
||||
c := S3RequestCounter.DeletePartialMatch(labels)
|
||||
c += S3RequestHistogram.DeletePartialMatch(labels)
|
||||
c += S3TimeToFirstByteHistogram.DeletePartialMatch(labels)
|
||||
c += S3BucketTrafficReceivedBytesCounter.DeletePartialMatch(labels)
|
||||
c += S3BucketTrafficSentBytesCounter.DeletePartialMatch(labels)
|
||||
c += S3DeletedObjectsCounter.DeletePartialMatch(labels)
|
||||
c += S3UploadedObjectsCounter.DeletePartialMatch(labels)
|
||||
c += S3BucketSizeBytesGauge.DeletePartialMatch(labels)
|
||||
c += S3BucketPhysicalSizeBytesGauge.DeletePartialMatch(labels)
|
||||
c += S3BucketObjectCountGauge.DeletePartialMatch(labels)
|
||||
glog.V(0).Infof("delete inactive bucket metrics, %s: %d", bucket, c)
|
||||
}
|
||||
}
|
||||
|
||||
bucketLastActiveLock.Unlock()
|
||||
|
||||
for _, bucket := range expiredBuckets {
|
||||
labels := prometheus.Labels{"bucket": bucket}
|
||||
// Only delete gauges and histograms, which represent current state.
|
||||
// Counters (traffic, requests, objects) must persist for the process
|
||||
// lifetime so that Prometheus rate()/increase() queries work correctly.
|
||||
c := S3RequestHistogram.DeletePartialMatch(labels)
|
||||
c += S3TimeToFirstByteHistogram.DeletePartialMatch(labels)
|
||||
c += S3BucketSizeBytesGauge.DeletePartialMatch(labels)
|
||||
c += S3BucketPhysicalSizeBytesGauge.DeletePartialMatch(labels)
|
||||
c += S3BucketObjectCountGauge.DeletePartialMatch(labels)
|
||||
glog.V(0).Infof("delete inactive bucket metrics, %s: %d", bucket, c)
|
||||
}
|
||||
|
||||
time.Sleep(bucketAtiveTTL)
|
||||
}
|
||||
|
||||
|
||||
@@ -300,9 +300,13 @@ func (l *DiskLocation) DeleteCollectionFromDiskLocation(collection string) (e er
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
for _, v := range delVolsMap {
|
||||
for k, v := range delVolsMap {
|
||||
if err := v.Destroy(false); err != nil {
|
||||
errChain <- err
|
||||
} else {
|
||||
l.volumesLock.Lock()
|
||||
delete(l.volumes, k)
|
||||
l.volumesLock.Unlock()
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
@@ -383,14 +387,10 @@ func (l *DiskLocation) UnloadVolume(vid needle.VolumeId) error {
|
||||
func (l *DiskLocation) unmountVolumeByCollection(collectionName string) map[needle.VolumeId]*Volume {
|
||||
deltaVols := make(map[needle.VolumeId]*Volume, 0)
|
||||
for k, v := range l.volumes {
|
||||
if v.Collection == collectionName && !v.isCompacting && !v.isCommitCompacting {
|
||||
if v.Collection == collectionName && !v.isCompactionInProgress.Load() {
|
||||
deltaVols[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
for k := range deltaVols {
|
||||
delete(l.volumes, k)
|
||||
}
|
||||
return deltaVols
|
||||
}
|
||||
|
||||
|
||||
+12
-7
@@ -5,6 +5,7 @@ import (
|
||||
"path"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
|
||||
@@ -45,8 +46,7 @@ type Volume struct {
|
||||
lastCompactRevision uint16
|
||||
ldbTimeout int64
|
||||
|
||||
isCompacting bool
|
||||
isCommitCompacting bool
|
||||
isCompactionInProgress atomic.Bool
|
||||
|
||||
volumeInfoRWLock sync.RWMutex
|
||||
volumeInfo *volume_server_pb.VolumeInfo
|
||||
@@ -224,6 +224,16 @@ func (v *Volume) SyncToDisk() {
|
||||
|
||||
// Close cleanly shuts down this volume
|
||||
func (v *Volume) Close() {
|
||||
// Wait for any in-progress compaction to finish and claim the flag so no
|
||||
// new compaction can start. This must happen BEFORE acquiring
|
||||
// dataFileAccessLock to avoid deadlocking with CommitCompact which holds
|
||||
// the flag while waiting for the lock.
|
||||
for !v.isCompactionInProgress.CompareAndSwap(false, true) {
|
||||
time.Sleep(521 * time.Millisecond)
|
||||
glog.Warningf("Volume Close wait for compaction %d", v.Id)
|
||||
}
|
||||
defer v.isCompactionInProgress.Store(false)
|
||||
|
||||
v.dataFileAccessLock.Lock()
|
||||
defer v.dataFileAccessLock.Unlock()
|
||||
|
||||
@@ -231,11 +241,6 @@ func (v *Volume) Close() {
|
||||
}
|
||||
|
||||
func (v *Volume) doClose() {
|
||||
for v.isCommitCompacting {
|
||||
time.Sleep(521 * time.Millisecond)
|
||||
glog.Warningf("Volume Close wait for compaction %d", v.Id)
|
||||
}
|
||||
|
||||
if v.nm != nil {
|
||||
if err := v.nm.Sync(); err != nil {
|
||||
glog.Warningf("Volume Close fail to sync volume idx %d", v.Id)
|
||||
|
||||
@@ -64,22 +64,26 @@ func (v *Volume) CompactByVolumeData(opts *CompactOptions) error {
|
||||
//v.accessLock.Lock()
|
||||
//defer v.accessLock.Unlock()
|
||||
//glog.V(3).Infof("Got Compaction lock...")
|
||||
if v.isCompacting || v.isCommitCompacting {
|
||||
if !v.isCompactionInProgress.CompareAndSwap(false, true) {
|
||||
glog.V(0).Infof("Volume %d is already compacting...", v.Id)
|
||||
return nil
|
||||
}
|
||||
v.isCompacting = true
|
||||
defer func() {
|
||||
v.isCompacting = false
|
||||
}()
|
||||
defer v.isCompactionInProgress.Store(false)
|
||||
|
||||
v.lastCompactIndexOffset = v.IndexFileSize()
|
||||
v.lastCompactRevision = v.SuperBlock.CompactionRevision
|
||||
glog.V(3).Infof("creating copies for volume %d ,last offset %d...", v.Id, v.lastCompactIndexOffset)
|
||||
if v.DataBackend == nil {
|
||||
return fmt.Errorf("volume %d backend is empty remote:%v", v.Id, v.HasRemoteFile())
|
||||
}
|
||||
nm := v.nm
|
||||
if nm == nil {
|
||||
return fmt.Errorf("volume %d needle map is nil", v.Id)
|
||||
}
|
||||
if err := v.DataBackend.Sync(); err != nil {
|
||||
glog.V(0).Infof("compact failed to sync volume %d", v.Id)
|
||||
}
|
||||
if err := v.nm.Sync(); err != nil {
|
||||
if err := nm.Sync(); err != nil {
|
||||
glog.V(0).Infof("compact failed to sync volume idx %d", v.Id)
|
||||
}
|
||||
|
||||
@@ -102,14 +106,11 @@ func (v *Volume) CompactByIndex(opts *CompactOptions) error {
|
||||
}
|
||||
glog.V(3).Infof("Compact2 volume %d ...", v.Id)
|
||||
|
||||
if v.isCompacting || v.isCommitCompacting {
|
||||
if !v.isCompactionInProgress.CompareAndSwap(false, true) {
|
||||
glog.V(0).Infof("Volume %d is already compacting2 ...", v.Id)
|
||||
return nil
|
||||
}
|
||||
v.isCompacting = true
|
||||
defer func() {
|
||||
v.isCompacting = false
|
||||
}()
|
||||
defer v.isCompactionInProgress.Store(false)
|
||||
|
||||
v.lastCompactIndexOffset = v.IndexFileSize()
|
||||
v.lastCompactRevision = v.SuperBlock.CompactionRevision
|
||||
@@ -117,10 +118,14 @@ func (v *Volume) CompactByIndex(opts *CompactOptions) error {
|
||||
if v.DataBackend == nil {
|
||||
return fmt.Errorf("volume %d backend is empty remote:%v", v.Id, v.HasRemoteFile())
|
||||
}
|
||||
nm := v.nm
|
||||
if nm == nil {
|
||||
return fmt.Errorf("volume %d needle map is nil", v.Id)
|
||||
}
|
||||
if err := v.DataBackend.Sync(); err != nil {
|
||||
glog.V(0).Infof("compact2 failed to sync volume dat %d: %v", v.Id, err)
|
||||
}
|
||||
if err := v.nm.Sync(); err != nil {
|
||||
if err := nm.Sync(); err != nil {
|
||||
glog.V(0).Infof("compact2 failed to sync volume idx %d: %v", v.Id, err)
|
||||
}
|
||||
|
||||
@@ -139,14 +144,11 @@ func (v *Volume) CommitCompact() error {
|
||||
}
|
||||
glog.V(0).Infof("Committing volume %d vacuuming...", v.Id)
|
||||
|
||||
if v.isCommitCompacting {
|
||||
glog.V(0).Infof("Volume %d is already commit compacting ...", v.Id)
|
||||
if !v.isCompactionInProgress.CompareAndSwap(false, true) {
|
||||
glog.V(0).Infof("Volume %d is already compacting ...", v.Id)
|
||||
return nil
|
||||
}
|
||||
v.isCommitCompacting = true
|
||||
defer func() {
|
||||
v.isCommitCompacting = false
|
||||
}()
|
||||
defer v.isCompactionInProgress.Store(false)
|
||||
|
||||
v.dataFileAccessLock.Lock()
|
||||
defer v.dataFileAccessLock.Unlock()
|
||||
@@ -530,7 +532,7 @@ func (v *Volume) copyDataBasedOnIndexFile(opts *CompactOptions) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if v.Ttl.String() == "" {
|
||||
if v.Ttl.String() == "" && v.nm != nil {
|
||||
dstDatSize, _, err := dstDatBackend.GetStat()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -70,7 +70,7 @@ func (v *Volume) Destroy(onlyEmpty bool) (err error) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if v.isCompacting || v.isCommitCompacting {
|
||||
if !v.isCompactionInProgress.CompareAndSwap(false, true) {
|
||||
err = fmt.Errorf("volume %d is compacting", v.Id)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
var (
|
||||
MAJOR_VERSION = int32(4)
|
||||
MINOR_VERSION = int32(16)
|
||||
MINOR_VERSION = int32(17)
|
||||
VERSION_NUMBER = fmt.Sprintf("%d.%02d", MAJOR_VERSION, MINOR_VERSION)
|
||||
VERSION = util.SizeLimit + " " + VERSION_NUMBER
|
||||
COMMIT = ""
|
||||
|
||||
@@ -25,6 +25,7 @@ type VacuumTask struct {
|
||||
garbageThreshold float64
|
||||
progress float64
|
||||
grpcDialOption grpc.DialOption
|
||||
volumeSize uint64
|
||||
}
|
||||
|
||||
// NewVacuumTask creates a new unified vacuum task instance
|
||||
@@ -51,6 +52,7 @@ func (t *VacuumTask) Execute(ctx context.Context, params *worker_pb.TaskParams)
|
||||
}
|
||||
|
||||
t.garbageThreshold = vacuumParams.GarbageThreshold
|
||||
t.volumeSize = params.VolumeSize
|
||||
|
||||
t.GetLogger().WithFields(map[string]interface{}{
|
||||
"volume_id": t.volumeID,
|
||||
@@ -62,7 +64,7 @@ func (t *VacuumTask) Execute(ctx context.Context, params *worker_pb.TaskParams)
|
||||
// Step 1: Check volume status and garbage ratio
|
||||
t.ReportProgress(10.0)
|
||||
t.GetLogger().Info("Checking volume status")
|
||||
eligible, currentGarbageRatio, err := t.checkVacuumEligibility()
|
||||
eligible, currentGarbageRatio, err := t.checkVacuumEligibility(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check vacuum eligibility: %v", err)
|
||||
}
|
||||
@@ -83,14 +85,14 @@ func (t *VacuumTask) Execute(ctx context.Context, params *worker_pb.TaskParams)
|
||||
"threshold": t.garbageThreshold,
|
||||
}).Info("Performing vacuum operation")
|
||||
|
||||
if err := t.performVacuum(); err != nil {
|
||||
if err := t.performVacuum(ctx); err != nil {
|
||||
return fmt.Errorf("failed to perform vacuum: %v", err)
|
||||
}
|
||||
|
||||
// Step 3: Verify vacuum results
|
||||
t.ReportProgress(90.0)
|
||||
t.GetLogger().Info("Verifying vacuum results")
|
||||
if err := t.verifyVacuumResults(); err != nil {
|
||||
if err := t.verifyVacuumResults(ctx); err != nil {
|
||||
glog.Warningf("Vacuum verification failed: %v", err)
|
||||
// Don't fail the task - vacuum operation itself succeeded
|
||||
}
|
||||
@@ -146,15 +148,28 @@ func (t *VacuumTask) GetProgress() float64 {
|
||||
return t.progress
|
||||
}
|
||||
|
||||
// vacuumTimeout returns a dynamic timeout scaled by volume size, matching the
|
||||
// topology vacuum approach. base is the per-GB multiplier (e.g. 1 minute for
|
||||
// check, 3 minutes for compact).
|
||||
func (t *VacuumTask) vacuumTimeout(base time.Duration) time.Duration {
|
||||
if t.volumeSize == 0 {
|
||||
glog.V(1).Infof("volume %d has no size metric, using minimum timeout", t.volumeID)
|
||||
}
|
||||
sizeGB := int64(t.volumeSize/1024/1024/1024) + 1
|
||||
return base * time.Duration(sizeGB)
|
||||
}
|
||||
|
||||
// Helper methods for real vacuum operations
|
||||
|
||||
// checkVacuumEligibility checks if the volume meets vacuum criteria
|
||||
func (t *VacuumTask) checkVacuumEligibility() (bool, float64, error) {
|
||||
func (t *VacuumTask) checkVacuumEligibility(ctx context.Context) (bool, float64, error) {
|
||||
var garbageRatio float64
|
||||
|
||||
err := operation.WithVolumeServerClient(false, pb.ServerAddress(t.server), t.grpcDialOption,
|
||||
func(client volume_server_pb.VolumeServerClient) error {
|
||||
resp, err := client.VacuumVolumeCheck(context.Background(), &volume_server_pb.VacuumVolumeCheckRequest{
|
||||
checkCtx, cancel := context.WithTimeout(ctx, t.vacuumTimeout(time.Minute))
|
||||
defer cancel()
|
||||
resp, err := client.VacuumVolumeCheck(checkCtx, &volume_server_pb.VacuumVolumeCheckRequest{
|
||||
VolumeId: t.volumeID,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -178,12 +193,14 @@ func (t *VacuumTask) checkVacuumEligibility() (bool, float64, error) {
|
||||
}
|
||||
|
||||
// performVacuum executes the actual vacuum operation
|
||||
func (t *VacuumTask) performVacuum() error {
|
||||
func (t *VacuumTask) performVacuum(ctx context.Context) error {
|
||||
return operation.WithVolumeServerClient(false, pb.ServerAddress(t.server), t.grpcDialOption,
|
||||
func(client volume_server_pb.VolumeServerClient) error {
|
||||
// Step 1: Compact the volume
|
||||
// Step 1: Compact the volume (3 min per GB, matching topology vacuum)
|
||||
t.GetLogger().Info("Compacting volume")
|
||||
stream, err := client.VacuumVolumeCompact(context.Background(), &volume_server_pb.VacuumVolumeCompactRequest{
|
||||
compactCtx, compactCancel := context.WithTimeout(ctx, t.vacuumTimeout(3*time.Minute))
|
||||
defer compactCancel()
|
||||
stream, err := client.VacuumVolumeCompact(compactCtx, &volume_server_pb.VacuumVolumeCompactRequest{
|
||||
VolumeId: t.volumeID,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -202,18 +219,22 @@ func (t *VacuumTask) performVacuum() error {
|
||||
glog.V(2).Infof("Volume %d compact progress: %d bytes processed", t.volumeID, resp.ProcessedBytes)
|
||||
}
|
||||
|
||||
// Step 2: Commit the vacuum
|
||||
// Step 2: Commit the vacuum (1 min per GB)
|
||||
t.GetLogger().Info("Committing vacuum operation")
|
||||
_, err = client.VacuumVolumeCommit(context.Background(), &volume_server_pb.VacuumVolumeCommitRequest{
|
||||
commitCtx, commitCancel := context.WithTimeout(ctx, t.vacuumTimeout(time.Minute))
|
||||
defer commitCancel()
|
||||
_, err = client.VacuumVolumeCommit(commitCtx, &volume_server_pb.VacuumVolumeCommitRequest{
|
||||
VolumeId: t.volumeID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("vacuum commit failed: %v", err)
|
||||
}
|
||||
|
||||
// Step 3: Cleanup old files
|
||||
// Step 3: Cleanup old files (1 min per GB)
|
||||
t.GetLogger().Info("Cleaning up vacuum files")
|
||||
_, err = client.VacuumVolumeCleanup(context.Background(), &volume_server_pb.VacuumVolumeCleanupRequest{
|
||||
cleanupCtx, cleanupCancel := context.WithTimeout(ctx, t.vacuumTimeout(time.Minute))
|
||||
defer cleanupCancel()
|
||||
_, err = client.VacuumVolumeCleanup(cleanupCtx, &volume_server_pb.VacuumVolumeCleanupRequest{
|
||||
VolumeId: t.volumeID,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -226,10 +247,12 @@ func (t *VacuumTask) performVacuum() error {
|
||||
}
|
||||
|
||||
// verifyVacuumResults checks the volume status after vacuum
|
||||
func (t *VacuumTask) verifyVacuumResults() error {
|
||||
func (t *VacuumTask) verifyVacuumResults(ctx context.Context) error {
|
||||
return operation.WithVolumeServerClient(false, pb.ServerAddress(t.server), t.grpcDialOption,
|
||||
func(client volume_server_pb.VolumeServerClient) error {
|
||||
resp, err := client.VacuumVolumeCheck(context.Background(), &volume_server_pb.VacuumVolumeCheckRequest{
|
||||
verifyCtx, cancel := context.WithTimeout(ctx, t.vacuumTimeout(time.Minute))
|
||||
defer cancel()
|
||||
resp, err := client.VacuumVolumeCheck(verifyCtx, &volume_server_pb.VacuumVolumeCheckRequest{
|
||||
VolumeId: t.volumeID,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user