mirror of
https://github.com/pgsty/minio.git
synced 2026-10-01 23:35:59 +03:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce23e9d930 | |||
| aa30b7db08 | |||
| f5ae4246f4 | |||
| bf5e68e472 | |||
| d975565cc0 | |||
| 8eaecfa90b | |||
| d52a2bd2e2 | |||
| 7e04af0a9a | |||
| ed533683cc | |||
| c72a0e2794 | |||
| cb74a6f041 | |||
| 5c8156c6df | |||
| dd0f8cbf39 | |||
| 775a9b2ebd | |||
| 59ba6d1d47 | |||
| 0fbedf2310 | |||
| f53f56996e | |||
| 669305a627 |
@@ -1,17 +0,0 @@
|
||||
.git
|
||||
.github
|
||||
default.etcd
|
||||
*.gz
|
||||
*.tar.gz
|
||||
*.bzip2
|
||||
*.zip
|
||||
browser/node_modules
|
||||
node_modules
|
||||
docs/debugging/s3-verify/s3-verify
|
||||
docs/debugging/xl-meta/xl-meta
|
||||
docs/debugging/s3-check-md5/s3-check-md5
|
||||
docs/debugging/hash-set/hash-set
|
||||
docs/debugging/healing-bin/healing-bin
|
||||
docs/debugging/inspect/inspect
|
||||
docs/debugging/pprofgoparser/pprofgoparser
|
||||
docs/debugging/reorder-disks/reorder-disks
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: community, triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Report bugs in the PGSTY SILO server (`pgsty/silo`) here. Community maintainers
|
||||
handle reports on a best-effort basis. There is no SLA, SLO, or emergency
|
||||
production-support channel. Follow the
|
||||
[Code of Conduct](https://github.com/pgsty/silo/blob/main/code_of_conduct.md).
|
||||
Report suspected vulnerabilities privately through
|
||||
[SECURITY.md](https://github.com/pgsty/silo/blob/main/SECURITY.md).
|
||||
For patches, see the [contribution guide](https://github.com/pgsty/silo/blob/main/CONTRIBUTING.md).
|
||||
|
||||
<!--- Provide a general summary of the issue in the Title above -->
|
||||
|
||||
## Expected Behavior
|
||||
<!--- If you're describing a bug, tell us what should happen -->
|
||||
<!--- If you're suggesting a change/improvement, tell us how it should work -->
|
||||
|
||||
## Current Behavior
|
||||
<!--- If describing a bug, tell us what happens instead of the expected behavior -->
|
||||
<!--- If suggesting a change/improvement, explain the difference from current behavior -->
|
||||
|
||||
## Possible Solution
|
||||
<!--- Not obligatory, but suggest a fix/reason for the bug, -->
|
||||
<!--- or ideas how to implement the addition or change -->
|
||||
|
||||
## Steps to Reproduce (for bugs)
|
||||
<!--- Provide a link to a live example, or an unambiguous set of steps to -->
|
||||
<!--- reproduce this bug. Include code to reproduce, if relevant -->
|
||||
<!--- and include the relevant Silo logs with secrets and credentials removed -->
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
## Context
|
||||
<!--- How has this issue affected you? What are you trying to accomplish? -->
|
||||
<!--- Providing context helps us come up with a solution that is most useful in the real world -->
|
||||
|
||||
## Regression
|
||||
<!-- Is this issue a regression? (Yes / No) -->
|
||||
<!-- If Yes, optionally include the Silo version, commit id, or PR that caused the regression. -->
|
||||
|
||||
## Your Environment
|
||||
<!--- Include as many relevant details about the environment you experienced the bug in -->
|
||||
* Version used (`silo --version`):
|
||||
* Server setup and configuration:
|
||||
* Operating System and version (`uname -a`):
|
||||
@@ -1,8 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Silo Documentation
|
||||
url: https://silo.pgsty.com/docs/
|
||||
about: Installation, configuration, operations, and compatibility guidance
|
||||
- name: Private Security Report
|
||||
url: https://github.com/pgsty/silo/security/advisories/new
|
||||
about: Privately report a suspected vulnerability in Silo
|
||||
@@ -1,23 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: community, triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Suggest improvements to the PGSTY SILO server (`pgsty/silo`) here.
|
||||
For patches, see the [contribution guide](https://github.com/pgsty/silo/blob/main/CONTRIBUTING.md).
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -1,45 +0,0 @@
|
||||
## Contribution Licensing (no CLA, inbound=outbound, DCO required)
|
||||
|
||||
This pull request contributes to PGSTY SILO (`pgsty/silo`). Code contributions
|
||||
are accepted under AGPL-3.0-or-later, the same license as the server.
|
||||
This project does not use a CLA or require a separate Apache-2.0 license grant.
|
||||
By submitting this pull request I represent that I have the right to contribute
|
||||
the code changes under this repository's
|
||||
[GNU Affero General Public License v3.0 or later](https://www.gnu.org/licenses/agpl-3.0.html)
|
||||
and retain copyright in my original work. Existing copyright and license
|
||||
notices remain intact; separately licensed material keeps its applicable terms.
|
||||
Every commit must carry a DCO `Signed-off-by` trailer
|
||||
(`git commit -s`) certifying the
|
||||
[Developer Certificate of Origin](https://developercertificate.org/) — see
|
||||
[CONTRIBUTING.md](https://github.com/pgsty/silo/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## Description
|
||||
|
||||
|
||||
## Motivation and Context
|
||||
|
||||
|
||||
## How to test this PR?
|
||||
|
||||
|
||||
## Compatibility impact
|
||||
|
||||
<!-- Note effects on APIs, clients, MINIO_* configuration, metrics, headers,
|
||||
routes, storage metadata, module/import paths, upgrades, or rollback. -->
|
||||
|
||||
|
||||
## Types of changes
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Optimization (provides speedup with no functional changes)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
|
||||
|
||||
## Checklist:
|
||||
- [ ] All commits are signed off (`git commit -s`) per the [DCO](https://developercertificate.org/)
|
||||
- [ ] Fixes a regression (If yes, please add `commit-id` or `PR #` here)
|
||||
- [ ] Unit tests added/updated
|
||||
- [ ] `make verifiers` passes
|
||||
- [ ] Relevant package tests and `make build` pass
|
||||
- [ ] Compatibility and rollback impact documented
|
||||
- [ ] Internal documentation updated
|
||||
- [ ] Public documentation update opened in `pgsty/silo.pgsty.com`, if needed
|
||||
@@ -1,96 +0,0 @@
|
||||
version: 2
|
||||
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
|
||||
builds:
|
||||
- id: silo
|
||||
main: .
|
||||
binary: silo
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
goamd64:
|
||||
- v1
|
||||
flags:
|
||||
- -tags=kqueue
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- "{{ .Env.LDFLAGS }}"
|
||||
|
||||
archives:
|
||||
- id: silo
|
||||
ids:
|
||||
- silo
|
||||
name_template: "silo_{{ .Env.PKG_VERSION }}_{{ .Os }}_{{ .Arch }}"
|
||||
# Explicit so the license materials cannot silently drop out of the
|
||||
# binary archives: GoReleaser's default file globs would miss NOTICE.
|
||||
# CREDITS stays out deliberately -- at 1.8MB it would dominate the
|
||||
# archive; it remains available in the repository and the OCI image.
|
||||
files:
|
||||
- README.md
|
||||
- LICENSE
|
||||
- NOTICE
|
||||
|
||||
checksum:
|
||||
name_template: "silo_{{ .Env.PKG_VERSION }}_checksums.txt"
|
||||
algorithm: sha256
|
||||
|
||||
# Generate one SPDX JSON document per platform archive. SBOMs are created
|
||||
# before the checksum stage, so the signed checksum manifest covers both the
|
||||
# archives and their corresponding software bills of materials.
|
||||
sboms:
|
||||
- id: silo-archives
|
||||
artifacts: archive
|
||||
# Avoid network-backed package enrichment: the release SBOM must be
|
||||
# reproducible from the artifact alone and the PR gate must work offline.
|
||||
args:
|
||||
- "$artifact"
|
||||
- "--output"
|
||||
- "spdx-json=$document"
|
||||
env:
|
||||
- SYFT_FILE_METADATA_CATALOGER_ENABLED=true
|
||||
- SYFT_CHECK_FOR_APP_UPDATE=false
|
||||
|
||||
# A keyless Sigstore bundle is the detached signature for the checksum
|
||||
# manifest. Consumers can verify the whole archive/SBOM set without trusting a
|
||||
# long-lived project key copied into the repository.
|
||||
signs:
|
||||
- id: silo-checksums
|
||||
cmd: cosign
|
||||
signature: "${artifact}.sigstore.json"
|
||||
args:
|
||||
- sign-blob
|
||||
- "--bundle=${signature}"
|
||||
- "${artifact}"
|
||||
- --yes
|
||||
artifacts: checksum
|
||||
output: true
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: pgsty
|
||||
name: silo
|
||||
draft: true
|
||||
prerelease: false
|
||||
replace_existing_draft: true
|
||||
replace_existing_artifacts: false
|
||||
mode: replace
|
||||
# Draft replacement matches the release name; keep it identical to the tag.
|
||||
name_template: "{{ .Tag }}"
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- "^docs:"
|
||||
- "^test:"
|
||||
- "Merge pull request"
|
||||
- "Merge branch"
|
||||
|
||||
announce:
|
||||
skip: true
|
||||
@@ -1,111 +0,0 @@
|
||||
name: silo
|
||||
arch: ${NFPM_ARCH}
|
||||
platform: linux
|
||||
version: ${PKG_VERSION}
|
||||
version_schema: none
|
||||
release: ${NFPM_RELEASE}
|
||||
section: utils
|
||||
priority: optional
|
||||
maintainer: "Ruohang Feng (@Vonng) <rh@vonng.com>"
|
||||
description: S3-Interface Libre Object Storage, a community-maintained S3-compatible server.
|
||||
vendor: PGSTY
|
||||
homepage: https://silo.pgsty.com
|
||||
license: AGPL-3.0-or-later
|
||||
|
||||
contents:
|
||||
- src: ${NFPM_SOURCE}
|
||||
dst: /usr/bin/silo
|
||||
expand: true
|
||||
file_info:
|
||||
mode: 0755
|
||||
owner: root
|
||||
group: root
|
||||
- src: ${NFPM_UNIT}
|
||||
dst: /usr/lib/systemd/system/silo.service
|
||||
expand: true
|
||||
file_info:
|
||||
mode: 0644
|
||||
owner: root
|
||||
group: root
|
||||
- src: ${NFPM_DEFAULTS}
|
||||
dst: /etc/default/silo
|
||||
type: config|noreplace
|
||||
expand: true
|
||||
file_info:
|
||||
mode: 0644
|
||||
owner: root
|
||||
group: root
|
||||
- src: ${NFPM_SYSUSERS}
|
||||
dst: /usr/lib/sysusers.d/silo.conf
|
||||
expand: true
|
||||
file_info:
|
||||
mode: 0644
|
||||
owner: root
|
||||
group: root
|
||||
# The license materials are declared once per packager: nfpm only honors
|
||||
# type: license on rpm and silently drops such entries from deb and apk, so
|
||||
# the rpm keeps its %license flag while deb and apk carry plain files at the
|
||||
# same path.
|
||||
- src: ${NFPM_LICENSE}
|
||||
dst: /usr/share/doc/silo/LICENSE
|
||||
type: license
|
||||
packager: rpm
|
||||
expand: true
|
||||
file_info:
|
||||
mode: 0644
|
||||
owner: root
|
||||
group: root
|
||||
- src: ${NFPM_NOTICE}
|
||||
dst: /usr/share/doc/silo/NOTICE
|
||||
type: license
|
||||
packager: rpm
|
||||
expand: true
|
||||
file_info:
|
||||
mode: 0644
|
||||
owner: root
|
||||
group: root
|
||||
- src: ${NFPM_LICENSE}
|
||||
dst: /usr/share/doc/silo/LICENSE
|
||||
packager: deb
|
||||
expand: true
|
||||
file_info:
|
||||
mode: 0644
|
||||
owner: root
|
||||
group: root
|
||||
- src: ${NFPM_NOTICE}
|
||||
dst: /usr/share/doc/silo/NOTICE
|
||||
packager: deb
|
||||
expand: true
|
||||
file_info:
|
||||
mode: 0644
|
||||
owner: root
|
||||
group: root
|
||||
- src: ${NFPM_LICENSE}
|
||||
dst: /usr/share/doc/silo/LICENSE
|
||||
packager: apk
|
||||
expand: true
|
||||
file_info:
|
||||
mode: 0644
|
||||
owner: root
|
||||
group: root
|
||||
- src: ${NFPM_NOTICE}
|
||||
dst: /usr/share/doc/silo/NOTICE
|
||||
packager: apk
|
||||
expand: true
|
||||
file_info:
|
||||
mode: 0644
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
scripts:
|
||||
postinstall: buildscripts/package/postinstall.sh
|
||||
preremove: buildscripts/package/preremove.sh
|
||||
|
||||
rpm:
|
||||
group: Applications/File
|
||||
compression: gzip:9
|
||||
|
||||
deb:
|
||||
compression: gzip
|
||||
fields:
|
||||
License: AGPL-3.0-or-later
|
||||
@@ -1,105 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="1500" height="570" viewBox="0 0 1500 570"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
role="img" aria-labelledby="silo-logo-title silo-logo-desc"
|
||||
shape-rendering="geometricPrecision">
|
||||
<title id="silo-logo-title">SILO logo</title>
|
||||
<desc id="silo-logo-desc">The SILO horizontal lockup: the circular silo emblem on the left, the SILO wordmark on the right.</desc>
|
||||
|
||||
<defs>
|
||||
<!-- Emblem gradient, in the emblem's local coordinates; the group transform maps it. -->
|
||||
<linearGradient id="silo-logo-mark-color" x1="276" y1="720" x2="742" y2="286" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#064A83"/>
|
||||
<stop offset="0.5" stop-color="#007FA8"/>
|
||||
<stop offset="1" stop-color="#22C7C9"/>
|
||||
</linearGradient>
|
||||
<!-- Wordmark gradient, in the wordmark's local coordinates. -->
|
||||
<linearGradient id="silo-logo-word-color" x1="44.0" y1="-94.4" x2="1950.0" y2="794.4" gradientUnits="userSpaceOnUse">
|
||||
<stop class="silo-logo-wm-a" offset="0.06" stop-color="#1d588c"/>
|
||||
<stop class="silo-logo-wm-b" offset="0.94" stop-color="#b4762e"/>
|
||||
</linearGradient>
|
||||
<style>
|
||||
/* Light theme values of --pg-strong / --copper; dark theme swaps in its own pair. */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.silo-logo-wm-a { stop-color: #7fb8e8; }
|
||||
.silo-logo-wm-b { stop-color: #e0a35c; }
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<!-- Circular emblem, shifted from its native viewBox (230 213 570 570) to the 0..570 square. -->
|
||||
<g id="silo-logo-mark" transform="translate(-230 -213)" fill="url(#silo-logo-mark-color)">
|
||||
<!-- Outer circular band, intentionally opened at the lower-right plinth. -->
|
||||
<path d="
|
||||
M 734 676
|
||||
A 283.5 278.5 0 1 0 310 692
|
||||
L 359 692
|
||||
A 247 248.5 0 1 1 688 676
|
||||
Z"/>
|
||||
|
||||
<!-- Flowing left wall; its upper tangent matches the inner ellipse. -->
|
||||
<path d="
|
||||
M 300.57 375
|
||||
C 292 390 300 430 328 450
|
||||
C 343 461 357 472 374 481
|
||||
C 410 501 426 517 426 544
|
||||
L 426 676
|
||||
L 336 676
|
||||
C 308 647 286 608 274 565
|
||||
C 262 522 263 479 272 439
|
||||
C 278 413 286 389 300.57 375
|
||||
Z"/>
|
||||
|
||||
<!-- Right column with tangent-continuous upper shoulder. -->
|
||||
<path d="
|
||||
M 602 373
|
||||
Q 602 368 607 370
|
||||
C 619 374 634 381 634 389
|
||||
L 634 647
|
||||
Q 634 649 636 649
|
||||
L 708 649
|
||||
L 734 676
|
||||
L 602 676
|
||||
Z"/>
|
||||
|
||||
<!-- Lower circular cap. -->
|
||||
<path d="
|
||||
M 310 692
|
||||
L 714 692
|
||||
C 668 743 596 774 512 774
|
||||
C 428 774 355 743 310 692
|
||||
Z"/>
|
||||
|
||||
<!-- Main silo body, with a tangent-continuous right shoulder. -->
|
||||
<path d="
|
||||
M 389 389
|
||||
C 389 374 447 351 512 351
|
||||
C 540 351 565 354 580 359
|
||||
Q 583 360 583 364
|
||||
L 583 676
|
||||
L 443 676
|
||||
L 443 541
|
||||
C 443 509 426 490 389 470
|
||||
Z"/>
|
||||
|
||||
<!-- Peaked roof with softly tapered, burr-free tips. -->
|
||||
<path d="
|
||||
M 512 274
|
||||
L 647 365
|
||||
Q 649 370 647 376
|
||||
C 609 350 563 337 512 337
|
||||
C 460 337 414 350 377 376
|
||||
Q 375 370 377 365
|
||||
Z"/>
|
||||
</g>
|
||||
|
||||
<!-- Wordmark, scaled 3/7 to a 300-unit cap height and centered on the emblem's axis. -->
|
||||
<g id="silo-logo-word" transform="translate(645.429 135) scale(0.428571)"
|
||||
fill="url(#silo-logo-word-color)" fill-rule="nonzero">
|
||||
<path id="silo-logo-s" d="M0 592V492H134V551L167 584H374L408 550V434L375 401H110L2 293V108L110 0H426L534 108V209H400V149L367 116H169L136 149V252L169 285H434L542 393V590L432 700H108Z"/>
|
||||
<path id="silo-logo-i" d="M637 0H773V700H637Z"/>
|
||||
<path id="silo-logo-l" d="M888 0H1024V585H1374V700H888Z"/>
|
||||
<path id="silo-logo-o" d="M1404 585V115L1519 0H1879L1994 115V585L1879 700H1519ZM1807 584 1858 533V167L1807 116H1591L1540 167V533L1591 584Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -1,30 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="1624" height="570" viewBox="0 0 1994 700"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
role="img" aria-labelledby="title desc"
|
||||
shape-rendering="geometricPrecision">
|
||||
<title id="title">SILO wordmark</title>
|
||||
<desc id="desc">The SILO wordmark set in Chakra Petch Bold, outlined, with the blue-to-copper brand gradient.</desc>
|
||||
|
||||
<defs>
|
||||
<linearGradient id="silo-wordmark-color" x1="44.0" y1="-94.4" x2="1950.0" y2="794.4" gradientUnits="userSpaceOnUse">
|
||||
<stop class="wm-a" offset="0.06" stop-color="#1d588c"/>
|
||||
<stop class="wm-b" offset="0.94" stop-color="#b4762e"/>
|
||||
</linearGradient>
|
||||
<style>
|
||||
/* Light theme values of --pg-strong / --copper; dark theme swaps in its own pair. */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.wm-a { stop-color: #7fb8e8; }
|
||||
.wm-b { stop-color: #e0a35c; }
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
|
||||
<g fill="url(#silo-wordmark-color)" fill-rule="nonzero">
|
||||
<path id="silo-s" d="M0 592V492H134V551L167 584H374L408 550V434L375 401H110L2 293V108L110 0H426L534 108V209H400V149L367 116H169L136 149V252L169 285H434L542 393V590L432 700H108Z"/>
|
||||
<path id="silo-i" d="M637 0H773V700H637Z"/>
|
||||
<path id="silo-l" d="M888 0H1024V585H1374V700H888Z"/>
|
||||
<path id="silo-o" d="M1404 585V115L1519 0H1879L1994 115V585L1879 700H1519ZM1807 584 1858 533V167L1807 116H1591L1540 167V533L1591 584Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,82 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="570" height="570" viewBox="230 213 570 570"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
role="img" aria-labelledby="title desc"
|
||||
shape-rendering="geometricPrecision">
|
||||
<title id="title">SILO emblem</title>
|
||||
<desc id="desc">A smooth circular SILO mark with a peaked roof, flowing left wall, columns, and a curved base.</desc>
|
||||
|
||||
<defs>
|
||||
<linearGradient id="silo-color" x1="276" y1="720" x2="742" y2="286" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#064A83"/>
|
||||
<stop offset="0.5" stop-color="#007FA8"/>
|
||||
<stop offset="1" stop-color="#22C7C9"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<g fill="url(#silo-color)">
|
||||
<!-- Outer circular band, intentionally opened at the lower-right plinth. -->
|
||||
<path d="
|
||||
M 734 676
|
||||
A 283.5 278.5 0 1 0 310 692
|
||||
L 359 692
|
||||
A 247 248.5 0 1 1 688 676
|
||||
Z"/>
|
||||
|
||||
<!-- Flowing left wall; its upper tangent matches the inner ellipse. -->
|
||||
<path d="
|
||||
M 300.57 375
|
||||
C 292 390 300 430 328 450
|
||||
C 343 461 357 472 374 481
|
||||
C 410 501 426 517 426 544
|
||||
L 426 676
|
||||
L 336 676
|
||||
C 308 647 286 608 274 565
|
||||
C 262 522 263 479 272 439
|
||||
C 278 413 286 389 300.57 375
|
||||
Z"/>
|
||||
|
||||
<!-- Right column with tangent-continuous upper shoulder. -->
|
||||
<path d="
|
||||
M 602 373
|
||||
Q 602 368 607 370
|
||||
C 619 374 634 381 634 389
|
||||
L 634 647
|
||||
Q 634 649 636 649
|
||||
L 708 649
|
||||
L 734 676
|
||||
L 602 676
|
||||
Z"/>
|
||||
|
||||
<!-- Lower circular cap. -->
|
||||
<path d="
|
||||
M 310 692
|
||||
L 714 692
|
||||
C 668 743 596 774 512 774
|
||||
C 428 774 355 743 310 692
|
||||
Z"/>
|
||||
|
||||
<!-- Main silo body, with a tangent-continuous right shoulder. -->
|
||||
<path d="
|
||||
M 389 389
|
||||
C 389 374 447 351 512 351
|
||||
C 540 351 565 354 580 359
|
||||
Q 583 360 583 364
|
||||
L 583 676
|
||||
L 443 676
|
||||
L 443 541
|
||||
C 443 509 426 490 389 470
|
||||
Z"/>
|
||||
|
||||
<!-- Peaked roof with softly tapered, burr-free tips. -->
|
||||
<path d="
|
||||
M 512 274
|
||||
L 647 365
|
||||
Q 649 370 647 376
|
||||
C 609 350 563 337 512 337
|
||||
C 460 337 414 350 377 376
|
||||
Q 375 370 377 365
|
||||
Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1,60 +0,0 @@
|
||||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 30
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
|
||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||
daysUntilClose: 15
|
||||
|
||||
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
|
||||
onlyLabels: []
|
||||
|
||||
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
|
||||
exemptLabels:
|
||||
- "security"
|
||||
- "pending discussion"
|
||||
- "do-not-close"
|
||||
|
||||
# Set to true to ignore issues in a project (defaults to false)
|
||||
exemptProjects: false
|
||||
|
||||
# Set to true to ignore issues in a milestone (defaults to false)
|
||||
exemptMilestones: false
|
||||
|
||||
# Set to true to ignore issues with an assignee (defaults to false)
|
||||
exemptAssignees: false
|
||||
|
||||
# Label to use when marking as stale
|
||||
staleLabel: stale
|
||||
|
||||
# Comment to post when marking as stale. Set to `false` to disable
|
||||
markComment: >-
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed after 15 days if no further activity
|
||||
occurs. Thank you for your contributions.
|
||||
# Comment to post when removing the stale label.
|
||||
# unmarkComment: >
|
||||
# Your comment here.
|
||||
|
||||
# Comment to post when closing a stale Issue or Pull Request.
|
||||
# closeComment: >
|
||||
# Your comment here.
|
||||
|
||||
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||
limitPerRun: 1
|
||||
|
||||
# Limit to only `issues` or `pulls`
|
||||
# only: issues
|
||||
|
||||
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
|
||||
# pulls:
|
||||
# daysUntilStale: 30
|
||||
# markComment: >
|
||||
# This pull request has been automatically marked as stale because it has not had
|
||||
# recent activity. It will be closed if no further activity occurs. Thank you
|
||||
# for your contributions.
|
||||
|
||||
# issues:
|
||||
# exemptLabels:
|
||||
# - confirmed
|
||||
@@ -1,43 +0,0 @@
|
||||
name: DCO
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Verify DCO sign-off
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Every non-merge commit in the pull request must carry a Signed-off-by
|
||||
# trailer matching the commit author's email, certifying the Developer
|
||||
# Certificate of Origin 1.1 (https://developercertificate.org/).
|
||||
# Only commits authored from a GitHub-issued bot address are exempt; a
|
||||
# display name is attacker-controlled and must never grant the exemption.
|
||||
- name: Check Signed-off-by trailers
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
fail=0
|
||||
while read -r sha; do
|
||||
author_name="$(git log -1 --format='%an' "${sha}")"
|
||||
author_email="$(git log -1 --format='%ae' "${sha}")"
|
||||
case "${author_email}" in
|
||||
*"[bot]@users.noreply.github.com") continue ;;
|
||||
esac
|
||||
if ! git log -1 --format='%(trailers:key=Signed-off-by,valueonly)' "${sha}" |
|
||||
grep -qiF "<${author_email}>"; then
|
||||
echo "::error::commit ${sha} by ${author_name} <${author_email}> lacks a matching Signed-off-by trailer; sign with 'git commit -s', repair with 'git rebase --signoff'"
|
||||
fail=1
|
||||
fi
|
||||
done < <(git rev-list --no-merges "${BASE_SHA}..${HEAD_SHA}")
|
||||
exit "${fail}"
|
||||
@@ -1,422 +0,0 @@
|
||||
name: Publish Docker Image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Published RELEASE.* tag to package as pgsty/silo"
|
||||
required: true
|
||||
type: string
|
||||
recovery:
|
||||
description: "Run the current main workflow against an already-published tag"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
attestations: write
|
||||
artifact-metadata: write
|
||||
|
||||
concurrency:
|
||||
group: docker-release
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Images are built from a published release rather than from the build
|
||||
# that produced it, so an abandoned draft can never leave :latest
|
||||
# pointing at something nobody shipped.
|
||||
- name: Validate published release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
INPUT_TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${INPUT_TAG}"
|
||||
VERSION_HYPHEN="${TAG#RELEASE.}"
|
||||
PKG_VERSION="$(echo "${VERSION_HYPHEN}" | sed -E 's/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2})-([0-9]{2})-([0-9]{2})Z$/\1\2\3\4\5\6.0.0/')"
|
||||
if [ "${PKG_VERSION}" = "${VERSION_HYPHEN}" ]; then
|
||||
echo "Invalid release tag: ${TAG}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IS_DRAFT="$(gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" --json isDraft --jq .isDraft)"
|
||||
IS_PRERELEASE="$(gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" --json isPrerelease --jq .isPrerelease)"
|
||||
PUBLISHED_AT="$(gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" --json publishedAt --jq .publishedAt)"
|
||||
LATEST_TAG="$(gh release view --repo "${GITHUB_REPOSITORY}" --json tagName --jq .tagName)"
|
||||
|
||||
if [ "${IS_DRAFT}" != false ] || [ "${IS_PRERELEASE}" != false ] || [ -z "${PUBLISHED_AT}" ]; then
|
||||
echo "${TAG} must be a published, non-prerelease GitHub Release"
|
||||
exit 1
|
||||
fi
|
||||
# This workflow moves :latest, so it must not run for an older tag.
|
||||
if [ "${TAG}" != "${LATEST_TAG}" ]; then
|
||||
echo "Refusing to replace Docker latest with non-latest release ${TAG} (latest is ${LATEST_TAG})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
echo "RELEASE_TAG=${TAG}"
|
||||
echo "PKG_VERSION=${PKG_VERSION}"
|
||||
echo "PUBLISHED_AT=${PUBLISHED_AT}"
|
||||
} >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Validate Docker Hub credentials
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${{ secrets.DOCKERHUB_USERNAME }}" ] || [ -z "${{ secrets.DOCKERHUB_TOKEN }}" ]; then
|
||||
echo "Missing Docker Hub credentials. Set DOCKERHUB_USERNAME and DOCKERHUB_TOKEN repository secrets."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout release tag
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Verify workflow identity matches release source
|
||||
env:
|
||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||||
RECOVERY: ${{ inputs.recovery }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CHECKED_OUT_REVISION="$(git rev-parse HEAD)"
|
||||
if [ "${CHECKED_OUT_REVISION}" != "${GITHUB_SHA}" ]; then
|
||||
if [ "${RECOVERY}" != "true" ] || [ "${GITHUB_REF}" != "refs/heads/${DEFAULT_BRANCH}" ]; then
|
||||
echo "Checked out ${CHECKED_OUT_REVISION}, but workflow identity is ${GITHUB_SHA}. Dispatch from ${RELEASE_TAG}, or use recovery from ${DEFAULT_BRANCH}." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Recovery workflow ${GITHUB_SHA} is packaging published source ${CHECKED_OUT_REVISION}."
|
||||
fi
|
||||
|
||||
- name: Prepare verified Docker contexts
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
assets_dir="docker-release/assets"
|
||||
mkdir -p "${assets_dir}"
|
||||
|
||||
amd64_archive="silo_${PKG_VERSION}_linux_amd64.tar.gz"
|
||||
arm64_archive="silo_${PKG_VERSION}_linux_arm64.tar.gz"
|
||||
checksums="silo_${PKG_VERSION}_checksums.txt"
|
||||
gh release download "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" \
|
||||
--dir "${assets_dir}" \
|
||||
--pattern "${amd64_archive}" \
|
||||
--pattern "${arm64_archive}" \
|
||||
--pattern "${checksums}"
|
||||
|
||||
# The binaries going into the images are the published ones, checked
|
||||
# against the published checksums, not a rebuild that merely ought to
|
||||
# match them. awk matches the manifest filename column exactly: a
|
||||
# substring grep would also pull in the archive's .sbom.json line,
|
||||
# whose file is deliberately not downloaded in this lane.
|
||||
cd "${assets_dir}"
|
||||
awk -v name="${amd64_archive}" '$2 == name' "${checksums}" | sha256sum --check
|
||||
awk -v name="${arm64_archive}" '$2 == name' "${checksums}" | sha256sum --check
|
||||
|
||||
# The checksum manifest and both archives must have provenance from
|
||||
# this repository's release workflow at the exact checked-out tag.
|
||||
for artifact in "${checksums}" "${amd64_archive}" "${arm64_archive}"; do
|
||||
gh attestation verify "${artifact}" \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--signer-workflow "${GITHUB_REPOSITORY}/.github/workflows/release.yml" \
|
||||
--source-digest "$(git -C "${GITHUB_WORKSPACE}" rev-parse HEAD)" \
|
||||
--source-ref "refs/tags/${RELEASE_TAG}" >/dev/null
|
||||
done
|
||||
cd "${GITHUB_WORKSPACE}"
|
||||
|
||||
# Dockerfile.goreleaser expects the binary at the context root and
|
||||
# the entrypoint scripts under dockerscripts/, which is the layout
|
||||
# GoReleaser used to assemble via extra_files.
|
||||
for arch in amd64 arm64; do
|
||||
context="docker-release/${arch}"
|
||||
archive="${assets_dir}/silo_${PKG_VERSION}_linux_${arch}.tar.gz"
|
||||
mkdir -p "${context}/dockerscripts"
|
||||
tar -xzf "${archive}" -C "${context}" silo
|
||||
cp Dockerfile.goreleaser Dockerfile.distroless LICENSE NOTICE CREDITS "${context}/"
|
||||
cp dockerscripts/docker-entrypoint.sh dockerscripts/download-static-curl.sh \
|
||||
"${context}/dockerscripts/"
|
||||
done
|
||||
|
||||
# The classic image bundles mcli. Resolve its two archive digests
|
||||
# from the immutable published release instead of trusting defaults
|
||||
# copied into an older Server tag. This also gives a recovery run a
|
||||
# narrow override when a tag selected the right mcli release but
|
||||
# accidentally retained stale archive pins.
|
||||
MC_REPO="$(awk -F= '/^ARG MC_REPO=/{print $2; exit}' Dockerfile.goreleaser)"
|
||||
MC_VERSION="$(awk -F= '/^ARG MC_VERSION=/{print $2; exit}' Dockerfile.goreleaser)"
|
||||
test -n "${MC_REPO}"
|
||||
test -n "${MC_VERSION}"
|
||||
MC_VERSION_HYPHEN="${MC_VERSION#RELEASE.}"
|
||||
MC_PKG_VERSION="$(echo "${MC_VERSION_HYPHEN}" | sed -E 's/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2})-([0-9]{2})-([0-9]{2})Z$/\1\2\3\4\5\6.0.0/')"
|
||||
if [ "${MC_PKG_VERSION}" = "${MC_VERSION_HYPHEN}" ]; then
|
||||
echo "Invalid bundled mcli tag: ${MC_VERSION}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$(gh release view "${MC_VERSION}" --repo "${MC_REPO}" --json isDraft --jq .isDraft)" != false ] || \
|
||||
[ "$(gh release view "${MC_VERSION}" --repo "${MC_REPO}" --json isPrerelease --jq .isPrerelease)" != false ] || \
|
||||
[ "$(gh release view "${MC_VERSION}" --repo "${MC_REPO}" --json isImmutable --jq .isImmutable)" != true ]; then
|
||||
echo "Bundled mcli ${MC_REPO}@${MC_VERSION} must be a published immutable release" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mc_checksums="mcli_${MC_PKG_VERSION}_checksums.txt"
|
||||
gh release download "${MC_VERSION}" --repo "${MC_REPO}" \
|
||||
--dir "${assets_dir}" --pattern "${mc_checksums}"
|
||||
gh attestation verify "${assets_dir}/${mc_checksums}" \
|
||||
--repo "${MC_REPO}" \
|
||||
--signer-workflow "${MC_REPO}/.github/workflows/release.yml" \
|
||||
--source-ref "refs/tags/${MC_VERSION}" >/dev/null
|
||||
|
||||
MC_AMD64_SHA256="$(awk -v name="mcli_${MC_PKG_VERSION}_linux_amd64.tar.gz" '$2 == name {print $1}' "${assets_dir}/${mc_checksums}")"
|
||||
MC_ARM64_SHA256="$(awk -v name="mcli_${MC_PKG_VERSION}_linux_arm64.tar.gz" '$2 == name {print $1}' "${assets_dir}/${mc_checksums}")"
|
||||
[[ "${MC_AMD64_SHA256}" =~ ^[0-9a-f]{64}$ ]]
|
||||
[[ "${MC_ARM64_SHA256}" =~ ^[0-9a-f]{64}$ ]]
|
||||
|
||||
{
|
||||
echo "MC_AMD64_SHA256=${MC_AMD64_SHA256}"
|
||||
echo "MC_ARM64_SHA256=${MC_ARM64_SHA256}"
|
||||
} >> "${GITHUB_ENV}"
|
||||
|
||||
echo "RELEASE_REVISION=$(git rev-parse HEAD)" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: arm64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Install Syft
|
||||
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
||||
with:
|
||||
syft-version: v1.50.0
|
||||
|
||||
- name: Build and push amd64 image
|
||||
id: build-amd64
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: docker-release/amd64
|
||||
file: docker-release/amd64/Dockerfile.goreleaser
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
build-args: |
|
||||
MC_AMD64_SHA256=${{ env.MC_AMD64_SHA256 }}
|
||||
MC_ARM64_SHA256=${{ env.MC_ARM64_SHA256 }}
|
||||
tags: |
|
||||
pgsty/silo:${{ env.RELEASE_TAG }}-amd64
|
||||
pgsty/silo:latest-amd64
|
||||
labels: |
|
||||
org.opencontainers.image.version=${{ env.RELEASE_TAG }}
|
||||
org.opencontainers.image.created=${{ env.PUBLISHED_AT }}
|
||||
org.opencontainers.image.revision=${{ env.RELEASE_REVISION }}
|
||||
|
||||
- name: Build and push arm64 image
|
||||
id: build-arm64
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: docker-release/arm64
|
||||
file: docker-release/arm64/Dockerfile.goreleaser
|
||||
platforms: linux/arm64
|
||||
push: true
|
||||
build-args: |
|
||||
MC_AMD64_SHA256=${{ env.MC_AMD64_SHA256 }}
|
||||
MC_ARM64_SHA256=${{ env.MC_ARM64_SHA256 }}
|
||||
tags: |
|
||||
pgsty/silo:${{ env.RELEASE_TAG }}-arm64
|
||||
pgsty/silo:latest-arm64
|
||||
labels: |
|
||||
org.opencontainers.image.version=${{ env.RELEASE_TAG }}
|
||||
org.opencontainers.image.created=${{ env.PUBLISHED_AT }}
|
||||
org.opencontainers.image.revision=${{ env.RELEASE_REVISION }}
|
||||
|
||||
# The distroless variant is a pilot published alongside the classic
|
||||
# image; it ships the silo binary alone and relies on the native
|
||||
# `silo healthcheck` subcommand for container health.
|
||||
# Design: https://silo.pgsty.com/compatibility/feature/healthcheck/
|
||||
- name: Build and push amd64 distroless image
|
||||
id: build-amd64-distroless
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: docker-release/amd64
|
||||
file: docker-release/amd64/Dockerfile.distroless
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: |
|
||||
pgsty/silo:${{ env.RELEASE_TAG }}-distroless-amd64
|
||||
pgsty/silo:distroless-amd64
|
||||
labels: |
|
||||
org.opencontainers.image.version=${{ env.RELEASE_TAG }}
|
||||
org.opencontainers.image.created=${{ env.PUBLISHED_AT }}
|
||||
org.opencontainers.image.revision=${{ env.RELEASE_REVISION }}
|
||||
|
||||
- name: Build and push arm64 distroless image
|
||||
id: build-arm64-distroless
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: docker-release/arm64
|
||||
file: docker-release/arm64/Dockerfile.distroless
|
||||
platforms: linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
pgsty/silo:${{ env.RELEASE_TAG }}-distroless-arm64
|
||||
pgsty/silo:distroless-arm64
|
||||
labels: |
|
||||
org.opencontainers.image.version=${{ env.RELEASE_TAG }}
|
||||
org.opencontainers.image.created=${{ env.PUBLISHED_AT }}
|
||||
org.opencontainers.image.revision=${{ env.RELEASE_REVISION }}
|
||||
|
||||
- name: Verify HEALTHCHECK survived the distroless push
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# HEALTHCHECK is a Docker extension absent from the OCI image
|
||||
# spec, and a publish path can drop it silently. Check the pushed
|
||||
# architecture image now, before the versioned and rolling
|
||||
# multi-arch manifests are created, so a broken health config
|
||||
# stops their promotion. (The architecture-suffixed tags above
|
||||
# are already public by this point - full staging-then-promote
|
||||
# would be a workflow-wide redesign shared with the classic
|
||||
# image lanes.)
|
||||
docker pull "pgsty/silo:${RELEASE_TAG}-distroless-amd64" >/dev/null
|
||||
test "$(docker inspect -f '{{json .Config.Healthcheck.Test}}' "pgsty/silo:${RELEASE_TAG}-distroless-amd64")" \
|
||||
= '["CMD","/usr/bin/silo","healthcheck","ready"]'
|
||||
|
||||
- name: Publish multi-architecture manifests
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p docker-release/metadata
|
||||
docker buildx imagetools create \
|
||||
--tag "pgsty/silo:${RELEASE_TAG}" \
|
||||
--metadata-file docker-release/metadata/release.json \
|
||||
"pgsty/silo:${RELEASE_TAG}-amd64" \
|
||||
"pgsty/silo:${RELEASE_TAG}-arm64"
|
||||
docker buildx imagetools create \
|
||||
--tag "pgsty/silo:latest" \
|
||||
--metadata-file docker-release/metadata/latest.json \
|
||||
"pgsty/silo:latest-amd64" \
|
||||
"pgsty/silo:latest-arm64"
|
||||
docker buildx imagetools inspect "pgsty/silo:${RELEASE_TAG}"
|
||||
docker buildx imagetools inspect "pgsty/silo:latest"
|
||||
|
||||
docker buildx imagetools create \
|
||||
--tag "pgsty/silo:${RELEASE_TAG}-distroless" \
|
||||
--metadata-file docker-release/metadata/release-distroless.json \
|
||||
"pgsty/silo:${RELEASE_TAG}-distroless-amd64" \
|
||||
"pgsty/silo:${RELEASE_TAG}-distroless-arm64"
|
||||
docker buildx imagetools create \
|
||||
--tag "pgsty/silo:distroless" \
|
||||
--metadata-file docker-release/metadata/distroless.json \
|
||||
"pgsty/silo:distroless-amd64" \
|
||||
"pgsty/silo:distroless-arm64"
|
||||
docker buildx imagetools inspect "pgsty/silo:${RELEASE_TAG}-distroless"
|
||||
docker buildx imagetools inspect "pgsty/silo:distroless"
|
||||
|
||||
RELEASE_DIGEST="$(jq -r '."containerimage.descriptor".digest' docker-release/metadata/release.json)"
|
||||
LATEST_DIGEST="$(jq -r '."containerimage.descriptor".digest' docker-release/metadata/latest.json)"
|
||||
if ! [[ "${RELEASE_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
|
||||
echo "Invalid release manifest digest: ${RELEASE_DIGEST}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "${RELEASE_DIGEST}" != "${LATEST_DIGEST}" ]; then
|
||||
echo "Release and latest tags resolved to different manifests" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "SILO_IMAGE_DIGEST=${RELEASE_DIGEST}" >> "${GITHUB_ENV}"
|
||||
|
||||
DISTROLESS_RELEASE_DIGEST="$(jq -r '."containerimage.descriptor".digest' docker-release/metadata/release-distroless.json)"
|
||||
DISTROLESS_ROLLING_DIGEST="$(jq -r '."containerimage.descriptor".digest' docker-release/metadata/distroless.json)"
|
||||
if ! [[ "${DISTROLESS_RELEASE_DIGEST}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
|
||||
echo "Invalid distroless manifest digest: ${DISTROLESS_RELEASE_DIGEST}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "${DISTROLESS_RELEASE_DIGEST}" != "${DISTROLESS_ROLLING_DIGEST}" ]; then
|
||||
echo "Distroless release and rolling tags resolved to different manifests" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "SILO_DISTROLESS_DIGEST=${DISTROLESS_RELEASE_DIGEST}" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Generate architecture image SBOMs
|
||||
env:
|
||||
AMD64_DIGEST: ${{ steps.build-amd64.outputs.digest }}
|
||||
ARM64_DIGEST: ${{ steps.build-arm64.outputs.digest }}
|
||||
DISTROLESS_AMD64_DIGEST: ${{ steps.build-amd64-distroless.outputs.digest }}
|
||||
DISTROLESS_ARM64_DIGEST: ${{ steps.build-arm64-distroless.outputs.digest }}
|
||||
SYFT_CHECK_FOR_APP_UPDATE: "false"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p docker-release/sbom
|
||||
# Each per-architecture digest names an OCI index (image plus the
|
||||
# provenance attestation buildx attaches), and Syft's platform
|
||||
# default on an index follows the amd64 runner - an arm64-only
|
||||
# index would fail outright. Select the platform explicitly.
|
||||
syft "registry:index.docker.io/pgsty/silo@${AMD64_DIGEST}" \
|
||||
--platform linux/amd64 \
|
||||
--output "spdx-json=docker-release/sbom/linux-amd64.spdx.json"
|
||||
syft "registry:index.docker.io/pgsty/silo@${ARM64_DIGEST}" \
|
||||
--platform linux/arm64 \
|
||||
--output "spdx-json=docker-release/sbom/linux-arm64.spdx.json"
|
||||
syft "registry:index.docker.io/pgsty/silo@${DISTROLESS_AMD64_DIGEST}" \
|
||||
--platform linux/amd64 \
|
||||
--output "spdx-json=docker-release/sbom/linux-amd64-distroless.spdx.json"
|
||||
syft "registry:index.docker.io/pgsty/silo@${DISTROLESS_ARM64_DIGEST}" \
|
||||
--platform linux/arm64 \
|
||||
--output "spdx-json=docker-release/sbom/linux-arm64-distroless.spdx.json"
|
||||
|
||||
- name: Attest amd64 image SBOM
|
||||
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
|
||||
with:
|
||||
subject-name: index.docker.io/pgsty/silo
|
||||
subject-digest: ${{ steps.build-amd64.outputs.digest }}
|
||||
sbom-path: docker-release/sbom/linux-amd64.spdx.json
|
||||
push-to-registry: true
|
||||
|
||||
- name: Attest arm64 image SBOM
|
||||
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
|
||||
with:
|
||||
subject-name: index.docker.io/pgsty/silo
|
||||
subject-digest: ${{ steps.build-arm64.outputs.digest }}
|
||||
sbom-path: docker-release/sbom/linux-arm64.spdx.json
|
||||
push-to-registry: true
|
||||
|
||||
- name: Attest amd64 distroless image SBOM
|
||||
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
|
||||
with:
|
||||
subject-name: index.docker.io/pgsty/silo
|
||||
subject-digest: ${{ steps.build-amd64-distroless.outputs.digest }}
|
||||
sbom-path: docker-release/sbom/linux-amd64-distroless.spdx.json
|
||||
push-to-registry: true
|
||||
|
||||
- name: Attest arm64 distroless image SBOM
|
||||
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
|
||||
with:
|
||||
subject-name: index.docker.io/pgsty/silo
|
||||
subject-digest: ${{ steps.build-arm64-distroless.outputs.digest }}
|
||||
sbom-path: docker-release/sbom/linux-arm64-distroless.spdx.json
|
||||
push-to-registry: true
|
||||
|
||||
- name: Attest multi-architecture image provenance
|
||||
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
|
||||
with:
|
||||
subject-name: index.docker.io/pgsty/silo
|
||||
subject-digest: ${{ env.SILO_IMAGE_DIGEST }}
|
||||
push-to-registry: true
|
||||
|
||||
- name: Attest multi-architecture distroless image provenance
|
||||
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
|
||||
with:
|
||||
subject-name: index.docker.io/pgsty/silo
|
||||
subject-digest: ${{ env.SILO_DISTROLESS_DIGEST }}
|
||||
push-to-registry: true
|
||||
@@ -1,234 +0,0 @@
|
||||
name: Finalize Release Packages
|
||||
|
||||
# Manual-only lane that runs AFTER the maintainer has GPG-signed the RPMs in a
|
||||
# Draft release (buildscripts/sign-release-rpms.sh --upload) and BEFORE the
|
||||
# release is published. GPG signing rewrites the RPM bytes, which strands the
|
||||
# SBOMs, the packages checksum manifest, and the attestations that release.yml
|
||||
# generated from the as-built packages. This lane regenerates those materials
|
||||
# from the published (signed) bytes under the workflow identity, so the
|
||||
# sigstore layer describes exactly what the release ships.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Draft RELEASE.* tag whose signed RPMs need refreshed SBOMs and checksums"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
artifact-metadata: write
|
||||
|
||||
concurrency:
|
||||
group: release-${{ inputs.tag }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
finalize:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout release tag
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Verify workflow identity matches release source
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CHECKED_OUT_REVISION="$(git rev-parse HEAD)"
|
||||
if [ "${CHECKED_OUT_REVISION}" != "${GITHUB_SHA}" ]; then
|
||||
echo "Checked out ${CHECKED_OUT_REVISION}, but workflow identity is ${GITHUB_SHA}. Dispatch from the release tag." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Compute release variables
|
||||
env:
|
||||
# Environment passthrough keeps the dispatch input out of the script
|
||||
# source, mirroring release.yml.
|
||||
INPUT_TAG: ${{ inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${INPUT_TAG}"
|
||||
if [[ ! "${TAG}" =~ ^RELEASE\.[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}-[0-9]{2}-[0-9]{2}Z$ ]]; then
|
||||
echo "Invalid release tag format: ${TAG}" >&2
|
||||
exit 1
|
||||
fi
|
||||
VERSION_HYPHEN="${TAG#RELEASE.}"
|
||||
PKG_VERSION="$(echo "${VERSION_HYPHEN}" | sed -E 's/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2})-([0-9]{2})-([0-9]{2})Z$/\1\2\3\4\5\6.0.0/')"
|
||||
{
|
||||
echo "RELEASE_TAG=${TAG}"
|
||||
echo "PKG_VERSION=${PKG_VERSION}"
|
||||
} >> "${GITHUB_ENV}"
|
||||
|
||||
- name: Refuse to touch a published release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "$(gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --json isDraft --jq .isDraft)" != "true" ]; then
|
||||
echo "${RELEASE_TAG} is not a Draft release; finalize runs only before publishing." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install Syft
|
||||
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
||||
with:
|
||||
syft-version: v1.50.0
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
with:
|
||||
cosign-release: v3.1.2
|
||||
|
||||
- name: Download and verify the package set
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# sign-release-rpms.sh owns the package identity; import its values
|
||||
# the same way test-release.yml does so the lanes cannot drift.
|
||||
eval "$(grep -E '^expected_(release|fingerprint)=' buildscripts/sign-release-rpms.sh)"
|
||||
test -n "${expected_release}"
|
||||
test -n "${expected_fingerprint}"
|
||||
|
||||
packages_dir="finalize/packages"
|
||||
sidecar_dir="finalize/sidecars"
|
||||
mkdir -p "${packages_dir}" "${sidecar_dir}"
|
||||
|
||||
manifest="silo_${PKG_VERSION}_packages_checksums.txt"
|
||||
|
||||
# Exactly the twelve manifest subjects land in packages_dir; the
|
||||
# tarball SBOMs in the release root do not match these patterns.
|
||||
gh release download "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --dir "${packages_dir}" \
|
||||
--pattern '*.rpm' --pattern '*.deb' --pattern '*.apk' \
|
||||
--pattern '*.rpm.sbom.json' --pattern '*.deb.sbom.json' --pattern '*.apk.sbom.json'
|
||||
gh release download "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --dir "${sidecar_dir}" \
|
||||
--pattern '*.rpm.sha256sum' --pattern "${manifest}"
|
||||
|
||||
cd "${packages_dir}"
|
||||
subject_count="$(find . -maxdepth 1 -type f | wc -l | tr -d ' ')"
|
||||
if [ "${subject_count}" -ne 12 ]; then
|
||||
echo "Expected twelve package subjects, found ${subject_count}" >&2
|
||||
find . -maxdepth 1 -type f >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The signed RPMs must match the .sha256sum sidecars the signing
|
||||
# script regenerated and uploaded alongside them.
|
||||
for rpm_file in "silo-${PKG_VERSION}-${expected_release}.x86_64.rpm" \
|
||||
"silo-${PKG_VERSION}-${expected_release}.aarch64.rpm"; do
|
||||
test -s "${rpm_file}"
|
||||
actual="$(sha256sum "${rpm_file}" | awk '{print $1}')"
|
||||
recorded="$(awk '{print $1}' "../sidecars/${rpm_file}.sha256sum")"
|
||||
if [ "${actual}" != "${recorded}" ]; then
|
||||
echo "Digest mismatch for ${rpm_file}: sidecar ${recorded}, asset ${actual}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Everything the maintainer did not re-sign must still match the
|
||||
# manifest release.yml generated: this lane refreshes RPM materials,
|
||||
# it does not accept drift anywhere else.
|
||||
for package_file in *.deb *.apk *.deb.sbom.json *.apk.sbom.json; do
|
||||
awk -v name="${package_file}" '$2 == name' "../sidecars/${manifest}" | sha256sum --check
|
||||
done
|
||||
|
||||
- name: Verify RPM GPG signatures
|
||||
run: |
|
||||
set -euo pipefail
|
||||
eval "$(grep -E '^expected_fingerprint=' buildscripts/sign-release-rpms.sh)"
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes rpm
|
||||
sudo rpmkeys --import buildscripts/pgsty-rpm-signing-key.asc
|
||||
key_id="$(printf '%s' "${expected_fingerprint}" | tail -c 8 | tr '[:upper:]' '[:lower:]')"
|
||||
for rpm_file in finalize/packages/*.rpm; do
|
||||
signature_output="$(sudo rpmkeys --checksig --verbose "${rpm_file}")"
|
||||
printf '%s\n' "${signature_output}"
|
||||
if ! printf '%s\n' "${signature_output}" | tr '[:upper:]' '[:lower:]' | grep -q "key id ${key_id}: ok"; then
|
||||
echo "Signature verification failed for ${rpm_file}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Regenerate RPM SBOMs and the packages checksum manifest
|
||||
env:
|
||||
SYFT_CHECK_FOR_APP_UPDATE: "false"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd finalize/packages
|
||||
for rpm_file in *.rpm; do
|
||||
rm -f "${rpm_file}.sbom.json"
|
||||
syft "${rpm_file}" --output "spdx-json=${rpm_file}.sbom.json"
|
||||
done
|
||||
|
||||
manifest="silo_${PKG_VERSION}_packages_checksums.txt"
|
||||
mapfile -t subjects < <(find . -maxdepth 1 -type f \
|
||||
\( -name '*.rpm' -o -name '*.deb' -o -name '*.apk' -o -name '*.sbom.json' \) | sort)
|
||||
if [ "${#subjects[@]}" -ne 12 ]; then
|
||||
echo "Expected six packages and six SBOMs, found ${#subjects[@]} subjects" >&2
|
||||
exit 1
|
||||
fi
|
||||
sha256sum "${subjects[@]}" | sed 's# \./# #' > "${manifest}"
|
||||
cosign sign-blob --bundle="${manifest}.sigstore.json" "${manifest}" --yes
|
||||
|
||||
- name: Attest the finalized package artifacts
|
||||
id: attest-finalize
|
||||
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
|
||||
with:
|
||||
subject-path: |
|
||||
finalize/packages/*.rpm
|
||||
finalize/packages/*.rpm.sbom.json
|
||||
finalize/packages/*_checksums.txt
|
||||
finalize/packages/*_checksums.txt.sigstore.json
|
||||
finalize/sidecars/*.rpm.sha256sum
|
||||
|
||||
- name: Preserve finalize provenance bundle as a release asset
|
||||
env:
|
||||
BUNDLE_PATH: ${{ steps.attest-finalize.outputs.bundle-path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -s "${BUNDLE_PATH}"
|
||||
cp "${BUNDLE_PATH}" "finalize/packages/silo_${PKG_VERSION}_packages_provenance.sigstore.json"
|
||||
|
||||
- name: Upload finalized assets to the Draft release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
eval "$(grep -E '^expected_release=' buildscripts/sign-release-rpms.sh)"
|
||||
manifest="silo_${PKG_VERSION}_packages_checksums.txt"
|
||||
cd finalize/packages
|
||||
files=(
|
||||
"silo-${PKG_VERSION}-${expected_release}.x86_64.rpm.sbom.json"
|
||||
"silo-${PKG_VERSION}-${expected_release}.aarch64.rpm.sbom.json"
|
||||
"${manifest}"
|
||||
"${manifest}.sigstore.json"
|
||||
"silo_${PKG_VERSION}_packages_provenance.sigstore.json"
|
||||
)
|
||||
gh release upload "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --clobber "${files[@]}"
|
||||
|
||||
for asset in "${files[@]}"; do
|
||||
local_digest="sha256:$(sha256sum "${asset}" | awk '{print $1}')"
|
||||
remote_digest=""
|
||||
for attempt in 1 2 3 4 5; do
|
||||
remote_digest="$(gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" --json assets \
|
||||
--jq ".assets[] | select(.name == \"${asset}\") | .digest")"
|
||||
if [ "${local_digest}" = "${remote_digest}" ]; then
|
||||
break
|
||||
fi
|
||||
if [ "${attempt}" -lt 5 ]; then
|
||||
sleep 2
|
||||
fi
|
||||
done
|
||||
if [ "${local_digest}" != "${remote_digest}" ]; then
|
||||
echo "GitHub asset digest mismatch for ${asset}" >&2
|
||||
echo "Local: ${local_digest}" >&2
|
||||
echo "Remote: ${remote_digest}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Verified GitHub asset: ${asset} ${remote_digest}"
|
||||
done
|
||||
@@ -1,156 +0,0 @@
|
||||
name: Go CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
# Cancel superseded runs for the same PR; never cancel main push runs.
|
||||
# Keyed on PR number (not head_ref) so fork PRs sharing a branch name
|
||||
# do not collide.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
verify:
|
||||
name: Format, Build, Vet
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Check gofmt
|
||||
run: |
|
||||
mapfile -t unformatted < <(gofmt -l main.go cmd internal \
|
||||
buildscripts/rebrand-guard buildscripts/helm-migration-guard)
|
||||
if [ "${#unformatted[@]}" -ne 0 ]; then
|
||||
echo "The following files are not gofmt-formatted:"
|
||||
printf '%s\n' "${unformatted[@]}"
|
||||
gofmt -d "${unformatted[@]}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
run: go build ./...
|
||||
|
||||
- name: Vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: Verify rebrand compatibility contracts
|
||||
run: |
|
||||
go run ./buildscripts/rebrand-guard
|
||||
buildscripts/verify-rebrand.sh
|
||||
dockerscripts/docker-entrypoint_test.sh
|
||||
|
||||
quality:
|
||||
name: Lint, Generated Files
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Lint
|
||||
run: make lint
|
||||
|
||||
- name: Check generated files
|
||||
run: make check-gen
|
||||
|
||||
race-s3select:
|
||||
name: Race, S3 Select
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Run S3 Select tests under race detector
|
||||
run: go test -race ./internal/s3select/... -count=1
|
||||
|
||||
crosscompile:
|
||||
name: Cross Compile
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Build supported targets
|
||||
run: make crosscompile
|
||||
|
||||
test-internal:
|
||||
name: Test internal/
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
# internal/http listener tests bind [::1]; runners disable IPv6 by default.
|
||||
- name: Enable IPv6
|
||||
run: |
|
||||
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||
|
||||
- name: Run internal tests
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
MINIO_API_REQUESTS_MAX: "10000"
|
||||
run: go test ./internal/... -count=1
|
||||
|
||||
test-cmd:
|
||||
name: Test cmd/
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 35
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
# Some server tests bind IPv6 listeners; runners disable IPv6 by default.
|
||||
- name: Enable IPv6
|
||||
run: |
|
||||
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
|
||||
sudo sysctl net.ipv6.conf.default.disable_ipv6=0
|
||||
|
||||
- name: Run cmd tests
|
||||
env:
|
||||
CGO_ENABLED: 0
|
||||
MINIO_API_REQUESTS_MAX: "10000"
|
||||
# cmd/ is one large package; raise go test's default 10m per-package
|
||||
# timeout so slower runners fail on the job timeout, not a panic.
|
||||
run: go test ./cmd/ -count=1 -timeout 30m
|
||||
@@ -1,246 +0,0 @@
|
||||
name: Release
|
||||
|
||||
# Retry contract: an absent or single unfinalized Draft may be rebuilt from
|
||||
# scratch; a published release or a Draft carrying finalize's GPG-derived
|
||||
# provenance marker is terminal for this lane. The per-tag lock serializes
|
||||
# workflows, but a maintainer must not publish the Draft while this job runs.
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "RELEASE.*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Release tag (e.g. RELEASE.2026-03-24T12-00-00Z)"
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
artifact-metadata: write
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref_name }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# Build the code at the tag being released, not whatever branch the
|
||||
# dispatch ran from. On a tag push this is the tag ref already; on
|
||||
# workflow_dispatch it pins the checkout to the requested tag so the
|
||||
# artifacts cannot be built from one ref and published under another.
|
||||
ref: ${{ github.event.inputs.tag || github.ref }}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Verify clean checkout
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# GitHub's OIDC certificate records GITHUB_SHA, not the ref passed to
|
||||
# actions/checkout. A manual dispatch must therefore be launched from
|
||||
# the release tag itself; otherwise the provenance identity would
|
||||
# describe different source from the bytes being published.
|
||||
CHECKED_OUT_REVISION="$(git rev-parse HEAD)"
|
||||
if [ "${CHECKED_OUT_REVISION}" != "${GITHUB_SHA}" ]; then
|
||||
echo "Checked out ${CHECKED_OUT_REVISION}, but workflow identity is ${GITHUB_SHA}. Dispatch from the release tag." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ -n "$(git status --porcelain)" ]; then
|
||||
echo "Refusing to release from a dirty working tree:" >&2
|
||||
git status --porcelain >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Verify rebrand compatibility contracts
|
||||
run: |
|
||||
set -euo pipefail
|
||||
go run ./buildscripts/rebrand-guard
|
||||
buildscripts/verify-rebrand.sh
|
||||
dockerscripts/docker-entrypoint_test.sh
|
||||
|
||||
- name: Compute release variables
|
||||
env:
|
||||
# Passed through the environment, never interpolated into the script
|
||||
# body: a dispatch input reaches bash as data, so it cannot inject
|
||||
# commands the way a `${{ ... }}` splice into the source would.
|
||||
INPUT_TAG: ${{ github.event.inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="${INPUT_TAG:-${GITHUB_REF_NAME}}"
|
||||
# Whitelist the exact tag shape before the value is used anywhere. bash
|
||||
# =~ anchors ^...$ to the whole string (not per line, as sed would), so
|
||||
# a tag carrying a newline cannot pass and then smuggle extra lines into
|
||||
# $GITHUB_ENV below.
|
||||
if [[ ! "${TAG}" =~ ^RELEASE\.[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}-[0-9]{2}-[0-9]{2}Z$ ]]; then
|
||||
echo "Invalid release tag format: ${TAG}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! TAG_COMMIT="$(git rev-parse "${TAG}^{commit}" 2>/dev/null)"; then
|
||||
echo "Release tag ${TAG} does not resolve to a commit" >&2
|
||||
exit 1
|
||||
fi
|
||||
HEAD_COMMIT="$(git rev-parse HEAD)"
|
||||
if [ "${TAG_COMMIT}" != "${HEAD_COMMIT}" ]; then
|
||||
echo "Release tag ${TAG} resolves to ${TAG_COMMIT}, checkout is ${HEAD_COMMIT}" >&2
|
||||
exit 1
|
||||
fi
|
||||
VERSION_HYPHEN="${TAG#RELEASE.}"
|
||||
PKG_VERSION="$(echo "${VERSION_HYPHEN}" | sed -E 's/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2})-([0-9]{2})-([0-9]{2})Z$/\1\2\3\4\5\6.0.0/')"
|
||||
VERSION_COLON="$(echo "${VERSION_HYPHEN}" | sed -E 's/T([0-9]{2})-([0-9]{2})-([0-9]{2})Z$/T\1:\2:\3Z/')"
|
||||
LDFLAGS="$(MINIO_RELEASE=RELEASE go run buildscripts/gen-ldflags.go "${VERSION_COLON}")"
|
||||
|
||||
{
|
||||
echo "RELEASE_TAG=${TAG}"
|
||||
echo "PKG_VERSION=${PKG_VERSION}"
|
||||
echo "LDFLAGS=${LDFLAGS}"
|
||||
} >> "${GITHUB_ENV}"
|
||||
|
||||
echo "Release tag: ${TAG}"
|
||||
echo "Package version: ${PKG_VERSION}"
|
||||
echo "LDFLAGS: ${LDFLAGS}"
|
||||
|
||||
- name: Check existing release state
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
buildscripts/check-release-state.sh "${RELEASE_TAG}"
|
||||
|
||||
# Both installer actions are pinned to immutable commits. The explicit
|
||||
# tool versions keep the release format reproducible across workflow
|
||||
# reruns while the installers verify the downloaded executables.
|
||||
- name: Install Syft
|
||||
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
||||
with:
|
||||
syft-version: v1.50.0
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
|
||||
with:
|
||||
cosign-release: v3.1.2
|
||||
|
||||
- name: Build Draft release with GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v7
|
||||
with:
|
||||
version: "~> v2"
|
||||
args: release --clean --skip=validate --config .github/goreleaser.yml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GORELEASER_CURRENT_TAG: ${{ env.RELEASE_TAG }}
|
||||
LDFLAGS: ${{ env.LDFLAGS }}
|
||||
PKG_VERSION: ${{ env.PKG_VERSION }}
|
||||
|
||||
- name: Verify binary provenance stamps
|
||||
run: |
|
||||
set -euo pipefail
|
||||
buildscripts/verify-build-provenance.sh
|
||||
|
||||
- name: Install nFPM
|
||||
run: |
|
||||
set -euo pipefail
|
||||
go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.47.0
|
||||
echo "$(go env GOPATH)/bin" >> "${GITHUB_PATH}"
|
||||
|
||||
- name: Build nFPM packages
|
||||
run: |
|
||||
set -euo pipefail
|
||||
buildscripts/package-release.sh
|
||||
|
||||
- name: Generate package SBOMs and signed checksum manifest
|
||||
env:
|
||||
SYFT_CHECK_FOR_APP_UPDATE: "false"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
packages_dir="dist/packages"
|
||||
mapfile -t packages < <(find "${packages_dir}" -maxdepth 1 -type f \
|
||||
\( -name '*.rpm' -o -name '*.deb' -o -name '*.apk' \) | sort)
|
||||
if [ "${#packages[@]}" -ne 6 ]; then
|
||||
echo "Expected six Linux packages, found ${#packages[@]}" >&2
|
||||
printf '%s\n' "${packages[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for package in "${packages[@]}"; do
|
||||
syft "${package}" --output "spdx-json=${package}.sbom.json"
|
||||
done
|
||||
|
||||
manifest="${packages_dir}/silo_${PKG_VERSION}_packages_checksums.txt"
|
||||
(
|
||||
cd "${packages_dir}"
|
||||
mapfile -t subjects < <(find . -maxdepth 1 -type f \
|
||||
\( -name '*.rpm' -o -name '*.deb' -o -name '*.apk' -o -name '*.sbom.json' \) | sort)
|
||||
if [ "${#subjects[@]}" -ne 12 ]; then
|
||||
echo "Expected six packages and six SBOMs, found ${#subjects[@]} subjects" >&2
|
||||
exit 1
|
||||
fi
|
||||
sha256sum "${subjects[@]}" | sed 's# \./# #' > "$(basename "${manifest}")"
|
||||
)
|
||||
cosign sign-blob --bundle="${manifest}.sigstore.json" "${manifest}" --yes
|
||||
|
||||
- name: Attest downloadable release artifacts
|
||||
id: attest-release
|
||||
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
|
||||
with:
|
||||
subject-path: |
|
||||
dist/*.tar.gz
|
||||
dist/*.zip
|
||||
dist/*.sbom.json
|
||||
dist/*_checksums.txt
|
||||
dist/*.sigstore.json
|
||||
dist/packages/*.rpm
|
||||
dist/packages/*.deb
|
||||
dist/packages/*.apk
|
||||
dist/packages/*.sha256sum
|
||||
dist/packages/*.sbom.json
|
||||
dist/packages/*_checksums.txt
|
||||
dist/packages/*.sigstore.json
|
||||
|
||||
- name: Preserve provenance bundle as a release asset
|
||||
env:
|
||||
BUNDLE_PATH: ${{ steps.attest-release.outputs.bundle-path }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
test -s "${BUNDLE_PATH}"
|
||||
cp "${BUNDLE_PATH}" "dist/silo_${PKG_VERSION}_provenance.sigstore.json"
|
||||
|
||||
- name: Confirm unfinalized Draft release state
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REQUIRE_DRAFT: "true"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
buildscripts/check-release-state.sh "${RELEASE_TAG}"
|
||||
|
||||
- name: Upload nFPM packages to Draft release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mapfile -t files < <(find dist/packages -maxdepth 1 -type f \
|
||||
\( -name '*.rpm' -o -name '*.deb' -o -name '*.apk' -o -name '*.sha256sum' \
|
||||
-o -name '*.sbom.json' -o -name '*_checksums.txt' -o -name '*.sigstore.json' \) | sort)
|
||||
if [ "${#files[@]}" -eq 0 ]; then
|
||||
echo "No packages were generated."
|
||||
exit 1
|
||||
fi
|
||||
gh release upload "${RELEASE_TAG}" "${files[@]}" \
|
||||
"dist/silo_${PKG_VERSION}_provenance.sigstore.json"
|
||||
|
||||
- name: Upload dist artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
@@ -1,508 +0,0 @@
|
||||
name: Test Release Pipeline
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/goreleaser.yml"
|
||||
- ".github/nfpm.yml"
|
||||
- "Dockerfile.goreleaser"
|
||||
- "Dockerfile.distroless"
|
||||
- "cmd/healthcheck-main.go"
|
||||
- "cmd/main.go"
|
||||
- "dockerscripts/download-static-curl.sh"
|
||||
- "dockerscripts/docker-entrypoint.sh"
|
||||
- "dockerscripts/docker-entrypoint_test.sh"
|
||||
- "silo.service"
|
||||
- "silo.env"
|
||||
- "silo.sysusers"
|
||||
- "buildscripts/package-release.sh"
|
||||
- "buildscripts/package/postinstall.sh"
|
||||
- "buildscripts/package/preremove.sh"
|
||||
- "buildscripts/package/lifecycle_test.sh"
|
||||
- "buildscripts/minio-upgrade.sh"
|
||||
- "buildscripts/sign-release-rpms.sh"
|
||||
- "buildscripts/verify-build-provenance.sh"
|
||||
- "buildscripts/check-release-state.sh"
|
||||
- "buildscripts/check-release-state_test.sh"
|
||||
- "buildscripts/verify-rebrand.sh"
|
||||
- "buildscripts/verify-helm-migration.sh"
|
||||
- "buildscripts/helm-migration-guard/**"
|
||||
- "helm/silo/**"
|
||||
- "buildscripts/rebrand-guard/**"
|
||||
- "buildscripts/gen-ldflags.go"
|
||||
- ".github/workflows/release.yml"
|
||||
- ".github/workflows/docker-release.yml"
|
||||
- ".github/workflows/finalize-release.yml"
|
||||
- ".github/workflows/test-release.yml"
|
||||
- ".gitignore"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Compute test variables
|
||||
run: |
|
||||
set -euo pipefail
|
||||
RELEASE_TAG="RELEASE.2026-02-14T12-00-00Z"
|
||||
VERSION_COLON="2026-02-14T12:00:00Z"
|
||||
PKG_VERSION="20260214120000.0.0"
|
||||
LDFLAGS="$(MINIO_RELEASE=RELEASE go run buildscripts/gen-ldflags.go "${VERSION_COLON}")"
|
||||
{
|
||||
echo "RELEASE_TAG=${RELEASE_TAG}"
|
||||
echo "PKG_VERSION=${PKG_VERSION}"
|
||||
echo "LDFLAGS=${LDFLAGS}"
|
||||
} >> "${GITHUB_ENV}"
|
||||
echo "PKG_VERSION: ${PKG_VERSION}"
|
||||
echo "LDFLAGS: ${LDFLAGS}"
|
||||
|
||||
- name: GoReleaser config check
|
||||
uses: goreleaser/goreleaser-action@v7
|
||||
with:
|
||||
version: "~> v2"
|
||||
args: check --config .github/goreleaser.yml
|
||||
|
||||
- name: Validate Helm chart and legacy upgrade identity
|
||||
run: buildscripts/verify-helm-migration.sh
|
||||
|
||||
- name: Install Syft
|
||||
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
||||
with:
|
||||
syft-version: v1.50.0
|
||||
|
||||
- name: Build snapshot artifacts
|
||||
uses: goreleaser/goreleaser-action@v7
|
||||
with:
|
||||
version: "~> v2"
|
||||
# A pull-request snapshot has no trusted release identity. Exercise
|
||||
# the SBOM/checksum pipeline here, and reserve keyless signing for
|
||||
# the tag-triggered release workflow with GitHub OIDC.
|
||||
args: release --snapshot --clean --skip=publish,docker,sign --config .github/goreleaser.yml
|
||||
env:
|
||||
LDFLAGS: ${{ env.LDFLAGS }}
|
||||
PKG_VERSION: ${{ env.PKG_VERSION }}
|
||||
|
||||
- name: Verify archive SBOM and checksum coverage
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mapfile -t archives < <(find dist -maxdepth 1 -type f -name 'silo_*.tar.gz' | sort)
|
||||
mapfile -t sboms < <(find dist -maxdepth 1 -type f -name 'silo_*.tar.gz.sbom.json' | sort)
|
||||
test "${#archives[@]}" -eq 6
|
||||
test "${#sboms[@]}" -eq 6
|
||||
manifest="dist/silo_${PKG_VERSION}_checksums.txt"
|
||||
test -s "${manifest}"
|
||||
(
|
||||
cd dist
|
||||
sha256sum --check "$(basename "${manifest}")"
|
||||
)
|
||||
test "$(wc -l < "${manifest}" | tr -d ' ')" -eq 12
|
||||
|
||||
- name: Verify binary provenance stamps
|
||||
run: |
|
||||
set -euo pipefail
|
||||
buildscripts/verify-build-provenance.sh
|
||||
|
||||
- name: Install package validation tools
|
||||
run: |
|
||||
set -euo pipefail
|
||||
go install github.com/goreleaser/nfpm/v2/cmd/nfpm@v2.47.0
|
||||
echo "$(go env GOPATH)/bin" >> "${GITHUB_PATH}"
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes rpm binutils
|
||||
|
||||
- name: Package snapshot binaries with nFPM
|
||||
run: |
|
||||
set -euo pipefail
|
||||
buildscripts/package-release.sh
|
||||
|
||||
- name: Validate package names, checksums, metadata, and payload
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd dist/packages
|
||||
|
||||
# The signing script asserts these same values, but it runs on the
|
||||
# maintainer's machine after the release workflow has already built
|
||||
# and uploaded. Take its expectations as the single source of truth
|
||||
# so nfpm.yml and the signing script cannot drift apart without
|
||||
# failing here first, while a fix is still cheap.
|
||||
#
|
||||
# This grep is deliberately limited to the eight identity variables,
|
||||
# all of which are single-line. That is what makes the eval safe:
|
||||
# should one ever become multi-line, the grep captures an
|
||||
# unterminated quote and the eval aborts on a syntax error under
|
||||
# set -e rather than quietly binding an empty value and comparing
|
||||
# against nothing. expected_payload is multi-line by design and must
|
||||
# stay out of this set for the same reason.
|
||||
eval "$(grep -E '^expected_(release|vendor|packager|url|summary|description|license|group)=' \
|
||||
../../buildscripts/sign-release-rpms.sh)"
|
||||
for value in "${expected_release}" "${expected_vendor}" "${expected_packager}" \
|
||||
"${expected_url}" "${expected_summary}" "${expected_description}" \
|
||||
"${expected_license}" "${expected_group}"; do
|
||||
test -n "${value}"
|
||||
done
|
||||
|
||||
# These are the public download names; a drift here breaks every
|
||||
# script that fetches packages by URL. RPM and DEB carry the PGSTY
|
||||
# release segment; APK cannot (Alpine pkgrel admits only -r<integer>),
|
||||
# so it stays bare. package-release.sh builds the same three shapes.
|
||||
expected=(
|
||||
"silo-${PKG_VERSION}-${expected_release}.aarch64.rpm"
|
||||
"silo-${PKG_VERSION}-${expected_release}.x86_64.rpm"
|
||||
"silo_${PKG_VERSION}-${expected_release}_amd64.deb"
|
||||
"silo_${PKG_VERSION}-${expected_release}_arm64.deb"
|
||||
"silo_${PKG_VERSION}_aarch64.apk"
|
||||
"silo_${PKG_VERSION}_x86_64.apk"
|
||||
)
|
||||
|
||||
for package in "${expected[@]}"; do
|
||||
test -s "${package}"
|
||||
test -s "${package}.sha256sum"
|
||||
sha256sum --check "${package}.sha256sum"
|
||||
done
|
||||
|
||||
test "$(find . -maxdepth 1 -type f \( -name '*.rpm' -o -name '*.deb' -o -name '*.apk' \) | wc -l)" -eq 6
|
||||
|
||||
service_sha="$(sha256sum ../../silo.service | awk '{print $1}')"
|
||||
defaults_sha="$(sha256sum ../../silo.env | awk '{print $1}')"
|
||||
sysusers_sha="$(sha256sum ../../silo.sysusers | awk '{print $1}')"
|
||||
license_sha="$(sha256sum ../../LICENSE | awk '{print $1}')"
|
||||
notice_sha="$(sha256sum ../../NOTICE | awk '{print $1}')"
|
||||
|
||||
rpm_file="silo-${PKG_VERSION}-${expected_release}.x86_64.rpm"
|
||||
test "$(rpm -qp --queryformat '%{RELEASE}' "${rpm_file}")" = "${expected_release}"
|
||||
test "$(rpm -qp --queryformat '%{VENDOR}' "${rpm_file}")" = "${expected_vendor}"
|
||||
test "$(rpm -qp --queryformat '%{PACKAGER}' "${rpm_file}")" = "${expected_packager}"
|
||||
test "$(rpm -qp --queryformat '%{URL}' "${rpm_file}")" = "${expected_url}"
|
||||
test "$(rpm -qp --queryformat '%{SUMMARY}' "${rpm_file}")" = "${expected_summary}"
|
||||
test "$(rpm -qp --queryformat '%{DESCRIPTION}' "${rpm_file}")" = "${expected_description}"
|
||||
test "$(rpm -qp --queryformat '%{LICENSE}' "${rpm_file}")" = "${expected_license}"
|
||||
test "$(rpm -qp --queryformat '%{GROUP}' "${rpm_file}")" = "${expected_group}"
|
||||
rpm -qpl "${rpm_file}" | grep -Fx '/usr/bin/silo'
|
||||
rpm -qpl "${rpm_file}" | grep -Fx '/usr/lib/systemd/system/silo.service'
|
||||
rpm -qpl "${rpm_file}" | grep -Fx '/etc/default/silo'
|
||||
rpm -qpl "${rpm_file}" | grep -Fx '/usr/lib/sysusers.d/silo.conf'
|
||||
rpm -qpl "${rpm_file}" | grep -Fx '/usr/share/doc/silo/LICENSE'
|
||||
rpm -qpl "${rpm_file}" | grep -Fx '/usr/share/doc/silo/NOTICE'
|
||||
test "$(rpm -qpl "${rpm_file}" | wc -l)" -eq 6
|
||||
# nfpm only honors type: license on rpm, which is why nfpm.yml
|
||||
# declares the license materials once per packager. Pin the rpm
|
||||
# %license flag so that split cannot silently regress.
|
||||
rpm -qp --queryformat '[%{FILEFLAGS:fflags} %{FILENAMES}\n]' "${rpm_file}" \
|
||||
| grep -Fx 'l /usr/share/doc/silo/LICENSE'
|
||||
rpm -qp --queryformat '[%{FILEFLAGS:fflags} %{FILENAMES}\n]' "${rpm_file}" \
|
||||
| grep -Fx 'l /usr/share/doc/silo/NOTICE'
|
||||
if rpm -qp --conflicts "${rpm_file}" | grep -qi minio; then
|
||||
echo "RPM must not declare a cross-name conflict with MinIO" >&2
|
||||
exit 1
|
||||
fi
|
||||
if rpm -qp --obsoletes "${rpm_file}" | grep -qi minio; then
|
||||
echo "RPM must not obsolete a MinIO package" >&2
|
||||
exit 1
|
||||
fi
|
||||
if rpm -qp --provides "${rpm_file}" | grep -qi minio; then
|
||||
echo "RPM must not provide a MinIO package alias" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
deb_file="silo_${PKG_VERSION}-${expected_release}_amd64.deb"
|
||||
test "$(dpkg-deb --field "${deb_file}" Maintainer)" = "${expected_packager}"
|
||||
test "$(dpkg-deb --field "${deb_file}" Version)" = "${PKG_VERSION}-${expected_release}"
|
||||
test "$(dpkg-deb --field "${deb_file}" License)" = "${expected_license}"
|
||||
test "$(dpkg-deb --field "${deb_file}" Section)" = "utils"
|
||||
test "$(dpkg-deb --field "${deb_file}" Homepage)" = "${expected_url}"
|
||||
test "$(dpkg-deb --field "${deb_file}" Description)" = "${expected_description}"
|
||||
dpkg-deb --contents "${deb_file}" | grep -E 'usr/bin/silo$'
|
||||
dpkg-deb --contents "${deb_file}" | grep -E 'usr/lib/systemd/system/silo\.service$'
|
||||
dpkg-deb --contents "${deb_file}" | grep -E 'etc/default/silo$'
|
||||
dpkg-deb --contents "${deb_file}" | grep -E 'usr/lib/sysusers\.d/silo\.conf$'
|
||||
dpkg-deb --contents "${deb_file}" | grep -E 'usr/share/doc/silo/LICENSE$'
|
||||
dpkg-deb --contents "${deb_file}" | grep -E 'usr/share/doc/silo/NOTICE$'
|
||||
test "$(dpkg-deb --contents "${deb_file}" | awk '$1 !~ /^d/ { count++ } END { print count + 0 }')" -eq 6
|
||||
test -z "$(dpkg-deb --field "${deb_file}" Conflicts)"
|
||||
test -z "$(dpkg-deb --field "${deb_file}" Replaces)"
|
||||
test -z "$(dpkg-deb --field "${deb_file}" Provides)"
|
||||
|
||||
apk_info="$(tar -xOzf "silo_${PKG_VERSION}_x86_64.apk" .PKGINFO)"
|
||||
grep -Fx "pkgver = ${PKG_VERSION}" <<< "${apk_info}"
|
||||
grep -Fx "url = ${expected_url}" <<< "${apk_info}"
|
||||
grep -Fx "maintainer = ${expected_packager}" <<< "${apk_info}"
|
||||
grep -Fx "license = ${expected_license}" <<< "${apk_info}"
|
||||
grep -Fx "pkgdesc = ${expected_description}" <<< "${apk_info}"
|
||||
tar -tzf "silo_${PKG_VERSION}_x86_64.apk" | grep -Fx 'usr/bin/silo'
|
||||
tar -tzf "silo_${PKG_VERSION}_x86_64.apk" | grep -Fx 'usr/lib/systemd/system/silo.service'
|
||||
tar -tzf "silo_${PKG_VERSION}_x86_64.apk" | grep -Fx 'etc/default/silo'
|
||||
tar -tzf "silo_${PKG_VERSION}_x86_64.apk" | grep -Fx 'usr/lib/sysusers.d/silo.conf'
|
||||
tar -tzf "silo_${PKG_VERSION}_x86_64.apk" | grep -Fx 'usr/share/doc/silo/LICENSE'
|
||||
tar -tzf "silo_${PKG_VERSION}_x86_64.apk" | grep -Fx 'usr/share/doc/silo/NOTICE'
|
||||
test "$(tar -tzf "silo_${PKG_VERSION}_x86_64.apk" | awk '$0 !~ /^\./ && $0 !~ /\/$/ { count++ } END { print count + 0 }')" -eq 6
|
||||
if grep -Ei '^provides = .*minio' <<< "${apk_info}"; then
|
||||
echo "APK must not provide a MinIO package alias" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for arch in amd64 arm64; do
|
||||
if [ "${arch}" = amd64 ]; then
|
||||
rpm_arch=x86_64
|
||||
deb_arch=amd64
|
||||
apk_arch=x86_64
|
||||
else
|
||||
rpm_arch=aarch64
|
||||
deb_arch=arm64
|
||||
apk_arch=aarch64
|
||||
fi
|
||||
|
||||
test "$(rpm -qp --queryformat '%{ARCH}' "silo-${PKG_VERSION}-${expected_release}.${rpm_arch}.rpm")" = "${rpm_arch}"
|
||||
test "$(dpkg-deb --field "silo_${PKG_VERSION}-${expected_release}_${deb_arch}.deb" Architecture)" = "${deb_arch}"
|
||||
grep -Fx "arch = ${apk_arch}" <<< "$(tar -xOzf "silo_${PKG_VERSION}_${apk_arch}.apk" .PKGINFO)"
|
||||
|
||||
# Accepted weakness: this takes the first match, unsorted, where
|
||||
# find_binary in package-release.sh demands exactly one. It cannot
|
||||
# be reached with an ambiguous match today, because packaging runs
|
||||
# earlier in this same job and hard-fails on one. Revisit if
|
||||
# goamd64 gains a second level, or if find_binary's exactly-one
|
||||
# contract is ever relaxed -- at that point this weak copy would be
|
||||
# the only one left choosing silently.
|
||||
source_binary="$(find .. -maxdepth 2 -type f -path "../silo_linux_${arch}*/silo" | head -n 1)"
|
||||
source_sha="$(sha256sum "${source_binary}" | awk '{print $1}')"
|
||||
|
||||
# Do not pipe rpm2cpio here: Debian's build exits non-zero even when
|
||||
# it writes a correct payload, which trips `set -o pipefail`. Use
|
||||
# rpm's own digests instead -- -K checks the payload against the
|
||||
# header, and FILEDIGESTS is the sha256 rpm itself verifies on
|
||||
# install.
|
||||
rpm -K "silo-${PKG_VERSION}-${expected_release}.${rpm_arch}.rpm"
|
||||
rpm_sha="$(rpm -qp --queryformat '[%{FILENAMES} %{FILEDIGESTS}\n]' \
|
||||
"silo-${PKG_VERSION}-${expected_release}.${rpm_arch}.rpm" | awk '$1 == "/usr/bin/silo" { print $2 }')"
|
||||
rpm_service_sha="$(rpm -qp --queryformat '[%{FILENAMES} %{FILEDIGESTS}\n]' \
|
||||
"silo-${PKG_VERSION}-${expected_release}.${rpm_arch}.rpm" | awk '$1 == "/usr/lib/systemd/system/silo.service" { print $2 }')"
|
||||
rpm_defaults_sha="$(rpm -qp --queryformat '[%{FILENAMES} %{FILEDIGESTS}\n]' \
|
||||
"silo-${PKG_VERSION}-${expected_release}.${rpm_arch}.rpm" | awk '$1 == "/etc/default/silo" { print $2 }')"
|
||||
rpm_sysusers_sha="$(rpm -qp --queryformat '[%{FILENAMES} %{FILEDIGESTS}\n]' \
|
||||
"silo-${PKG_VERSION}-${expected_release}.${rpm_arch}.rpm" | awk '$1 == "/usr/lib/sysusers.d/silo.conf" { print $2 }')"
|
||||
rpm_license_sha="$(rpm -qp --queryformat '[%{FILENAMES} %{FILEDIGESTS}\n]' \
|
||||
"silo-${PKG_VERSION}-${expected_release}.${rpm_arch}.rpm" | awk '$1 == "/usr/share/doc/silo/LICENSE" { print $2 }')"
|
||||
rpm_notice_sha="$(rpm -qp --queryformat '[%{FILENAMES} %{FILEDIGESTS}\n]' \
|
||||
"silo-${PKG_VERSION}-${expected_release}.${rpm_arch}.rpm" | awk '$1 == "/usr/share/doc/silo/NOTICE" { print $2 }')"
|
||||
deb_sha="$(ar p "silo_${PKG_VERSION}-${expected_release}_${deb_arch}.deb" data.tar.gz | tar -xzOf - ./usr/bin/silo | sha256sum | awk '{print $1}')"
|
||||
deb_service_sha="$(ar p "silo_${PKG_VERSION}-${expected_release}_${deb_arch}.deb" data.tar.gz | tar -xzOf - ./usr/lib/systemd/system/silo.service | sha256sum | awk '{print $1}')"
|
||||
deb_defaults_sha="$(ar p "silo_${PKG_VERSION}-${expected_release}_${deb_arch}.deb" data.tar.gz | tar -xzOf - ./etc/default/silo | sha256sum | awk '{print $1}')"
|
||||
deb_sysusers_sha="$(ar p "silo_${PKG_VERSION}-${expected_release}_${deb_arch}.deb" data.tar.gz | tar -xzOf - ./usr/lib/sysusers.d/silo.conf | sha256sum | awk '{print $1}')"
|
||||
deb_license_sha="$(ar p "silo_${PKG_VERSION}-${expected_release}_${deb_arch}.deb" data.tar.gz | tar -xzOf - ./usr/share/doc/silo/LICENSE | sha256sum | awk '{print $1}')"
|
||||
deb_notice_sha="$(ar p "silo_${PKG_VERSION}-${expected_release}_${deb_arch}.deb" data.tar.gz | tar -xzOf - ./usr/share/doc/silo/NOTICE | sha256sum | awk '{print $1}')"
|
||||
apk_sha="$(tar -xzOf "silo_${PKG_VERSION}_${apk_arch}.apk" usr/bin/silo | sha256sum | awk '{print $1}')"
|
||||
apk_service_sha="$(tar -xzOf "silo_${PKG_VERSION}_${apk_arch}.apk" usr/lib/systemd/system/silo.service | sha256sum | awk '{print $1}')"
|
||||
apk_defaults_sha="$(tar -xzOf "silo_${PKG_VERSION}_${apk_arch}.apk" etc/default/silo | sha256sum | awk '{print $1}')"
|
||||
apk_sysusers_sha="$(tar -xzOf "silo_${PKG_VERSION}_${apk_arch}.apk" usr/lib/sysusers.d/silo.conf | sha256sum | awk '{print $1}')"
|
||||
apk_license_sha="$(tar -xzOf "silo_${PKG_VERSION}_${apk_arch}.apk" usr/share/doc/silo/LICENSE | sha256sum | awk '{print $1}')"
|
||||
apk_notice_sha="$(tar -xzOf "silo_${PKG_VERSION}_${apk_arch}.apk" usr/share/doc/silo/NOTICE | sha256sum | awk '{print $1}')"
|
||||
|
||||
test "${source_sha}" = "${rpm_sha}"
|
||||
test "${source_sha}" = "${deb_sha}"
|
||||
test "${source_sha}" = "${apk_sha}"
|
||||
test "${service_sha}" = "${rpm_service_sha}"
|
||||
test "${service_sha}" = "${deb_service_sha}"
|
||||
test "${service_sha}" = "${apk_service_sha}"
|
||||
test "${defaults_sha}" = "${rpm_defaults_sha}"
|
||||
test "${defaults_sha}" = "${deb_defaults_sha}"
|
||||
test "${defaults_sha}" = "${apk_defaults_sha}"
|
||||
test "${sysusers_sha}" = "${rpm_sysusers_sha}"
|
||||
test "${sysusers_sha}" = "${deb_sysusers_sha}"
|
||||
test "${sysusers_sha}" = "${apk_sysusers_sha}"
|
||||
test "${license_sha}" = "${rpm_license_sha}"
|
||||
test "${license_sha}" = "${deb_license_sha}"
|
||||
test "${license_sha}" = "${apk_license_sha}"
|
||||
test "${notice_sha}" = "${rpm_notice_sha}"
|
||||
test "${notice_sha}" = "${deb_notice_sha}"
|
||||
test "${notice_sha}" = "${apk_notice_sha}"
|
||||
done
|
||||
|
||||
find . -maxdepth 1 -type f | sort
|
||||
|
||||
- name: Build release runtime image and verify graceful shutdown
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# docker-release.yml is workflow_dispatch only, so this is the only
|
||||
# automated build of the release runtime layer and entrypoint before a
|
||||
# real publish. Assemble a minimal image from the linux/amd64 binary
|
||||
# goreleaser already produced; the mcli-download build stage is skipped
|
||||
# on purpose to keep this gate offline and deterministic.
|
||||
ctx="$(mktemp -d)"
|
||||
tar -xzf "dist/silo_${PKG_VERSION}_linux_amd64.tar.gz" -C "${ctx}" silo
|
||||
cp dockerscripts/docker-entrypoint.sh "${ctx}/docker-entrypoint.sh"
|
||||
{
|
||||
echo "FROM registry.access.redhat.com/ubi9/ubi-micro:latest"
|
||||
echo "COPY silo /usr/bin/silo"
|
||||
echo "COPY docker-entrypoint.sh /usr/bin/docker-entrypoint.sh"
|
||||
echo "RUN mkdir -p /data && chmod 0777 /data && chmod +x /usr/bin/silo /usr/bin/docker-entrypoint.sh"
|
||||
echo 'ENV HOME=/tmp'
|
||||
echo 'ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]'
|
||||
echo 'CMD ["silo"]'
|
||||
} > "${ctx}/Dockerfile"
|
||||
docker build -t silo-runtime-test:snapshot "${ctx}"
|
||||
|
||||
# PID 1 must be silo, not the entry shell, on every privilege path, so
|
||||
# a SIGTERM from docker stop reaches the server and it exits gracefully
|
||||
# instead of being killed at the stop timeout. Regression guard for the
|
||||
# exec-into-chroot entrypoint fix.
|
||||
assert_graceful() {
|
||||
name="$1"; shift
|
||||
docker rm -f "${name}" >/dev/null 2>&1 || true
|
||||
docker run -d --name "${name}" \
|
||||
-e MINIO_CI_CD=1 -e MINIO_ROOT_USER=ciadmin -e MINIO_ROOT_PASSWORD=ciadmin-secret-123 \
|
||||
"$@" silo-runtime-test:snapshot silo server /data --address :9000 >/dev/null
|
||||
up=""
|
||||
for _ in $(seq 1 60); do
|
||||
if docker logs "${name}" 2>&1 | grep -q "API:"; then up=1; break; fi
|
||||
if [ "$(docker inspect -f '{{.State.Running}}' "${name}")" != "true" ]; then break; fi
|
||||
sleep 1
|
||||
done
|
||||
if [ -z "${up}" ]; then echo "server did not start (${name}):"; docker logs "${name}" | tail -5; exit 1; fi
|
||||
pid1="$(docker exec "${name}" cat /proc/1/comm 2>/dev/null || echo '?')"
|
||||
start="$(date +%s)"; docker stop -t 15 "${name}" >/dev/null; end="$(date +%s)"
|
||||
code="$(docker inspect -f '{{.State.ExitCode}}' "${name}")"
|
||||
elapsed=$((end - start))
|
||||
echo "${name}: pid1=${pid1} stop=${elapsed}s exit=${code}"
|
||||
graceful=0; docker logs "${name}" 2>&1 | grep -q "Exiting on signal" && graceful=1
|
||||
docker rm -f "${name}" >/dev/null 2>&1 || true
|
||||
[ "${graceful}" = "1" ] || { echo "no graceful-shutdown log (${name}) - signal not forwarded"; exit 1; }
|
||||
[ "${code}" = "0" ] || { echo "non-zero exit (${name}): ${code}"; exit 1; }
|
||||
[ "${elapsed}" -lt 10 ] || { echo "shutdown too slow (${name}): ${elapsed}s - signal not forwarded"; exit 1; }
|
||||
}
|
||||
assert_graceful silo-rt-default
|
||||
assert_graceful silo-rt-dropuser -e MINIO_USERNAME=silo-user -e MINIO_GROUPNAME=silo-group
|
||||
assert_graceful silo-rt-rootless --user 1001:1001
|
||||
|
||||
# The compatibility shim translates only the legacy first argv token;
|
||||
# the image contains no /usr/bin/minio file.
|
||||
docker run --rm silo-runtime-test:snapshot sh -c 'test ! -e /usr/bin/minio'
|
||||
docker run --rm -d --name silo-rt-legacy \
|
||||
-e MINIO_CI_CD=1 -e MINIO_ROOT_USER=ciadmin -e MINIO_ROOT_PASSWORD=ciadmin-secret-123 \
|
||||
silo-runtime-test:snapshot minio server /data --address :9000 >/dev/null
|
||||
sleep 2
|
||||
test "$(docker exec silo-rt-legacy cat /proc/1/comm)" = silo
|
||||
docker rm -f silo-rt-legacy >/dev/null
|
||||
|
||||
- name: Build distroless runtime image and verify native healthcheck
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Unlike the classic image, Dockerfile.distroless has no release
|
||||
# download stages, so the real shipped file can be built and gated
|
||||
# here. It must keep working with nothing in it but the silo
|
||||
# binary: no shell, no mc, no entrypoint script.
|
||||
ctx="$(mktemp -d)"
|
||||
tar -xzf "dist/silo_${PKG_VERSION}_linux_amd64.tar.gz" -C "${ctx}" silo
|
||||
cp Dockerfile.distroless LICENSE NOTICE CREDITS "${ctx}/"
|
||||
docker build -t silo-distroless-test:snapshot -f "${ctx}/Dockerfile.distroless" "${ctx}"
|
||||
|
||||
# HEALTHCHECK is a Docker extension absent from the OCI image
|
||||
# spec; assert the exact probe command survived into the image
|
||||
# config, not merely a substring of it.
|
||||
test "$(docker inspect -f '{{json .Config.Healthcheck.Test}}' silo-distroless-test:snapshot)" \
|
||||
= '["CMD","/usr/bin/silo","healthcheck","ready"]'
|
||||
|
||||
# /data ships in the image layer world-writable (issue #55):
|
||||
# there is no entrypoint left to repair ownership at runtime.
|
||||
# Export the rootfs once, then assert each required and each
|
||||
# forbidden entry individually: tar's member-argument mode exits
|
||||
# non-zero on any missing name, which under pipefail masks a
|
||||
# found forbidden file, and -tv prints symlinks as 'name ->
|
||||
# target' which defeats $-anchored greps.
|
||||
probe="$(docker create silo-distroless-test:snapshot server /data)"
|
||||
docker export "${probe}" -o "${ctx}/rootfs.tar"
|
||||
docker rm "${probe}" >/dev/null
|
||||
tar -tf "${ctx}/rootfs.tar" > "${ctx}/names.txt"
|
||||
tar -tvf "${ctx}/rootfs.tar" > "${ctx}/verbose.txt"
|
||||
grep -E '^drwxrwxrwx.* data/$' "${ctx}/verbose.txt" >/dev/null
|
||||
for want in usr/bin/silo licenses/LICENSE licenses/NOTICE licenses/CREDITS; do
|
||||
grep -Fxq "${want}" "${ctx}/names.txt" || { echo "missing ${want}"; exit 1; }
|
||||
done
|
||||
for forbid in bin/sh usr/bin/sh busybox/sh usr/bin/minio usr/bin/mc usr/bin/mcli; do
|
||||
if grep -Fxq "${forbid}" "${ctx}/names.txt"; then
|
||||
echo "distroless image unexpectedly contains ${forbid}"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# The baked-in healthcheck must drive Docker's health state on its
|
||||
# own, the probe binary must be directly exec-able without any
|
||||
# shell, and SIGTERM must still reach PID 1 (the server binary is
|
||||
# the entrypoint) for a graceful stop.
|
||||
assert_distroless() {
|
||||
name="$1"; shift
|
||||
docker rm -f "${name}" >/dev/null 2>&1 || true
|
||||
docker run -d --name "${name}" \
|
||||
-e MINIO_CI_CD=1 -e MINIO_ROOT_USER=ciadmin -e MINIO_ROOT_PASSWORD=ciadmin-secret-123 \
|
||||
"$@" silo-distroless-test:snapshot server /data --address :9000 >/dev/null
|
||||
status=""
|
||||
for _ in $(seq 1 90); do
|
||||
status="$(docker inspect -f '{{.State.Health.Status}}' "${name}" 2>/dev/null || echo '?')"
|
||||
if [ "${status}" = "healthy" ]; then break; fi
|
||||
if [ "$(docker inspect -f '{{.State.Running}}' "${name}")" != "true" ]; then break; fi
|
||||
sleep 1
|
||||
done
|
||||
if [ "${status}" != "healthy" ]; then
|
||||
echo "container never became healthy (${name}): status=${status}"
|
||||
docker logs "${name}" 2>&1 | tail -5
|
||||
exit 1
|
||||
fi
|
||||
docker exec "${name}" /usr/bin/silo healthcheck ready
|
||||
docker exec "${name}" /usr/bin/silo healthcheck cluster
|
||||
start="$(date +%s)"; docker stop -t 15 "${name}" >/dev/null; end="$(date +%s)"
|
||||
code="$(docker inspect -f '{{.State.ExitCode}}' "${name}")"
|
||||
elapsed=$((end - start))
|
||||
graceful=0; docker logs "${name}" 2>&1 | grep -q "Exiting on signal" && graceful=1
|
||||
echo "${name}: health=${status} stop=${elapsed}s exit=${code}"
|
||||
docker rm -f "${name}" >/dev/null 2>&1 || true
|
||||
[ "${graceful}" = "1" ] || { echo "no graceful-shutdown log (${name}) - signal not forwarded"; exit 1; }
|
||||
[ "${code}" = "0" ] || { echo "non-zero exit (${name}): ${code}"; exit 1; }
|
||||
[ "${elapsed}" -lt 10 ] || { echo "shutdown too slow (${name}): ${elapsed}s - signal not forwarded"; exit 1; }
|
||||
}
|
||||
assert_distroless silo-dl-default
|
||||
assert_distroless silo-dl-rootless --user 1001:1001
|
||||
|
||||
- name: Validate release scripts
|
||||
run: |
|
||||
set -euo pipefail
|
||||
bash -n buildscripts/package-release.sh
|
||||
bash -n buildscripts/minio-upgrade.sh
|
||||
bash -n buildscripts/sign-release-rpms.sh
|
||||
bash -n buildscripts/verify-build-provenance.sh
|
||||
bash -n buildscripts/check-release-state.sh
|
||||
bash -n buildscripts/check-release-state_test.sh
|
||||
bash -n buildscripts/verify-rebrand.sh
|
||||
bash -n buildscripts/verify-helm-migration.sh
|
||||
sh -n buildscripts/package/postinstall.sh
|
||||
sh -n buildscripts/package/preremove.sh
|
||||
bash -n buildscripts/package/lifecycle_test.sh
|
||||
buildscripts/package/lifecycle_test.sh
|
||||
bash -n dockerscripts/docker-entrypoint_test.sh
|
||||
bash -n dockerscripts/download-static-curl.sh
|
||||
dockerscripts/docker-entrypoint_test.sh
|
||||
go run ./buildscripts/rebrand-guard
|
||||
buildscripts/verify-rebrand.sh
|
||||
test -x buildscripts/package-release.sh
|
||||
test -x buildscripts/sign-release-rpms.sh
|
||||
test -x buildscripts/verify-build-provenance.sh
|
||||
test -x buildscripts/check-release-state.sh
|
||||
test -x buildscripts/check-release-state_test.sh
|
||||
test -x buildscripts/verify-rebrand.sh
|
||||
test -x buildscripts/verify-helm-migration.sh
|
||||
test -x buildscripts/package/postinstall.sh
|
||||
test -x buildscripts/package/preremove.sh
|
||||
test -x buildscripts/package/lifecycle_test.sh
|
||||
test -x dockerscripts/docker-entrypoint_test.sh
|
||||
buildscripts/check-release-state_test.sh
|
||||
@@ -1,36 +0,0 @@
|
||||
name: VulnCheck
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
vulncheck:
|
||||
name: Analysis
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
|
||||
- name: Install govulncheck
|
||||
run: |
|
||||
go install golang.org/x/vuln/cmd/govulncheck@v1.7.0
|
||||
echo "$(go env GOPATH)/bin" >> "${GITHUB_PATH}"
|
||||
|
||||
- name: Run govulncheck
|
||||
run: govulncheck -show verbose ./...
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
**/*.swp
|
||||
cover.out
|
||||
*~
|
||||
minio
|
||||
silo
|
||||
!*/
|
||||
site/
|
||||
**/*.test
|
||||
**/*.sublime-workspace
|
||||
/.idea/
|
||||
/Minio.iml
|
||||
**/access.log
|
||||
vendor/
|
||||
.DS_Store
|
||||
*.syso
|
||||
coverage.txt
|
||||
.vscode/
|
||||
*.tar.bz2
|
||||
parts/
|
||||
prime/
|
||||
stage/
|
||||
.sia_temp/
|
||||
config.json
|
||||
node_modules/
|
||||
mc.*
|
||||
s3-check-md5*
|
||||
xl-meta*
|
||||
healing-*
|
||||
inspect*.zip
|
||||
200M*
|
||||
hash-set
|
||||
minio.RELEASE*
|
||||
mc
|
||||
nancy
|
||||
inspects/*
|
||||
.bin/
|
||||
*.gz
|
||||
docs/debugging/s3-verify/s3-verify
|
||||
docs/debugging/xl-meta/xl-meta
|
||||
docs/debugging/s3-check-md5/s3-check-md5
|
||||
docs/debugging/hash-set/hash-set
|
||||
docs/debugging/healing-bin/healing-bin
|
||||
docs/debugging/inspect/inspect
|
||||
docs/debugging/pprofgoparser/pprofgoparser
|
||||
docs/debugging/reorder-disks/reorder-disks
|
||||
docs/debugging/populate-hard-links/populate-hardlinks
|
||||
docs/debugging/xattr/xattr
|
||||
hash-set
|
||||
healing-bin
|
||||
inspect
|
||||
pprofgoparser
|
||||
reorder-disks
|
||||
s3-check-md5
|
||||
s3-verify
|
||||
xattr
|
||||
xl-meta
|
||||
|
||||
.gitignore
|
||||
|
||||
dist/
|
||||
|
||||
|
||||
.claude/
|
||||
.codex/
|
||||
AGENTS.md
|
||||
CLAUDE.md
|
||||
_bmad/
|
||||
_bmad-output/
|
||||
docs/security/
|
||||
docs/rebranding.md
|
||||
.release-sign/
|
||||
@@ -1,64 +0,0 @@
|
||||
version: "2"
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- durationcheck
|
||||
- forcetypeassert
|
||||
- gocritic
|
||||
- gomodguard_v2
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- revive
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- unused
|
||||
- usetesting
|
||||
- whitespace
|
||||
settings:
|
||||
misspell:
|
||||
locale: US
|
||||
staticcheck:
|
||||
checks:
|
||||
- all
|
||||
- -SA1008
|
||||
- -SA1019
|
||||
- -SA4000
|
||||
- -SA9004
|
||||
- -ST1000
|
||||
- -ST1005
|
||||
- -ST1016
|
||||
- -U1000
|
||||
exclusions:
|
||||
generated: lax
|
||||
rules:
|
||||
- linters:
|
||||
- forcetypeassert
|
||||
path: _test\.go
|
||||
- path: (.+)\.go$
|
||||
text: 'empty-block:'
|
||||
- path: (.+)\.go$
|
||||
text: 'unused-parameter:'
|
||||
- path: (.+)\.go$
|
||||
text: 'dot-imports:'
|
||||
- path: (.+)\.go$
|
||||
text: should have a package comment
|
||||
- path: (.+)\.go$
|
||||
text: error strings should not be capitalized or end with punctuation or a newline
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
issues:
|
||||
max-issues-per-linter: 100
|
||||
max-same-issues: 100
|
||||
formatters:
|
||||
enable:
|
||||
- gofumpt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generate CONTRIBUTORS.md: contributors.sh
|
||||
|
||||
# Tip for finding duplicates (besides scanning the output of CONTRIBUTORS.md for name
|
||||
# duplicates that aren't also email duplicates): scan the output of:
|
||||
# git log --format='%aE - %aN' | sort -uf
|
||||
#
|
||||
# For explanation on this file format: man git-shortlog
|
||||
|
||||
Anand Babu (AB) Periasamy <ab@min.io> Anand Babu (AB) Periasamy <abperiasamy@users.noreply.github.com>
|
||||
Anand Babu (AB) Periasamy <ab@min.io> <ab@unlocksmith.org>
|
||||
Anis Elleuch <vadmeste@gmail.com>
|
||||
Frederick F. Kautz IV <fkautz@min.io> <fkautz@alumni.cmu.edu>
|
||||
Harshavardhana <harsha@min.io> <harsha@harshavardhana.net>
|
||||
Harshavardhana <harsha@min.io> <badger@gitter.im>
|
||||
Harshavardhana <harsha@min.io>
|
||||
Krishna Srinivas <krishna@min.io> <krishna.srinivas@gmail.com>
|
||||
Matthew Farrellee <matt@cs.wisc.edu>
|
||||
Nate Rosenblum <flander@gmail.com>
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
[files]
|
||||
extend-exclude = [".git/", "docs/", "CREDITS", "go.mod", "go.sum"]
|
||||
ignore-hidden = false
|
||||
|
||||
[default]
|
||||
extend-ignore-re = [
|
||||
"Patrick Collison",
|
||||
"Copyright 2014 Unknwon",
|
||||
"[0-9A-Za-z/+=]{64}",
|
||||
"ZXJuZXQxDjAMBgNVBA-some-junk-Q4wDAYDVQQLEwVNaW5pbzEOMAwGA1UEAxMF",
|
||||
"eyJmb28iOiJiYXIifQ",
|
||||
"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.*",
|
||||
"MIIDBTCCAe2gAwIBAgIQWHw7h.*",
|
||||
'http\.Header\{"X-Amz-Server-Side-Encryptio":',
|
||||
"ZoEoZdLlzVbOlT9rbhD7ZN7TLyiYXSAlB79uGEge",
|
||||
"ERRO:",
|
||||
"(?Rm)^.*(#|//)\\s*spellchecker:disable-line$", # ignore line
|
||||
]
|
||||
|
||||
[default.extend-words]
|
||||
"encrypter" = "encrypter"
|
||||
"kms" = "kms"
|
||||
"requestor" = "requestor"
|
||||
|
||||
[default.extend-identifiers]
|
||||
"HashiCorp" = "HashiCorp"
|
||||
|
||||
[type.go.extend-identifiers]
|
||||
"bui" = "bui"
|
||||
"dm2nd" = "dm2nd"
|
||||
"ot" = "ot"
|
||||
"ParseND" = "ParseND"
|
||||
"ParseNDStream" = "ParseNDStream"
|
||||
"pn" = "pn"
|
||||
"TestGetPartialObjectMisAligned" = "TestGetPartialObjectMisAligned"
|
||||
"thr" = "thr"
|
||||
"toi" = "toi"
|
||||
|
||||
[type.go]
|
||||
extend-ignore-identifiers-re = [
|
||||
# Variants of `typ` used to mean `type` in golang as it is otherwise a
|
||||
# keyword - some of these (like typ1 -> type1) can be fixed, but probably
|
||||
# not worth the effort.
|
||||
"[tT]yp[0-9]*",
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
# AGPLv3 Compliance
|
||||
|
||||
Silo is distributed under the [GNU Affero General Public License v3.0](LICENSE).
|
||||
It incorporates source code from the MinIO project and preserves the original
|
||||
copyright, license, and attribution notices in [`NOTICE`](NOTICE),
|
||||
[`CREDITS`](CREDITS), and source-file headers.
|
||||
|
||||
You are responsible for determining how the AGPLv3 applies to your use,
|
||||
modification, deployment, and distribution of Silo and its dependencies. The
|
||||
Silo maintainers cannot provide legal advice or determine whether a particular
|
||||
application or service satisfies the license. Consult qualified counsel when
|
||||
the obligations are material to your deployment.
|
||||
|
||||
If you convey modified binaries or provide network access to a modified
|
||||
version, review the complete AGPLv3 text and ensure that the corresponding
|
||||
source and notices are made available as required. Dependency licenses and
|
||||
separate notices continue to apply independently.
|
||||
-157
@@ -1,157 +0,0 @@
|
||||
# Contributing to Silo
|
||||
|
||||
Silo welcomes focused contributions that improve security, reliability,
|
||||
compatibility, packaging, tests, or maintainability. This repository preserves
|
||||
MinIO-compatible interfaces and storage formats, so changes must identify and
|
||||
test any compatibility impact.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
Fork the current Silo source repository, create a topic branch, and submit a
|
||||
pull request. Discuss broad or compatibility-sensitive changes in an issue
|
||||
before implementation.
|
||||
|
||||
### Set up a checkout
|
||||
|
||||
```sh
|
||||
git clone https://github.com/pgsty/silo
|
||||
cd silo
|
||||
go build -o silo .
|
||||
./silo --version
|
||||
```
|
||||
|
||||
### Keep the lineage remote separate
|
||||
|
||||
```sh
|
||||
git remote add lineage https://github.com/minio/minio
|
||||
git fetch lineage
|
||||
```
|
||||
|
||||
Do not merge an upstream branch into a pull request unless the maintainers have
|
||||
agreed on the scope. Silo intentionally carries a small downstream delta.
|
||||
|
||||
### Create your feature branch
|
||||
|
||||
Create a separate branch before making code changes:
|
||||
|
||||
```
|
||||
git checkout -b my-new-feature
|
||||
```
|
||||
|
||||
### Test Silo server changes
|
||||
|
||||
Before opening a pull request:
|
||||
|
||||
- Add or update tests for changed behavior.
|
||||
- Run `make verifiers`.
|
||||
- If `make rebrand-guard` reports a changed compatibility set, review the
|
||||
listed identifiers; when the change is intended, refresh the baseline with
|
||||
`go run ./buildscripts/rebrand-guard --write` and commit
|
||||
`buildscripts/rebrand-guard/compat-baseline.json`.
|
||||
- Run the smallest relevant package tests, then `make test` when practical.
|
||||
- Run `make build` and confirm the generated executable is `silo`.
|
||||
- Explain any preserved `MINIO_*`, `minio_*`, `x-minio-*`, `/minio/*`,
|
||||
`.minio.sys`, ARN, module/import-path, or serialized compatibility name.
|
||||
|
||||
### Commit changes
|
||||
|
||||
After verification, commit your changes with a concise message and a DCO
|
||||
sign-off (see [Licensing of Contributions](#licensing-of-contributions)):
|
||||
|
||||
```
|
||||
git commit -s -am 'Fix object replication retry handling'
|
||||
```
|
||||
|
||||
### Push to the branch
|
||||
|
||||
Push your locally committed changes to the remote origin (your fork)
|
||||
|
||||
```
|
||||
git push origin my-new-feature
|
||||
```
|
||||
|
||||
### Create a Pull Request
|
||||
|
||||
Pull requests should include motivation, reproduction steps where applicable,
|
||||
test evidence, compatibility notes, and documentation impact. Public product
|
||||
documentation is owned by the separate
|
||||
[`pgsty/silo.pgsty.com`](https://github.com/pgsty/silo.pgsty.com) repository.
|
||||
|
||||
## Licensing of Contributions
|
||||
|
||||
Code contributions to PGSTY SILO (`pgsty/silo`) are accepted under the
|
||||
[GNU AGPL v3.0 or later](LICENSE), the same license as the server. Submit issues
|
||||
and pull requests to this repository's maintainers. No separate Apache-2.0
|
||||
license grant to SILO or upstream MinIO maintainers is required.
|
||||
|
||||
* **No CLA.** We do not ask you to sign a Contributor License Agreement and we
|
||||
do not take your copyright. Contributions are accepted inbound=outbound: you
|
||||
keep the copyright to your changes and license them under the same
|
||||
AGPL-3.0-or-later as the project itself. The maintainers receive no rights
|
||||
beyond the project license.
|
||||
|
||||
* **DCO sign-off required.** Every commit must carry a
|
||||
`Signed-off-by: Your Name <you@example.com>` trailer certifying the
|
||||
[Developer Certificate of Origin 1.1](https://developercertificate.org/) —
|
||||
your statement that you have the right to submit the code under the project
|
||||
license. Sign each commit with:
|
||||
|
||||
```
|
||||
git commit -s
|
||||
```
|
||||
|
||||
Forgot some? Repair your branch with `git rebase --signoff` and force-push.
|
||||
CI rejects pull requests containing unsigned commits; the sign-off email
|
||||
must match the commit author email. (Lowercase `-s` is the plain-text DCO
|
||||
sign-off; cryptographic `-S`/GPG signing is welcome but independent.)
|
||||
|
||||
* **Provenance.** Only submit code you are entitled to submit. This matters
|
||||
more here than in most projects: Silo carries a downstream delta over an
|
||||
upstream code base, and cherry-picks from the lineage remote or other forks
|
||||
are routine. When relaying a patch written by someone else, preserve original
|
||||
authorship (`git cherry-pick -x`, keep the author field and any existing
|
||||
`Signed-off-by` trailers) and add your own sign-off as the person passing it
|
||||
along. Never import code from a proprietary distribution.
|
||||
|
||||
* **File headers.** Preserve existing copyright and license notices in inherited
|
||||
and third-party files. New original files name their actual copyright holders
|
||||
and use AGPL-3.0-or-later. Use a header such as the following, then append the
|
||||
standard AGPL boilerplate:
|
||||
|
||||
```
|
||||
// Copyright (c) 2026 Your Name
|
||||
```
|
||||
|
||||
* **Separately licensed material.** Documentation contributions in `docs/`
|
||||
follow its existing [CC BY 4.0 license](docs/LICENSE). Third-party components
|
||||
and earlier Apache-2.0 contributions retain their original licenses and
|
||||
attribution; this policy does not relicense earlier work.
|
||||
|
||||
* **Squash merges** must keep the `Signed-off-by:` trailers in the resulting
|
||||
commit message.
|
||||
|
||||
* **Authorship and tooling.** The human contributor is the author of the commit
|
||||
and the sole signatory of its DCO sign-off. Attribution trailers for
|
||||
assistive tooling (for example `Co-Authored-By:` naming an AI assistant) are
|
||||
informational only: they record which tools were used, and do not create
|
||||
authorship, co-authorship, or any copyright claim. Whoever signs off remains
|
||||
responsible for the content of the commit, whatever produced it.
|
||||
|
||||
## FAQs
|
||||
|
||||
### How does Silo manage dependencies?
|
||||
|
||||
Silo uses Go modules. Preserve the compatibility module and import paths in
|
||||
`go.mod`; downstream forks are selected with explicit `replace` directives.
|
||||
|
||||
- Run `go get foo/bar` in the source folder to add the dependency to `go.mod` file.
|
||||
|
||||
To remove a dependency
|
||||
|
||||
- Edit your code and remove the import reference.
|
||||
- Run `go mod tidy` in the source folder to remove dependency from `go.mod` file.
|
||||
|
||||
### What are the coding guidelines?
|
||||
|
||||
Follow the existing Go style, run `gofmt` on changed Go files, and keep changes
|
||||
compact. See the Go project's [code review comments](https://go.dev/wiki/CodeReviewComments).
|
||||
-184
File diff suppressed because one or more lines are too long
@@ -1,50 +0,0 @@
|
||||
# The distroless variant ships exactly one program: the silo binary.
|
||||
# No shell, no mc, no curl, no entrypoint script; health checking is
|
||||
# provided by the binary itself (`silo healthcheck`).
|
||||
# Design note: https://silo.pgsty.com/compatibility/feature/healthcheck/
|
||||
|
||||
# A distroless final stage cannot RUN anything, so /data is prepared in a
|
||||
# throwaway stage. It ships world-writable (see pgsty/silo#55): Docker
|
||||
# seeds fresh volumes from the image-layer mountpoint, no entrypoint
|
||||
# exists to repair ownership at runtime, and 0777 is what keeps every
|
||||
# privilege mode working, --user included.
|
||||
FROM busybox:1.37.0 AS prep
|
||||
RUN mkdir -p /prep/data && chmod 0777 /prep/data
|
||||
|
||||
FROM gcr.io/distroless/static-debian12:latest
|
||||
|
||||
LABEL org.opencontainers.image.title="Silo" \
|
||||
org.opencontainers.image.description="S3-Interface Libre Object Storage (distroless)" \
|
||||
org.opencontainers.image.url="https://silo.pgsty.com" \
|
||||
org.opencontainers.image.source="https://github.com/pgsty/silo" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0-or-later" \
|
||||
maintainer="PGSTY <https://silo.pgsty.com>"
|
||||
|
||||
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||
MINIO_SECRET_KEY_FILE=secret_key \
|
||||
MINIO_ROOT_USER_FILE=access_key \
|
||||
MINIO_ROOT_PASSWORD_FILE=secret_key \
|
||||
MINIO_KMS_SECRET_KEY_FILE=kms_master_key \
|
||||
MINIO_CONFIG_ENV_FILE=config.env \
|
||||
HOME=/tmp
|
||||
|
||||
COPY --chmod=0755 silo /usr/bin/silo
|
||||
# COPY of a directory copies its contents, not the directory entry, so an
|
||||
# empty /prep/data would arrive as a default root:0755 /data and non-root
|
||||
# runs would fail storage init. Copying the parent makes data/ itself a
|
||||
# copied entry, which --chmod then actually applies to.
|
||||
COPY --from=prep --chmod=0777 /prep/ /
|
||||
COPY LICENSE NOTICE CREDITS /licenses/
|
||||
|
||||
EXPOSE 9000
|
||||
VOLUME ["/data"]
|
||||
|
||||
# Exec form is mandatory: there is no /bin/sh in this image. `ready`
|
||||
# rather than `live` because Docker health feeds start-order gating
|
||||
# (readiness semantics); the two are identical unless KMS/etcd are used.
|
||||
# The outer timeout stays above the probe's own 5s deadline so the
|
||||
# probe can report its diagnostic line instead of being SIGKILLed.
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=2m --start-interval=2s --retries=3 \
|
||||
CMD ["/usr/bin/silo", "healthcheck", "ready"]
|
||||
|
||||
ENTRYPOINT ["/usr/bin/silo"]
|
||||
@@ -1,104 +0,0 @@
|
||||
FROM golang:1.27.1-alpine AS build
|
||||
|
||||
ARG TARGETARCH
|
||||
|
||||
ENV GOPATH=/go
|
||||
ENV CGO_ENABLED=0
|
||||
|
||||
ARG MC_REPO=pgsty/mc
|
||||
ARG MC_VERSION=RELEASE.2026-09-03T07-13-05Z
|
||||
ARG MC_AMD64_SHA256=cd7fcd449bb6b52e2eb727431ba6975b1e5d90df011a75869020ea9ac9e2b2a8
|
||||
ARG MC_ARM64_SHA256=7962afc37c3e60e5758b19e819cb62d2f340ee655fad7b067e2ac9bc5716c2e8
|
||||
|
||||
RUN apk add -U --no-cache \
|
||||
ca-certificates \
|
||||
bash \
|
||||
curl \
|
||||
jq && \
|
||||
case "${TARGETARCH}" in \
|
||||
amd64) MC_ARCH=amd64; MC_PINNED_SHA256="${MC_AMD64_SHA256}" ;; \
|
||||
arm64) MC_ARCH=arm64; MC_PINNED_SHA256="${MC_ARM64_SHA256}" ;; \
|
||||
*) echo "Unsupported TARGETARCH=${TARGETARCH}"; exit 1 ;; \
|
||||
esac && \
|
||||
if [ "${MC_VERSION}" = "latest" ]; then \
|
||||
MC_RELEASE_URL="https://api.github.com/repos/${MC_REPO}/releases/latest"; \
|
||||
else \
|
||||
MC_RELEASE_URL="https://api.github.com/repos/${MC_REPO}/releases/tags/${MC_VERSION}"; \
|
||||
fi && \
|
||||
curl -fsSL "${MC_RELEASE_URL}" -o /tmp/mc-release.json && \
|
||||
MC_ARCHIVE_URL=$(jq -r --arg arch "${MC_ARCH}" \
|
||||
'.assets[] | select(.name | endswith("_linux_" + $arch + ".tar.gz")) | .browser_download_url' \
|
||||
/tmp/mc-release.json | head -n 1) && \
|
||||
MC_CHECKSUM_URL=$(jq -r \
|
||||
'.assets[] | select(.name | endswith("_checksums.txt")) | .browser_download_url' \
|
||||
/tmp/mc-release.json | head -n 1) && \
|
||||
[ -n "${MC_ARCHIVE_URL}" ] || { echo "Cannot find mcli archive for linux/${MC_ARCH}"; exit 1; } && \
|
||||
[ -n "${MC_CHECKSUM_URL}" ] || { echo "Cannot find mcli checksums file"; exit 1; } && \
|
||||
ARCHIVE_NAME=$(basename "${MC_ARCHIVE_URL}") && \
|
||||
echo "Downloading ${ARCHIVE_NAME} ..." && \
|
||||
curl -fsSL "${MC_ARCHIVE_URL}" -o /tmp/mcli.tar.gz && \
|
||||
curl -fsSL "${MC_CHECKSUM_URL}" -o /tmp/mcli_checksums.txt && \
|
||||
EXPECTED=$(grep " ${ARCHIVE_NAME}$" /tmp/mcli_checksums.txt | awk '{print $1}') && \
|
||||
ACTUAL=$(sha256sum /tmp/mcli.tar.gz | awk '{print $1}') && \
|
||||
[ -n "${EXPECTED}" ] || { echo "Checksum entry not found for ${ARCHIVE_NAME}"; exit 1; } && \
|
||||
[ "${EXPECTED}" = "${MC_PINNED_SHA256}" ] || { echo "Published checksum drift for ${ARCHIVE_NAME}"; exit 1; } && \
|
||||
[ "${MC_PINNED_SHA256}" = "${ACTUAL}" ] || { echo "Checksum mismatch: expected ${MC_PINNED_SHA256}, got ${ACTUAL}"; exit 1; } && \
|
||||
echo "Checksum OK: ${ACTUAL}" && \
|
||||
mkdir -p /tmp/mcli-extract && \
|
||||
tar -xzf /tmp/mcli.tar.gz -C /tmp/mcli-extract/ && \
|
||||
if [ -f /tmp/mcli-extract/mcli ]; then \
|
||||
cp /tmp/mcli-extract/mcli /go/bin/mcli; \
|
||||
elif [ -f /tmp/mcli-extract/mc ]; then \
|
||||
cp /tmp/mcli-extract/mc /go/bin/mcli; \
|
||||
else \
|
||||
echo "No mc or mcli binary found in archive:"; ls -la /tmp/mcli-extract/; exit 1; \
|
||||
fi && \
|
||||
chmod +x /go/bin/mcli && \
|
||||
ln -sf mcli /go/bin/mc
|
||||
|
||||
COPY dockerscripts/download-static-curl.sh /build/download-static-curl
|
||||
RUN chmod +x /build/download-static-curl && \
|
||||
/build/download-static-curl
|
||||
|
||||
FROM registry.access.redhat.com/ubi9/ubi:latest AS certs
|
||||
RUN dnf -y install ca-certificates && \
|
||||
update-ca-trust && \
|
||||
cp /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem /tmp/ca-certificates.crt && \
|
||||
dnf clean all && \
|
||||
rm -rf /var/cache/dnf
|
||||
|
||||
FROM registry.access.redhat.com/ubi9/ubi-micro:latest
|
||||
|
||||
LABEL org.opencontainers.image.title="Silo" \
|
||||
org.opencontainers.image.description="S3-Interface Libre Object Storage" \
|
||||
org.opencontainers.image.url="https://silo.pgsty.com" \
|
||||
org.opencontainers.image.source="https://github.com/pgsty/silo" \
|
||||
org.opencontainers.image.licenses="AGPL-3.0-or-later" \
|
||||
maintainer="PGSTY <https://silo.pgsty.com>"
|
||||
|
||||
ENV MINIO_ACCESS_KEY_FILE=access_key \
|
||||
MINIO_SECRET_KEY_FILE=secret_key \
|
||||
MINIO_ROOT_USER_FILE=access_key \
|
||||
MINIO_ROOT_PASSWORD_FILE=secret_key \
|
||||
MINIO_KMS_SECRET_KEY_FILE=kms_master_key \
|
||||
MINIO_CONFIG_ENV_FILE=config.env \
|
||||
HOME=/tmp \
|
||||
MC_CONFIG_DIR=/tmp/.mc
|
||||
|
||||
COPY --from=certs /tmp/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY silo /usr/bin/silo
|
||||
COPY --from=build /go/bin/mcli /usr/bin/mcli
|
||||
COPY --from=build /go/bin/curl* /usr/bin/
|
||||
COPY dockerscripts/docker-entrypoint.sh /usr/bin/docker-entrypoint.sh
|
||||
COPY LICENSE /licenses/LICENSE
|
||||
COPY NOTICE /licenses/NOTICE
|
||||
COPY CREDITS /licenses/CREDITS
|
||||
|
||||
RUN chmod +x /usr/bin/silo /usr/bin/mcli /usr/bin/docker-entrypoint.sh && \
|
||||
ln -sf mcli /usr/bin/mc
|
||||
|
||||
EXPOSE 9000
|
||||
VOLUME ["/data"]
|
||||
|
||||
ENTRYPOINT ["/usr/bin/docker-entrypoint.sh"]
|
||||
CMD ["silo"]
|
||||
@@ -1,661 +0,0 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
@@ -1,260 +0,0 @@
|
||||
PWD := $(shell pwd)
|
||||
GOPATH := $(shell go env GOPATH)
|
||||
LDFLAGS := $(shell go run buildscripts/gen-ldflags.go)
|
||||
|
||||
GOOS ?= $(shell go env GOOS)
|
||||
GOARCH ?= $(shell go env GOARCH)
|
||||
GOLANGCI_VERSION ?= v2.13.1
|
||||
|
||||
VERSION ?= $(shell git describe --tags)
|
||||
REPO ?= docker.io/pgsty
|
||||
TAG ?= $(REPO)/silo:$(VERSION)
|
||||
|
||||
GOLANGCI_DIR = .bin/golangci/$(GOLANGCI_VERSION)
|
||||
GOLANGCI = $(GOLANGCI_DIR)/golangci-lint
|
||||
|
||||
all: build
|
||||
|
||||
checks: ## check dependencies
|
||||
@echo "Checking dependencies"
|
||||
@(env bash $(PWD)/buildscripts/checkdeps.sh)
|
||||
|
||||
help: ## print this help
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' Makefile | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-40s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
getdeps: ## fetch necessary dependencies
|
||||
@mkdir -p ${GOPATH}/bin
|
||||
@if [ ! -x "$(GOLANGCI)" ]; then \
|
||||
set -e; \
|
||||
echo "Installing golangci-lint $(GOLANGCI_VERSION)"; \
|
||||
script=$$(mktemp); \
|
||||
trap 'rm -f "$$script"' EXIT; \
|
||||
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_VERSION)/install.sh -o "$$script"; \
|
||||
sh "$$script" -b $(GOLANGCI_DIR) $(GOLANGCI_VERSION); \
|
||||
fi
|
||||
|
||||
crosscompile: ## cross compile Silo
|
||||
@(env bash $(PWD)/buildscripts/cross-compile.sh)
|
||||
|
||||
verifiers: lint check-gen rebrand-guard
|
||||
|
||||
rebrand-guard: ## verify Silo branding and protected compatibility identifiers
|
||||
@go run ./buildscripts/rebrand-guard
|
||||
@env bash $(PWD)/buildscripts/verify-rebrand.sh
|
||||
@env bash $(PWD)/dockerscripts/docker-entrypoint_test.sh
|
||||
|
||||
credits: ## regenerate CREDITS from the licenses of Go modules linked into the binary
|
||||
@env bash $(PWD)/buildscripts/gen-credits.sh
|
||||
|
||||
check-gen: ## check for updated autogenerated files
|
||||
@go generate ./... >/dev/null
|
||||
@go mod tidy -compat=1.27
|
||||
@env bash $(PWD)/buildscripts/gen-credits.sh
|
||||
@changed=$$(git diff --name-only -- '*_gen.go' '*_gen_test.go' '*_msgp_test.go' '*_string.go' go.mod go.sum CREDITS); \
|
||||
if [ -n "$$changed" ]; then \
|
||||
echo "Non-committed generated changes detected:"; \
|
||||
echo "$$changed"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@untracked=$$(git ls-files --others --exclude-standard -- '*_gen.go' '*_gen_test.go' '*_msgp_test.go' '*_string.go'); \
|
||||
if [ -n "$$untracked" ]; then \
|
||||
echo "Untracked generated files detected:"; \
|
||||
echo "$$untracked"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
lint: getdeps ## runs golangci-lint suite of linters
|
||||
@echo "Running $@ check"
|
||||
@$(GOLANGCI) run --build-tags kqueue --timeout=10m --config ./.golangci.yml
|
||||
@if command -v typos >/dev/null 2>&1; then typos ./; else echo "typos binary is not found.. skipping.."; fi
|
||||
|
||||
lint-fix: getdeps ## runs golangci-lint suite of linters with automatic fixes
|
||||
@echo "Running $@ check"
|
||||
@$(GOLANGCI) run --build-tags kqueue --timeout=10m --config ./.golangci.yml --fix
|
||||
|
||||
check: test
|
||||
test: verifiers build ## builds Silo, runs linters, tests
|
||||
@echo "Running unit tests"
|
||||
@MINIO_API_REQUESTS_MAX=10000 CGO_ENABLED=0 go test -v -tags kqueue,dev ./...
|
||||
|
||||
test-root-disable: install-race
|
||||
@echo "Running Silo root lockdown tests"
|
||||
@env bash $(PWD)/buildscripts/disable-root.sh
|
||||
|
||||
test-ilm: install-race
|
||||
@echo "Running ILM tests"
|
||||
@env bash $(PWD)/docs/bucket/replication/setup_ilm_expiry_replication.sh
|
||||
|
||||
test-ilm-transition: install-race
|
||||
@echo "Running ILM tiering tests with healing"
|
||||
@env bash $(PWD)/docs/bucket/lifecycle/setup_ilm_transition.sh
|
||||
|
||||
test-pbac: install-race
|
||||
@echo "Running bucket policies tests"
|
||||
@env bash $(PWD)/docs/iam/policies/pbac-tests.sh
|
||||
|
||||
test-decom: install-race
|
||||
@echo "Running Silo decom tests"
|
||||
@env bash $(PWD)/docs/distributed/decom.sh
|
||||
@env bash $(PWD)/docs/distributed/decom-encrypted.sh
|
||||
@env bash $(PWD)/docs/distributed/decom-encrypted-sse-s3.sh
|
||||
@env bash $(PWD)/docs/distributed/decom-compressed-sse-s3.sh
|
||||
@env bash $(PWD)/docs/distributed/decom-encrypted-kes.sh
|
||||
|
||||
test-versioning: install-race
|
||||
@echo "Running Silo versioning tests"
|
||||
@env bash $(PWD)/docs/bucket/versioning/versioning-tests.sh
|
||||
|
||||
test-configfile: install-race
|
||||
@env bash $(PWD)/docs/distributed/distributed-from-config-file.sh
|
||||
|
||||
test-upgrade:
|
||||
@echo "Running MinIO-to-Silo upgrade tests"
|
||||
@(env bash $(PWD)/buildscripts/minio-upgrade.sh)
|
||||
|
||||
test-race: verifiers build ## builds Silo, runs linters, tests (race)
|
||||
@echo "Running unit tests under -race"
|
||||
@(env bash $(PWD)/buildscripts/race.sh)
|
||||
|
||||
test-iam: install-race ## verify IAM (external IDP, etcd backends)
|
||||
@echo "Running tests for IAM (external IDP, etcd backends)"
|
||||
@MINIO_API_REQUESTS_MAX=10000 CGO_ENABLED=0 go test -timeout 15m -tags kqueue,dev -v -run TestIAM* ./cmd
|
||||
@echo "Running tests for IAM (external IDP, etcd backends) with -race"
|
||||
@MINIO_API_REQUESTS_MAX=10000 GORACE=history_size=7 CGO_ENABLED=1 go test -timeout 15m -race -tags kqueue,dev -v -run TestIAM* ./cmd
|
||||
|
||||
test-iam-ldap-upgrade-import: install-race ## verify IAM (external LDAP IDP)
|
||||
@echo "Running upgrade tests for IAM (LDAP backend)"
|
||||
@env bash $(PWD)/buildscripts/minio-iam-ldap-upgrade-import-test.sh
|
||||
|
||||
test-iam-import-with-missing-entities: install-race ## test import of external iam config withg missing entities
|
||||
@echo "Test IAM import configurations with missing entities"
|
||||
@env bash $(PWD)/docs/distributed/iam-import-with-missing-entities.sh
|
||||
|
||||
test-iam-import-with-openid: install-race
|
||||
@echo "Test IAM import configurations with openid"
|
||||
@env bash $(PWD)/docs/distributed/iam-import-with-openid.sh
|
||||
|
||||
test-sio-error:
|
||||
@(env bash $(PWD)/docs/bucket/replication/sio-error.sh)
|
||||
|
||||
test-replication-2site:
|
||||
@(env bash $(PWD)/docs/bucket/replication/setup_2site_existing_replication.sh)
|
||||
|
||||
test-replication-3site:
|
||||
@(env bash $(PWD)/docs/bucket/replication/setup_3site_replication.sh)
|
||||
|
||||
test-delete-replication:
|
||||
@(env bash $(PWD)/docs/bucket/replication/delete-replication.sh)
|
||||
|
||||
test-delete-marker-proxying:
|
||||
@(env bash $(PWD)/docs/bucket/replication/test_del_marker_proxying.sh)
|
||||
|
||||
test-replication: install-race test-replication-2site test-replication-3site test-delete-replication test-sio-error test-delete-marker-proxying ## verify multi site replication
|
||||
@echo "Running tests for replicating three sites"
|
||||
|
||||
test-site-replication-ldap: install-race ## verify automatic site replication
|
||||
@echo "Running tests for automatic site replication of IAM (with LDAP)"
|
||||
@(env bash $(PWD)/docs/site-replication/run-multi-site-ldap.sh)
|
||||
|
||||
test-site-replication-oidc: install-race ## verify automatic site replication
|
||||
@echo "Running tests for automatic site replication of IAM (with OIDC)"
|
||||
@(env bash $(PWD)/docs/site-replication/run-multi-site-oidc.sh)
|
||||
|
||||
test-site-replication-silo: install-race ## verify automatic site replication
|
||||
@echo "Running tests for automatic site replication of IAM (with Silo IDP)"
|
||||
@(env bash $(PWD)/docs/site-replication/run-multi-site-silo-idp.sh)
|
||||
@echo "Running tests for automatic site replication of SSE-C objects"
|
||||
@(env bash $(PWD)/docs/site-replication/run-ssec-object-replication.sh)
|
||||
@echo "Running tests for automatic site replication of SSE-C objects with SSE-KMS enabled for bucket"
|
||||
@(env bash $(PWD)/docs/site-replication/run-sse-kms-object-replication.sh)
|
||||
@echo "Running tests for automatic site replication of SSE-C objects with compression enabled for site"
|
||||
@(env bash $(PWD)/docs/site-replication/run-ssec-object-replication-with-compression.sh)
|
||||
|
||||
test-multipart: install-race ## test multipart
|
||||
@echo "Test multipart behavior when part files are missing"
|
||||
@(env bash $(PWD)/buildscripts/multipart-quorum-test.sh)
|
||||
|
||||
test-timeout: install-race ## test multipart
|
||||
@echo "Test server timeout"
|
||||
@(env bash $(PWD)/buildscripts/test-timeout.sh)
|
||||
|
||||
verify: install-race ## verify Silo in various setups
|
||||
@echo "Verifying build with race"
|
||||
@(env bash $(PWD)/buildscripts/verify-build.sh)
|
||||
|
||||
verify-healing: install-race ## verify healing and replacing disks with the Silo binary
|
||||
@echo "Verify healing build with race"
|
||||
@(env bash $(PWD)/buildscripts/verify-healing.sh)
|
||||
@(env bash $(PWD)/buildscripts/verify-healing-empty-erasure-set.sh)
|
||||
@(env bash $(PWD)/buildscripts/heal-inconsistent-versions.sh)
|
||||
|
||||
verify-healing-with-root-disks: install-race ## verify healing root disks
|
||||
@echo "Verify healing with root drives"
|
||||
@(env bash $(PWD)/buildscripts/verify-healing-with-root-disks.sh)
|
||||
|
||||
verify-healing-with-rewrite: install-race ## verify healing to rewrite old xl.meta -> new xl.meta
|
||||
@echo "Verify healing with rewrite"
|
||||
@(env bash $(PWD)/buildscripts/rewrite-old-new.sh)
|
||||
|
||||
verify-healing-inconsistent-versions: install-race ## verify resolving inconsistent versions
|
||||
@echo "Verify resolving inconsistent versions build with race"
|
||||
@(env bash $(PWD)/buildscripts/resolve-right-versions.sh)
|
||||
|
||||
build-debugging:
|
||||
@(env bash $(PWD)/docs/debugging/build.sh)
|
||||
|
||||
build: checks build-debugging ## builds Silo to $(PWD)
|
||||
@echo "Building Silo binary to './silo'"
|
||||
@CGO_ENABLED=0 GOOS=$(GOOS) GOARCH=$(GOARCH) go build -tags kqueue -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/silo 1>/dev/null
|
||||
|
||||
docker: checks build-debugging ## builds the local Linux Silo container image
|
||||
@echo "Building Silo container image '$(TAG)'"
|
||||
@set -e; \
|
||||
context=$$(mktemp -d); \
|
||||
trap 'rm -rf "$$context"' EXIT; \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=$(GOARCH) go build -tags kqueue -trimpath \
|
||||
--ldflags "$(LDFLAGS)" -o "$$context/silo"; \
|
||||
mkdir -p "$$context/dockerscripts"; \
|
||||
cp Dockerfile.goreleaser LICENSE NOTICE CREDITS "$$context/"; \
|
||||
cp dockerscripts/docker-entrypoint.sh dockerscripts/download-static-curl.sh \
|
||||
"$$context/dockerscripts/"; \
|
||||
docker build -q --no-cache --platform linux/$(GOARCH) -t $(TAG) --build-arg TARGETARCH=$(GOARCH) \
|
||||
-f "$$context/Dockerfile.goreleaser" "$$context"
|
||||
|
||||
docker-distroless: checks build-debugging ## builds the local Linux Silo distroless container image
|
||||
@echo "Building Silo distroless container image '$(TAG)-distroless'"
|
||||
@set -e; \
|
||||
context=$$(mktemp -d); \
|
||||
trap 'rm -rf "$$context"' EXIT; \
|
||||
CGO_ENABLED=0 GOOS=linux GOARCH=$(GOARCH) go build -tags kqueue -trimpath \
|
||||
--ldflags "$(LDFLAGS)" -o "$$context/silo"; \
|
||||
cp Dockerfile.distroless LICENSE NOTICE CREDITS "$$context/"; \
|
||||
docker build -q --no-cache --platform linux/$(GOARCH) -t $(TAG)-distroless \
|
||||
-f "$$context/Dockerfile.distroless" "$$context"
|
||||
|
||||
test-resiliency: build
|
||||
@echo "Running resiliency tests"
|
||||
@(DOCKER_COMPOSE_FILE=$(PWD)/docs/resiliency/docker-compose.yaml env bash $(PWD)/docs/resiliency/resiliency-tests.sh)
|
||||
|
||||
install-race: checks build-debugging ## builds Silo to $(PWD)
|
||||
@echo "Building Silo binary with -race to './silo'"
|
||||
@GORACE=history_size=7 CGO_ENABLED=1 go build -tags kqueue,dev -race -trimpath --ldflags "$(LDFLAGS)" -o $(PWD)/silo 1>/dev/null
|
||||
@echo "Installing Silo binary with -race to '$(GOPATH)/bin/silo'"
|
||||
@mkdir -p $(GOPATH)/bin && cp -af $(PWD)/silo $(GOPATH)/bin/silo
|
||||
|
||||
install: build ## builds Silo and installs it to $GOPATH/bin.
|
||||
@echo "Installing Silo binary to '$(GOPATH)/bin/silo'"
|
||||
@mkdir -p $(GOPATH)/bin && cp -af $(PWD)/silo $(GOPATH)/bin/silo
|
||||
@echo "Installation successful. To learn more, try \"silo --help\"."
|
||||
|
||||
clean: ## cleanup all generated assets
|
||||
@echo "Cleaning up all the generated files"
|
||||
@find . -name '*.test' | xargs rm -fv
|
||||
@find . -name '*~' | xargs rm -fv
|
||||
@find . -name '.#*#' | xargs rm -fv
|
||||
@find . -name '#*#' | xargs rm -fv
|
||||
@rm -rvf silo
|
||||
@rm -rvf build
|
||||
@rm -rvf release
|
||||
@rm -rvf .verify*
|
||||
@rm -rvf pkger_*.deb
|
||||
@@ -1,15 +0,0 @@
|
||||
MinIO Project, (C) 2015-2025 MinIO, Inc.
|
||||
|
||||
This product includes software developed at MinIO, Inc.
|
||||
(https://min.io/).
|
||||
|
||||
The MinIO project contains unmodified/modified subcomponents too with
|
||||
separate copyright notices and license terms. Your use of the source
|
||||
code for these subcomponents is subject to the terms and conditions
|
||||
of GNU Affero General Public License 3.0.
|
||||
|
||||
Silo Project modifications, (C) 2025-2026 PGSTY.
|
||||
|
||||
Silo is an independent community-maintained project incorporating MinIO
|
||||
source code. It is not affiliated with or endorsed by MinIO, Inc. Modified
|
||||
source and Silo release artifacts are maintained by the Silo project.
|
||||
@@ -1,93 +0,0 @@
|
||||
# Silo Pull Request Guidelines
|
||||
|
||||
These guidelines ensure high-quality commits in Silo's GitHub repositories, maintaining
|
||||
a clear, valuable commit history for our open-source projects. They apply to all contributors,
|
||||
fostering efficient reviews and robust code.
|
||||
|
||||
## Why Pull Requests?
|
||||
|
||||
Pull Requests (PRs) drive quality in Silo's codebase by:
|
||||
- Enabling peer review without pair programming.
|
||||
- Documenting changes for future reference.
|
||||
- Ensuring commits tell a clear story of development.
|
||||
|
||||
**A poor commit lasts forever, even if code is refactored.**
|
||||
|
||||
## Crafting a Quality PR
|
||||
|
||||
A strong Silo PR:
|
||||
- Delivers a complete, valuable change (feature, bug fix, or improvement).
|
||||
- Has a concise title (e.g., `[S3] Fix bucket policy parsing #1234`) and a summary with context, referencing issues (e.g., `#1234`).
|
||||
- Contains well-written, logical commits explaining *why* changes were made (e.g., “Add S3 bucket tagging support so that users can organize resources efficiently”).
|
||||
- Is small, focused, and easy to review—ideally one commit, unless multiple commits better narrate complex work.
|
||||
- Adheres to Silo's coding standards (e.g., Go style, error handling, testing).
|
||||
|
||||
PRs must flow smoothly through review to reach production. Large PRs should be split into smaller, manageable ones.
|
||||
|
||||
## Submitting PRs
|
||||
|
||||
1. **Title and Summary**:
|
||||
- Use a scannable title: `[Subsystem] Action Description #Issue` (e.g., `[IAM] Add role-based access control #567`).
|
||||
- Include context in the summary: what changed, why, and any issue references.
|
||||
- Use `[WIP]` for in-progress PRs to avoid premature merging or choose GitHub draft PRs.
|
||||
|
||||
2. **Commits**:
|
||||
- Write clear messages: what changed and why (e.g., “Refactor S3 API handler to reduce latency so that requests process 20% faster”).
|
||||
- Rebase to tidy commits before submitting (e.g., `git rebase -i main` to squash typos or reword messages), unless multiple contributors worked on the branch.
|
||||
- Keep PRs focused—one feature or fix. Split large changes into multiple PRs.
|
||||
|
||||
3. **Testing**:
|
||||
- Include unit tests for new functionality or bug fixes.
|
||||
- Ensure existing tests pass (`make test`).
|
||||
- Document testing steps in the PR summary if manual testing was performed.
|
||||
|
||||
4. **Before Submitting**:
|
||||
- Run `make verify` to check formatting, linting, and tests.
|
||||
- Reference related issues (e.g., “Closes #1234”).
|
||||
- Notify team members via GitHub `@mentions` if urgent or complex.
|
||||
|
||||
## Reviewing PRs
|
||||
|
||||
Reviewers ensure Silo's commit history remains a clear, reliable record. Responsibilities include:
|
||||
|
||||
1. **Commit Quality**:
|
||||
- Verify each commit explains *why* the change was made (e.g., “So that…”).
|
||||
- Request rebasing if commits are unclear, redundant, or lack context (e.g., “Please squash typo fixes into the parent commit”).
|
||||
|
||||
2. **Code Quality**:
|
||||
- Check adherence to Silo's Go standards (e.g., error handling, documentation).
|
||||
- Ensure tests cover new code and pass CI.
|
||||
- Flag bugs or critical issues for immediate fixes; suggest non-blocking improvements as follow-up issues.
|
||||
|
||||
3. **Flow**:
|
||||
- Review promptly to avoid blocking progress.
|
||||
- Balance quality and speed—minor issues can be addressed later via issues, not PR blocks.
|
||||
- If unable to complete the review, tag another reviewer (e.g., `@username please take over`).
|
||||
|
||||
4. **Shared Responsibility**:
|
||||
- All Silo contributors are reviewers. The first commenter on a PR owns the review unless they delegate.
|
||||
- Multiple reviewers are encouraged for complex PRs.
|
||||
|
||||
5. **No Self-Edits**:
|
||||
- Don’t modify the PR directly (e.g., fixing bugs). Request changes from the submitter or create a follow-up PR.
|
||||
- If you edit, you’re a collaborator, not a reviewer, and cannot merge.
|
||||
|
||||
6. **Testing**:
|
||||
- Assume the submitter tested the code. If testing is unclear, ask for details (e.g., “How was this tested?”).
|
||||
- Reject untested PRs unless testing is infeasible, then assist with test setup.
|
||||
|
||||
## Tips for Success
|
||||
|
||||
- **Small PRs**: Easier to review, faster to merge. Split large changes logically.
|
||||
- **Clear Commits**: Use `git rebase -i` to refine history before submitting.
|
||||
- **Engage Early**: Discuss complex changes in a GitHub issue before coding.
|
||||
- **Be Responsive**: Address reviewer feedback promptly to keep PRs moving.
|
||||
- **Learn from Reviews**: Use feedback to improve future contributions.
|
||||
|
||||
## Resources
|
||||
|
||||
- [Silo Contribution Guide](CONTRIBUTING.md)
|
||||
- [Effective Commit Messages](https://mislav.net/2014/02/hidden-documentation/)
|
||||
- [GitHub PR Tips](https://github.com/blog/1943-how-to-write-the-perfect-pull-request)
|
||||
|
||||
By following these guidelines, we ensure Silo's codebase remains high-quality, maintainable, and a joy to contribute to. Happy coding!
|
||||
@@ -1,183 +1,15 @@
|
||||
<h1 align="center">
|
||||
<a href="https://silo.pgsty.com/">
|
||||
<img src=".github/silo-logo.svg" alt="Silo" width="160">
|
||||
</a>
|
||||
</h1>
|
||||
# SILO repository cards
|
||||
|
||||
Generated by [Repository Cards](https://github.com/pgsty/silo/actions/workflows/repository-cards.yml)
|
||||
at 00:00 UTC daily (08:00 Asia/Shanghai). GitHub may queue scheduled runs.
|
||||
|
||||
<p align="center">
|
||||
<strong>S3-compatible object storage — a MinIO fork maintained by PGSTY</strong>
|
||||
</p>
|
||||
Snapshot: 2026-10-01. 3,693 stars; 58 community contributors.
|
||||
|
||||
- `contributors-light.svg` / `contributors-dark.svg`: human issue and PR authors across the SILO project scope, plus reviewed acknowledgements. Bots are excluded. Gold rings follow the reviewed companion-site roster; new authors are collected automatically.
|
||||
- `star-history-light.svg` / `star-history-dark.svg`: initial history reconstructed from the then-current stargazers; later points are daily observed totals, including decreases. Missing days are not fabricated.
|
||||
- `curated.json`: a cache of reviewed contributor credit from `pgsty/silo.pgsty.com/data/home/contributors.yaml`. The approved initial preview may be newer than the published site; a newer reviewed snapshot is retained until the site catches up.
|
||||
- `contributors.json`: generated contributor data and embedded raster avatars. Failed avatar refreshes use the previous image, or an initial when no image is available.
|
||||
- `history.json`: persistent daily totals. Keep this file when regenerating images.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://silo.pgsty.com/">Website</a> ·
|
||||
<a href="https://silo.pgsty.com/docs/">Documentation</a> ·
|
||||
<a href="https://silo.pgsty.com/download/">Download</a> ·
|
||||
<a href="https://silo.pgsty.com/tags/silo/">Release Notes</a> ·
|
||||
<a href="https://silo.pgsty.com/compatibility/server/">Compatibility</a> ·
|
||||
<a href="https://silo.pgsty.com/about/manifesto/">Manifesto</a> ·
|
||||
<a href="SECURITY.md">Security</a> ·
|
||||
<a href="README_ZH.md">中文</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://silo.pgsty.com/"><img alt="Website" src="https://img.shields.io/badge/Website-silo.pgsty.com-1d588c"></a>
|
||||
<a href="https://github.com/pgsty/silo/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/pgsty/silo?include_prereleases&label=release&logo=github"></a>
|
||||
<a href="https://hub.docker.com/r/pgsty/silo"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/pgsty/minio?logo=docker"></a>
|
||||
<a href="go.mod"><img alt="Go Version" src="https://img.shields.io/github/go-mod/go-version/pgsty/silo?logo=go"></a>
|
||||
<a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/license-AGPLv3-blue"></a>
|
||||
</p>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **PGSTY Silo** (hereinafter “Silo”) is an independent, community-maintained fork of the open-source MinIO server, published by [Pigsty](https://pigsty.io) from [`pgsty/silo`](https://github.com/pgsty/silo). It is not affiliated with, endorsed by, or sponsored by MinIO, Inc. “MinIO” is used only to identify the upstream project and compatibility lineage.
|
||||
|
||||
> [!NOTE]
|
||||
> Renamed from `pgsty/minio` to `pgsty/silo`, default branch `master` → `main`, on 2026-08-06. Artifacts under the original MinIO identity stay published on the archived [`minio`](https://github.com/pgsty/silo/tree/minio) branch and in releases up to [`RELEASE.2026-08-04T00-00-00Z`](https://github.com/pgsty/silo/releases/tag/RELEASE.2026-08-04T00-00-00Z).
|
||||
|
||||
## Overview
|
||||
|
||||
PGSTY SILO keeps one maintained release line of the open-source MinIO server alive after upstream ended community distribution: builds, packages, multi-arch images, security fixes, and the full web console. Pigsty runs it in production as its PostgreSQL backup repository.
|
||||
|
||||
It follows one rule — **the product and its delivery surfaces are renamed; the protocol and your data are not.** Everything else lives on [silo.pgsty.com](https://silo.pgsty.com/).
|
||||
|
||||
**Related:** [`pgsty/mc`](https://github.com/pgsty/mc) client (shipped as `mcli`) · [`pgsty/silo-console`](https://github.com/pgsty/silo-console) · [`pgsty/silo-pkg`](https://github.com/pgsty/silo-pkg) · [`pgsty/pigsty`](https://github.com/pgsty/pigsty)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://silo.pgsty.com/images/silo-console/console-metrics-simple.webp" alt="Silo Console">
|
||||
</p>
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
docker run -d --name silo -p 9000:9000 -p 9001:9001 \
|
||||
-e MINIO_ROOT_USER=minioadmin \
|
||||
-e MINIO_ROOT_PASSWORD=change-me-long-password \
|
||||
-v "$PWD/data:/data" \
|
||||
docker.io/pgsty/silo:latest server /data --console-address ":9001"
|
||||
```
|
||||
|
||||
<p align="center">
|
||||
<img src="https://silo.pgsty.com/images/silo-console/console-login.webp" alt="Silo Console">
|
||||
</p>
|
||||
|
||||
Console on <http://localhost:9001>, S3 API on <http://localhost:9000>. The image bundles the client as `mcli`:
|
||||
|
||||
```bash
|
||||
docker exec silo mcli alias set local http://127.0.0.1:9000 minioadmin change-me-long-password
|
||||
docker exec silo mcli mb local/demo && docker exec silo mcli ls local
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> For production, pin a release, use unique credentials and TLS, monitor the service, keep independent backups, and test recovery. Start from the [documentation](https://silo.pgsty.com/docs/).
|
||||
|
||||
## Install
|
||||
|
||||
| Method | Where |
|
||||
| :-- | :-- |
|
||||
| Container | [`pgsty/silo`](https://hub.docker.com/r/pgsty/silo), multi-arch for `linux/amd64` and `linux/arm64` |
|
||||
| Binaries | [GitHub Releases](https://github.com/pgsty/silo/releases) — Linux, macOS, Windows on `amd64` and `arm64` |
|
||||
| Packages | RPM, DEB, and APK, also via the [Pigsty repository](https://pigsty.io/docs/repo/) |
|
||||
| Kubernetes | Helm chart, see [Download & Install](https://silo.pgsty.com/download/) |
|
||||
| Source | `go build -o silo . && ./silo --version` |
|
||||
|
||||
Every release ships checksums, SPDX SBOMs, Sigstore-signed manifests, and GitHub build attestations. Installation methods and verification commands are documented at [Download & Install](https://silo.pgsty.com/download/); migrating from upstream MinIO — taking over an existing `minio.service` and its `/etc/default/minio`, and keeping data ownership stable with a `/etc/systemd/system/silo.service.d/10-legacy-user.conf` drop-in — is covered by the [migration guide](https://silo.pgsty.com/compatibility/migration/) and the [binary & service notes](https://silo.pgsty.com/compatibility/binary/).
|
||||
|
||||
## Compatibility
|
||||
|
||||
The S3 API, `MINIO_*` variables, `minio_*` metrics, `x-minio-*` headers, `/minio/*` routes, the `github.com/minio/*` import paths, and the on-disk format (including `.minio.sys`) are preserved and held in place by a CI compatibility check. Only Silo-owned delivery surfaces change: the `silo` executable, package, service, Helm chart, and container image — no `minio` binary alias is installed.
|
||||
|
||||
Every divergence from upstream is listed in the code-verified [compatibility audit](https://silo.pgsty.com/compatibility/server/). Treat each release as a downstream upgrade: pin versions, read the [release notes](https://silo.pgsty.com/tags/silo/), and keep a rollback path.
|
||||
|
||||
### TLS and Go upgrades
|
||||
|
||||
TLS key exchange follows Go's defaults across the S3 listener, node links,
|
||||
replication, identity providers, etcd, and external HTTP services. If an endpoint
|
||||
cannot accept ML-KEM, `GODEBUG=tlsmlkem=0` disables the default hybrid exchanges
|
||||
for the process; certificate verification remains enabled. This option does not
|
||||
disable ML-DSA signatures or resolve every TLS reset. Prefer updating the
|
||||
incompatible endpoint before removing the temporary setting.
|
||||
If only the new SecP hybrids cause problems, `GODEBUG=tlssecpmlkem=0` disables
|
||||
those groups while retaining X25519MLKEM768.
|
||||
|
||||
For builds targeting Go 1.27, setting either `SSL_CERT_FILE` or `SSL_CERT_DIR`
|
||||
on macOS replaces Keychain trust with on-disk roots and Go's verifier. Stale or
|
||||
incomplete CA paths can break previously trusted connections; unset inherited
|
||||
values to restore Keychain trust. Explicit certificates in the configured `CAs`
|
||||
directory remain additive to the selected root pool.
|
||||
Go 1.27 binaries require macOS 13 or later. See the
|
||||
[Go release notes](https://go.dev/doc/go1.27) and the
|
||||
[SILO stack investigation](docs/investigations/go127-stack.md).
|
||||
|
||||
## Security & Contributing
|
||||
|
||||
Report vulnerabilities privately as described in [`SECURITY.md`](SECURITY.md); every fix ships with a public [advisory](https://silo.pgsty.com/blog/security/). Contributions are accepted inbound=outbound under AGPL-3.0-or-later with no CLA — only DCO sign-off (`git commit -s`) is required; see [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
||||
|
||||
## Contributors
|
||||
|
||||
**41 community contributors** build SILO, Console, mcli, shared packages, and related projects. The list includes maintainers and every human Issue or PR author, ordered by merged PRs, other PRs, then issue reports. Gold rings highlight significant contributions.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Vonng"><img src="https://silo.pgsty.com/images/contributors/Vonng.svg" width="60" height="60" alt="@Vonng" title="@Vonng — Maintains SILO, Console, mcli, shared packages, releases, and documentation"></a>
|
||||
<a href="https://github.com/h5vx"><img src="https://silo.pgsty.com/images/contributors/h5vx.svg" width="60" height="60" alt="@h5vx" title="@h5vx — Implemented per-bucket CORS configuration and enforcement"></a>
|
||||
<a href="https://github.com/mrjavadseydi"><img src="https://silo.pgsty.com/images/contributors/mrjavadseydi.svg" width="60" height="60" alt="@mrjavadseydi" title="@mrjavadseydi — Fixed effective bucket quota metrics; proposed access-frequency ILM"></a>
|
||||
<a href="https://github.com/Dansyuqri"><img src="https://silo.pgsty.com/images/contributors/Dansyuqri.svg" width="60" height="60" alt="@Dansyuqri" title="@Dansyuqri — Added ChecksumType to multipart completion responses"></a>
|
||||
<a href="https://github.com/ycjlin"><img src="https://silo.pgsty.com/images/contributors/ycjlin.svg" width="60" height="60" alt="@ycjlin" title="@ycjlin — Fixed missing-bucket ListObjects semantics"></a>
|
||||
<a href="https://github.com/pinginfo"><img src="https://silo.pgsty.com/images/contributors/pinginfo.svg" width="60" height="60" alt="@pinginfo" title="@pinginfo — Repaired bucket notification streaming"></a>
|
||||
<a href="https://github.com/ZouhairCharef"><img src="https://silo.pgsty.com/images/contributors/ZouhairCharef.svg" width="60" height="60" alt="@ZouhairCharef" title="@ZouhairCharef — Patched CVE-2026-34986 in go-jose"></a>
|
||||
<a href="https://github.com/mfredenhagen"><img src="https://silo.pgsty.com/images/contributors/mfredenhagen.svg" width="60" height="60" alt="@mfredenhagen" title="@mfredenhagen — Patched CVE-2026-39883 in OpenTelemetry"></a>
|
||||
<a href="https://github.com/waterkip"><img src="https://silo.pgsty.com/images/contributors/waterkip.svg" width="60" height="60" alt="@waterkip" title="@waterkip — Repointed documentation links to the SILO portal"></a>
|
||||
<a href="https://github.com/mikemikimike"><img src="https://silo.pgsty.com/images/contributors/mikemikimike.svg" width="60" height="60" alt="@mikemikimike" title="@mikemikimike — Contributed the replicated SSE-C plaintext part-size fix"></a>
|
||||
<a href="https://github.com/metaneutrons"><img src="https://silo.pgsty.com/images/contributors/metaneutrons.svg" width="60" height="60" alt="@metaneutrons" title="@metaneutrons — Reported and proposed explicit-version delete authorization"></a>
|
||||
<a href="https://github.com/magicxor"><img src="https://silo.pgsty.com/images/contributors/magicxor.svg" width="60" height="60" alt="@magicxor" title="@magicxor — Reported and proposed conditional DELETE support for If-Match"></a>
|
||||
<a href="https://github.com/davinkevin"><img src="https://silo.pgsty.com/images/contributors/davinkevin.svg" width="60" height="60" alt="@davinkevin" title="@davinkevin — Proposed the distroless container image and dependency automation"></a>
|
||||
<a href="https://github.com/lem21h"><img src="https://silo.pgsty.com/images/contributors/lem21h.svg" width="48" height="48" alt="@lem21h" title="@lem21h — Proposed robustness and goroutine improvements"></a>
|
||||
<a href="https://github.com/sulin37392"><img src="https://silo.pgsty.com/images/contributors/sulin37392.svg" width="48" height="48" alt="@sulin37392" title="@sulin37392 — Proposed dependency updates"></a>
|
||||
<a href="https://github.com/cbornet"><img src="https://silo.pgsty.com/images/contributors/cbornet.svg" width="60" height="60" alt="@cbornet" title="@cbornet — Reported multipart and streaming checksum defects and missing-bucket semantics"></a>
|
||||
<a href="https://github.com/vampywiz17"><img src="https://silo.pgsty.com/images/contributors/vampywiz17.svg" width="60" height="60" alt="@vampywiz17" title="@vampywiz17 — Reported LDAP TLS and Console login regressions"></a>
|
||||
<a href="https://github.com/orenyomtov"><img src="https://silo.pgsty.com/images/contributors/orenyomtov.svg" width="60" height="60" alt="@orenyomtov" title="@orenyomtov — Reported the unsigned-header CopyObject cross-object read (SN-2026-011)"></a>
|
||||
<a href="https://github.com/mumu-lab"><img src="https://silo.pgsty.com/images/contributors/mumu-lab.svg" width="48" height="48" alt="@mumu-lab" title="@mumu-lab — Reported bucket quota metrics reading a deprecated field"></a>
|
||||
<a href="https://github.com/jvasile"><img src="https://silo.pgsty.com/images/contributors/jvasile.svg" width="48" height="48" alt="@jvasile" title="@jvasile — Reported missing user, group, and defaults in Debian packages"></a>
|
||||
<a href="https://github.com/pmezhuev"><img src="https://silo.pgsty.com/images/contributors/pmezhuev.svg" width="48" height="48" alt="@pmezhuev" title="@pmezhuev — Reported missing RPM package signatures"></a>
|
||||
<a href="https://github.com/TLINDEN"><img src="https://silo.pgsty.com/images/contributors/TLINDEN.svg" width="48" height="48" alt="@TLINDEN" title="@TLINDEN — Reported the missing client in release tarballs"></a>
|
||||
<a href="https://github.com/makinikm"><img src="https://silo.pgsty.com/images/contributors/makinikm.svg" width="48" height="48" alt="@makinikm" title="@makinikm — Reported the missing client in the container image"></a>
|
||||
<a href="https://github.com/meesudzu"><img src="https://silo.pgsty.com/images/contributors/meesudzu.svg" width="48" height="48" alt="@meesudzu" title="@meesudzu — Requested the migration guide from upstream MinIO"></a>
|
||||
<a href="https://github.com/kuldeep-link11"><img src="https://silo.pgsty.com/images/contributors/kuldeep-link11.svg" width="48" height="48" alt="@kuldeep-link11" title="@kuldeep-link11 — Reported NATS JWT credentials and target reload issues"></a>
|
||||
<a href="https://github.com/sargarass"><img src="https://silo.pgsty.com/images/contributors/sargarass.svg" width="48" height="48" alt="@sargarass" title="@sargarass — Reported ListMultipartUploads prefix and pagination semantics"></a>
|
||||
<a href="https://github.com/liuhaodongliu990-cmyk"><img src="https://silo.pgsty.com/images/contributors/liuhaodongliu990-cmyk.svg" width="48" height="48" alt="@liuhaodongliu990-cmyk" title="@liuhaodongliu990-cmyk — Reported indeterminate progress for prefix downloads"></a>
|
||||
<a href="https://github.com/Xavier-777"><img src="https://silo.pgsty.com/images/contributors/Xavier-777.svg" width="48" height="48" alt="@Xavier-777" title="@Xavier-777 — Reported Console lifecycle management and file preview gaps"></a>
|
||||
<a href="https://github.com/spaceg00se-r"><img src="https://silo.pgsty.com/images/contributors/spaceg00se-r.svg" width="48" height="48" alt="@spaceg00se-r" title="@spaceg00se-r — Requested cpuv1 support and reported a workflow token failure"></a>
|
||||
<a href="https://github.com/kh0mka"><img src="https://silo.pgsty.com/images/contributors/kh0mka.svg" width="48" height="48" alt="@kh0mka" title="@kh0mka — Reported inter-node I/O timeouts in ReadFileStreamHandler"></a>
|
||||
<a href="https://github.com/bagutzu"><img src="https://silo.pgsty.com/images/contributors/bagutzu.svg" width="48" height="48" alt="@bagutzu" title="@bagutzu — Requested KES-compatible external KMS and OpenBao support"></a>
|
||||
<a href="https://github.com/DestroyLee"><img src="https://silo.pgsty.com/images/contributors/DestroyLee.svg" width="48" height="48" alt="@DestroyLee" title="@DestroyLee — Reported the missing documentation navigation"></a>
|
||||
<a href="https://github.com/mosesdd"><img src="https://silo.pgsty.com/images/contributors/mosesdd.svg" width="48" height="48" alt="@mosesdd" title="@mosesdd — Requested a maintained Helm chart"></a>
|
||||
<a href="https://github.com/zylpsrs"><img src="https://silo.pgsty.com/images/contributors/zylpsrs.svg" width="48" height="48" alt="@zylpsrs" title="@zylpsrs — Reported missing Console tiering and site replication"></a>
|
||||
<a href="https://github.com/heroes1412"><img src="https://silo.pgsty.com/images/contributors/heroes1412.svg" width="48" height="48" alt="@heroes1412" title="@heroes1412 — Reported the unusable profiling option"></a>
|
||||
<a href="https://github.com/redfoxfox"><img src="https://silo.pgsty.com/images/contributors/redfoxfox.svg" width="48" height="48" alt="@redfoxfox" title="@redfoxfox — Reported Chinese documentation availability"></a>
|
||||
<a href="https://github.com/jiadzh"><img src="https://silo.pgsty.com/images/contributors/jiadzh.svg" width="48" height="48" alt="@jiadzh" title="@jiadzh — Requested Windows build guidance"></a>
|
||||
<a href="https://github.com/AntonOfTheWoods"><img src="https://silo.pgsty.com/images/contributors/AntonOfTheWoods.svg" width="48" height="48" alt="@AntonOfTheWoods" title="@AntonOfTheWoods — Asked for clarity on Helm chart and operator options"></a>
|
||||
<a href="https://github.com/chalukyaj"><img src="https://silo.pgsty.com/images/contributors/chalukyaj.svg" width="48" height="48" alt="@chalukyaj" title="@chalukyaj — Proposed making the SILO Operator easier to discover"></a>
|
||||
<a href="https://github.com/nsanitate"><img src="https://silo.pgsty.com/images/contributors/nsanitate.svg" width="48" height="48" alt="@nsanitate" title="@nsanitate — Proposed CNCF Sandbox governance"></a>
|
||||
<a href="https://github.com/Kesavaambati"><img src="https://silo.pgsty.com/images/contributors/Kesavaambati.svg" width="48" height="48" alt="@Kesavaambati" title="@Kesavaambati — Asked about community support and image maintenance"></a>
|
||||
</p>
|
||||
|
||||
[View the full contribution record](CONTRIBUTORS.md) for each person's proposals, fixes, and reports.
|
||||
|
||||
## Background
|
||||
|
||||
Upstream wound down its community edition: the web console was cut back to a stub, prebuilt community binaries stopped, and the community repository was archived. Silo exists to keep those deployments running. The fork is a means, not an identity — if upstream restores its community edition, we will narrow our scope and offer the fixes back.
|
||||
|
||||
The [**Manifesto**](https://silo.pgsty.com/about/manifesto/) is the project's public commitment in eleven articles, under one discipline: every article is either something already done with public evidence, or something explicitly refused. In short:
|
||||
|
||||
- **Compatibility contract** — the protocol and your data do not change, and every release documents its tested rollback target and path.
|
||||
- **The license cannot change** — AGPLv3, no CLA, no copyright aggregation; nobody here, ourselves included, holds enough copyright to relicense on everyone else's behalf.
|
||||
- **The never list**, append-only — no paywalling existing features, no registration wall on downloads, no telemetry (upstream's phone-home paths are removed outright), no CLA, no license change, no trademark enforcement against normal use.
|
||||
- **Security and release discipline** — a public advisory for every fix, and a release every one to two months, at most a quarter apart. Judge both against the public record.
|
||||
|
||||
Essays: [MinIO Is Dead](https://silo.pgsty.com/blog/post/minio-is-dead/) · [Who Takes Over?](https://silo.pgsty.com/blog/post/minio-alternative/) · [Long Live MinIO](https://silo.pgsty.com/blog/post/minio-resurrect/) · [Promise Kept](https://silo.pgsty.com/blog/post/minio-promise-kept/)
|
||||
|
||||
## License & Trademark
|
||||
|
||||
Silo is [AGPL-3.0-or-later](LICENSE), derived from [`minio/minio`](https://github.com/minio/minio) with upstream copyright and third-party notices preserved in [`NOTICE`](NOTICE) and [`CREDITS`](CREDITS). MinIO is a trademark of MinIO, Inc.; the name is used here only to identify the upstream project and compatibility lineage.
|
||||
|
||||
Details: [license](https://silo.pgsty.com/about/license/) · [attribution](https://silo.pgsty.com/about/attribution/) · [trademark](https://silo.pgsty.com/about/trademark/)
|
||||
The SVGs are self-contained. Source and instructions live on the default branch;
|
||||
this branch contains generated assets only. Do not merge it into `main`.
|
||||
|
||||
-163
@@ -1,163 +0,0 @@
|
||||
<h1 align="center">
|
||||
<a href="https://silo.pgsty.com/zh/">
|
||||
<img src=".github/silo-logo.svg" alt="Silo" width="160">
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
|
||||
<p align="center">
|
||||
<strong>S3 兼容对象存储 —— 由 PGSTY 维护的 MinIO 社区分支</strong>
|
||||
</p>
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://silo.pgsty.com/zh/">官网</a> ·
|
||||
<a href="https://silo.pgsty.com/zh/docs/">文档</a> ·
|
||||
<a href="https://silo.pgsty.com/zh/download/">下载</a> ·
|
||||
<a href="https://silo.pgsty.com/zh/tags/silo/">版本说明</a> ·
|
||||
<a href="https://silo.pgsty.com/zh/compatibility/server/">兼容性</a> ·
|
||||
<a href="https://silo.pgsty.com/zh/about/manifesto/">宣言</a> ·
|
||||
<a href="SECURITY.md">安全策略</a> ·
|
||||
<a href="README.md">English</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://silo.pgsty.com/zh/"><img alt="官网" src="https://img.shields.io/badge/%E5%AE%98%E7%BD%91-silo.pgsty.com%2Fzh-1d588c"></a>
|
||||
<a href="https://github.com/pgsty/silo/releases"><img alt="GitHub Release" src="https://img.shields.io/github/v/release/pgsty/silo?include_prereleases&label=release&logo=github"></a>
|
||||
<a href="https://hub.docker.com/r/pgsty/silo"><img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/pgsty/minio?logo=docker"></a>
|
||||
<a href="go.mod"><img alt="Go Version" src="https://img.shields.io/github/go-mod/go-version/pgsty/silo?logo=go"></a>
|
||||
<a href="LICENSE"><img alt="License" src="https://img.shields.io/badge/license-AGPLv3-blue"></a>
|
||||
</p>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **PGSTY Silo**(以下简称 Silo)是由 [Pigsty](https://pigsty.cc) 独立维护、从 [`pgsty/silo`](https://github.com/pgsty/silo) 发布的开源 MinIO 社区分支。本项目与 MinIO, Inc. 不存在隶属、背书或赞助关系;文中使用 “MinIO” 仅用于说明上游项目及兼容谱系。
|
||||
|
||||
> [!NOTE]
|
||||
> 2026-08-06,本仓库由 `pgsty/minio` 更名为 `pgsty/silo`,默认分支由 `master` 更名为 `main`。以原 MinIO 形态维持的归档构件仍位于归档的 [`minio`](https://github.com/pgsty/silo/tree/minio) 分支,以及截止 [`RELEASE.2026-08-04T00-00-00Z`](https://github.com/pgsty/silo/releases/tag/RELEASE.2026-08-04T00-00-00Z) 的历次发布中。
|
||||
|
||||
## 概述
|
||||
|
||||
上游停止社区发行后,Silo 为开源 MinIO 服务端维护一条持续可用的版本线:构建、软件包、多架构镜像、安全修复与完整 Web 控制台。Pigsty 在生产环境中用它承载 PostgreSQL 备份存储。
|
||||
|
||||
它只遵循一条原则:**改名的是产品与交付物,不是协议与你的数据。** 其余内容都在 [silo.pgsty.com](https://silo.pgsty.com/zh/)。
|
||||
|
||||
**相关项目:**[`pgsty/mc`](https://github.com/pgsty/mc) 客户端(以 `mcli` 发行) · [`pgsty/silo-console`](https://github.com/pgsty/silo-console) · [`pgsty/silo-pkg`](https://github.com/pgsty/silo-pkg) · [`pgsty/pigsty`](https://github.com/pgsty/pigsty)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://silo.pgsty.com/images/silo-console/console-metrics-simple.webp" alt="Silo 控制台">
|
||||
</p>
|
||||
|
||||
## 快速上手
|
||||
|
||||
```bash
|
||||
docker run -d --name silo -p 9000:9000 -p 9001:9001 \
|
||||
-e MINIO_ROOT_USER=minioadmin \
|
||||
-e MINIO_ROOT_PASSWORD=change-me-long-password \
|
||||
-v "$PWD/data:/data" \
|
||||
docker.io/pgsty/silo:latest server /data --console-address ":9001"
|
||||
```
|
||||
|
||||
<p align="center">
|
||||
<img src="https://silo.pgsty.com/images/silo-console/console-login.webp" alt="Silo 控制台">
|
||||
</p>
|
||||
|
||||
控制台位于 <http://localhost:9001>,S3 API 位于 <http://localhost:9000>。镜像内置客户端 `mcli`:
|
||||
|
||||
```bash
|
||||
docker exec silo mcli alias set local http://127.0.0.1:9000 minioadmin change-me-long-password
|
||||
docker exec silo mcli mb local/demo && docker exec silo mcli ls local
|
||||
```
|
||||
|
||||
> [!WARNING]
|
||||
> 生产环境应锁定版本,使用独立凭据与 TLS,配置监控,保留独立备份,并验证恢复流程。请从[文档](https://silo.pgsty.com/zh/docs/)开始。
|
||||
|
||||
## 安装
|
||||
|
||||
| 方式 | 位置 |
|
||||
| :-- | :-- |
|
||||
| 容器镜像 | [`pgsty/silo`](https://hub.docker.com/r/pgsty/silo),支持 `linux/amd64` 与 `linux/arm64` |
|
||||
| 二进制 | [GitHub Releases](https://github.com/pgsty/silo/releases),覆盖 Linux、macOS、Windows 的 `amd64` 与 `arm64` |
|
||||
| 软件包 | RPM、DEB、APK,也可通过 [Pigsty 软件仓库](https://pigsty.cc/docs/repo/) 安装 |
|
||||
| Kubernetes | Helm Chart,参见[下载与安装](https://silo.pgsty.com/zh/download/) |
|
||||
| 源码构建 | `go build -o silo . && ./silo --version` |
|
||||
|
||||
每个版本都附带校验和、SPDX SBOM、Sigstore 签名清单与 GitHub 构建证明。完整安装方式与验证命令见[下载与安装](https://silo.pgsty.com/zh/download/);从上游 MinIO 迁移 —— 接管既有 `minio.service` 与 `/etc/default/minio`,并用 `/etc/systemd/system/silo.service.d/10-legacy-user.conf` drop-in 保持数据属主不变 —— 见[迁移指南](https://silo.pgsty.com/zh/compatibility/migration/)与[二进制与服务说明](https://silo.pgsty.com/zh/compatibility/binary/)。
|
||||
|
||||
## 兼容性
|
||||
|
||||
S3 API、`MINIO_*` 环境变量、`minio_*` 指标、`x-minio-*` 头、`/minio/*` 路由、`github.com/minio/*` 导入路径与磁盘格式(含 `.minio.sys`)原样保留,并由 CI 兼容性门禁冻结。只有 Silo 自有交付面改名:`silo` 可执行文件、软件包、服务、Helm Chart 与容器镜像 —— 原生交付物不会安装 `minio` 二进制别名。
|
||||
|
||||
与上游的全部分歧,以逐项核验代码的[兼容性审计](https://silo.pgsty.com/zh/compatibility/server/)形式维护。每个版本仍应视为下游升级:锁定版本,阅读[版本说明](https://silo.pgsty.com/zh/tags/silo/),并保留回滚路径。
|
||||
|
||||
## 安全与贡献
|
||||
|
||||
请按照 [`SECURITY.md`](SECURITY.md) 私密报告漏洞;每项修复都会发布公开[安全公告](https://silo.pgsty.com/zh/blog/security/)。本项目不要求签署 CLA:贡献按 AGPL-3.0-or-later(inbound=outbound)接收,只需 DCO 签署(`git commit -s`),详见 [`CONTRIBUTING.md`](CONTRIBUTING.md)。
|
||||
|
||||
## 贡献者
|
||||
|
||||
**41 位社区贡献者**共同建设 SILO、Console、mcli、公共包与相关项目。名单包含维护者,以及所有提出 Issue 或 PR 的真人作者;按已合并 PR、其他 PR、Issue 报告排序,黄圈标记显著贡献。
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/Vonng"><img src="https://silo.pgsty.com/images/contributors/Vonng.svg" width="60" height="60" alt="@Vonng" title="@Vonng — 维护 SILO、Console、mcli、公共包、发行与文档"></a>
|
||||
<a href="https://github.com/h5vx"><img src="https://silo.pgsty.com/images/contributors/h5vx.svg" width="60" height="60" alt="@h5vx" title="@h5vx — 实现单桶 CORS 配置与请求执行"></a>
|
||||
<a href="https://github.com/mrjavadseydi"><img src="https://silo.pgsty.com/images/contributors/mrjavadseydi.svg" width="60" height="60" alt="@mrjavadseydi" title="@mrjavadseydi — 修复有效桶配额指标,并提交按访问频率分层的 ILM 方案"></a>
|
||||
<a href="https://github.com/Dansyuqri"><img src="https://silo.pgsty.com/images/contributors/Dansyuqri.svg" width="60" height="60" alt="@Dansyuqri" title="@Dansyuqri — 为分片上传完成响应补充 ChecksumType"></a>
|
||||
<a href="https://github.com/ycjlin"><img src="https://silo.pgsty.com/images/contributors/ycjlin.svg" width="60" height="60" alt="@ycjlin" title="@ycjlin — 修复缺失桶的 ListObjects 语义"></a>
|
||||
<a href="https://github.com/pinginfo"><img src="https://silo.pgsty.com/images/contributors/pinginfo.svg" width="60" height="60" alt="@pinginfo" title="@pinginfo — 修复桶通知的流式输出"></a>
|
||||
<a href="https://github.com/ZouhairCharef"><img src="https://silo.pgsty.com/images/contributors/ZouhairCharef.svg" width="60" height="60" alt="@ZouhairCharef" title="@ZouhairCharef — 修复 go-jose 中的 CVE-2026-34986"></a>
|
||||
<a href="https://github.com/mfredenhagen"><img src="https://silo.pgsty.com/images/contributors/mfredenhagen.svg" width="60" height="60" alt="@mfredenhagen" title="@mfredenhagen — 修复 OpenTelemetry 中的 CVE-2026-39883"></a>
|
||||
<a href="https://github.com/waterkip"><img src="https://silo.pgsty.com/images/contributors/waterkip.svg" width="60" height="60" alt="@waterkip" title="@waterkip — 将文档链接指向 SILO 门户"></a>
|
||||
<a href="https://github.com/mikemikimike"><img src="https://silo.pgsty.com/images/contributors/mikemikimike.svg" width="60" height="60" alt="@mikemikimike" title="@mikemikimike — 提交 SSE-C 复制分片明文尺寸修复"></a>
|
||||
<a href="https://github.com/metaneutrons"><img src="https://silo.pgsty.com/images/contributors/metaneutrons.svg" width="60" height="60" alt="@metaneutrons" title="@metaneutrons — 报告并提交显式版本删除鉴权方案"></a>
|
||||
<a href="https://github.com/magicxor"><img src="https://silo.pgsty.com/images/contributors/magicxor.svg" width="60" height="60" alt="@magicxor" title="@magicxor — 报告并提交 DELETE If-Match 条件请求支持方案"></a>
|
||||
<a href="https://github.com/davinkevin"><img src="https://silo.pgsty.com/images/contributors/davinkevin.svg" width="60" height="60" alt="@davinkevin" title="@davinkevin — 提交 distroless 容器镜像与依赖自动更新方案"></a>
|
||||
<a href="https://github.com/lem21h"><img src="https://silo.pgsty.com/images/contributors/lem21h.svg" width="48" height="48" alt="@lem21h" title="@lem21h — 提交健壮性与 goroutine 改进"></a>
|
||||
<a href="https://github.com/sulin37392"><img src="https://silo.pgsty.com/images/contributors/sulin37392.svg" width="48" height="48" alt="@sulin37392" title="@sulin37392 — 提交依赖更新"></a>
|
||||
<a href="https://github.com/cbornet"><img src="https://silo.pgsty.com/images/contributors/cbornet.svg" width="60" height="60" alt="@cbornet" title="@cbornet — 报告分片与流式校验和缺陷及缺失桶语义问题"></a>
|
||||
<a href="https://github.com/vampywiz17"><img src="https://silo.pgsty.com/images/contributors/vampywiz17.svg" width="60" height="60" alt="@vampywiz17" title="@vampywiz17 — 报告 LDAP TLS 与 Console 登录回归"></a>
|
||||
<a href="https://github.com/orenyomtov"><img src="https://silo.pgsty.com/images/contributors/orenyomtov.svg" width="60" height="60" alt="@orenyomtov" title="@orenyomtov — 报告未签名头导致的 CopyObject 跨对象读取(SN-2026-011)"></a>
|
||||
<a href="https://github.com/mumu-lab"><img src="https://silo.pgsty.com/images/contributors/mumu-lab.svg" width="48" height="48" alt="@mumu-lab" title="@mumu-lab — 报告桶配额指标读取已弃用字段的问题"></a>
|
||||
<a href="https://github.com/jvasile"><img src="https://silo.pgsty.com/images/contributors/jvasile.svg" width="48" height="48" alt="@jvasile" title="@jvasile — 报告 Debian 包缺少用户、用户组与默认配置"></a>
|
||||
<a href="https://github.com/pmezhuev"><img src="https://silo.pgsty.com/images/contributors/pmezhuev.svg" width="48" height="48" alt="@pmezhuev" title="@pmezhuev — 报告 RPM 包缺少 GPG 签名"></a>
|
||||
<a href="https://github.com/TLINDEN"><img src="https://silo.pgsty.com/images/contributors/TLINDEN.svg" width="48" height="48" alt="@TLINDEN" title="@TLINDEN — 报告发布压缩包缺少客户端"></a>
|
||||
<a href="https://github.com/makinikm"><img src="https://silo.pgsty.com/images/contributors/makinikm.svg" width="48" height="48" alt="@makinikm" title="@makinikm — 报告容器镜像缺少客户端"></a>
|
||||
<a href="https://github.com/meesudzu"><img src="https://silo.pgsty.com/images/contributors/meesudzu.svg" width="48" height="48" alt="@meesudzu" title="@meesudzu — 提出从上游 MinIO 迁移的指南需求"></a>
|
||||
<a href="https://github.com/kuldeep-link11"><img src="https://silo.pgsty.com/images/contributors/kuldeep-link11.svg" width="48" height="48" alt="@kuldeep-link11" title="@kuldeep-link11 — 报告 NATS JWT 凭据与通知目标重载问题"></a>
|
||||
<a href="https://github.com/sargarass"><img src="https://silo.pgsty.com/images/contributors/sargarass.svg" width="48" height="48" alt="@sargarass" title="@sargarass — 报告 ListMultipartUploads 前缀与分页语义问题"></a>
|
||||
<a href="https://github.com/liuhaodongliu990-cmyk"><img src="https://silo.pgsty.com/images/contributors/liuhaodongliu990-cmyk.svg" width="48" height="48" alt="@liuhaodongliu990-cmyk" title="@liuhaodongliu990-cmyk — 报告前缀下载进度显示异常"></a>
|
||||
<a href="https://github.com/Xavier-777"><img src="https://silo.pgsty.com/images/contributors/Xavier-777.svg" width="48" height="48" alt="@Xavier-777" title="@Xavier-777 — 报告 Console 生命周期管理与文件预览缺失"></a>
|
||||
<a href="https://github.com/spaceg00se-r"><img src="https://silo.pgsty.com/images/contributors/spaceg00se-r.svg" width="48" height="48" alt="@spaceg00se-r" title="@spaceg00se-r — 提出 cpuv1 支持需求并报告工作流令牌错误"></a>
|
||||
<a href="https://github.com/kh0mka"><img src="https://silo.pgsty.com/images/contributors/kh0mka.svg" width="48" height="48" alt="@kh0mka" title="@kh0mka — 报告 ReadFileStreamHandler 节点间 I/O 超时"></a>
|
||||
<a href="https://github.com/bagutzu"><img src="https://silo.pgsty.com/images/contributors/bagutzu.svg" width="48" height="48" alt="@bagutzu" title="@bagutzu — 提出兼容 KES 的外部 KMS 与 OpenBao 支持需求"></a>
|
||||
<a href="https://github.com/DestroyLee"><img src="https://silo.pgsty.com/images/contributors/DestroyLee.svg" width="48" height="48" alt="@DestroyLee" title="@DestroyLee — 报告文档目录导航缺失"></a>
|
||||
<a href="https://github.com/mosesdd"><img src="https://silo.pgsty.com/images/contributors/mosesdd.svg" width="48" height="48" alt="@mosesdd" title="@mosesdd — 提出维护 Helm Chart 的需求"></a>
|
||||
<a href="https://github.com/zylpsrs"><img src="https://silo.pgsty.com/images/contributors/zylpsrs.svg" width="48" height="48" alt="@zylpsrs" title="@zylpsrs — 报告 Console 缺少分层与站点复制"></a>
|
||||
<a href="https://github.com/heroes1412"><img src="https://silo.pgsty.com/images/contributors/heroes1412.svg" width="48" height="48" alt="@heroes1412" title="@heroes1412 — 报告性能分析选项不可用"></a>
|
||||
<a href="https://github.com/redfoxfox"><img src="https://silo.pgsty.com/images/contributors/redfoxfox.svg" width="48" height="48" alt="@redfoxfox" title="@redfoxfox — 报告中文文档站点不可用"></a>
|
||||
<a href="https://github.com/jiadzh"><img src="https://silo.pgsty.com/images/contributors/jiadzh.svg" width="48" height="48" alt="@jiadzh" title="@jiadzh — 提出 Windows 构建指导需求"></a>
|
||||
<a href="https://github.com/AntonOfTheWoods"><img src="https://silo.pgsty.com/images/contributors/AntonOfTheWoods.svg" width="48" height="48" alt="@AntonOfTheWoods" title="@AntonOfTheWoods — 提出明确 Helm Chart 与 Operator 选项的需求"></a>
|
||||
<a href="https://github.com/chalukyaj"><img src="https://silo.pgsty.com/images/contributors/chalukyaj.svg" width="48" height="48" alt="@chalukyaj" title="@chalukyaj — 提出改善 SILO Operator 可发现性的建议"></a>
|
||||
<a href="https://github.com/nsanitate"><img src="https://silo.pgsty.com/images/contributors/nsanitate.svg" width="48" height="48" alt="@nsanitate" title="@nsanitate — 提出加入 CNCF Sandbox 的治理建议"></a>
|
||||
<a href="https://github.com/Kesavaambati"><img src="https://silo.pgsty.com/images/contributors/Kesavaambati.svg" width="48" height="48" alt="@Kesavaambati" title="@Kesavaambati — 提出社区支持与容器镜像维护问题"></a>
|
||||
</p>
|
||||
|
||||
[查看完整贡献记录](CONTRIBUTORS.md),了解每位贡献者的提案、修复与问题报告。
|
||||
|
||||
## 背景
|
||||
|
||||
本项目因上游收缩社区版而生:Web 控制台被削减为残桩、社区预编译制品停发、社区仓库被归档。Silo 的存在就是让这些部署继续跑下去。Fork 是手段,不是身份 —— 若上游恢复社区版承诺,我们乐意收缩范围,并把修复回馈上游。
|
||||
|
||||
[**宣言**](https://silo.pgsty.com/zh/about/manifesto/)是项目的公开承诺,共十一条,通篇遵循一项纪律:**每一条,要么是已经在做且有公开证据的事实,要么是刻意拒绝的承诺。** 摘要:
|
||||
|
||||
- **兼容性合同** —— 协议与数据不改,每个版本都标注经过测试的回滚目标与路径。
|
||||
- **许可证无法变更** —— AGPLv3、无 CLA、不做版权聚合;包括我们自己在内,没有人握有足够版权代表所有贡献者重新授权。
|
||||
- **永不清单**(只增不减)—— 永不将既有功能移入付费墙、永不给下载设注册墙、永不加入遥测(上游回连路径已整体移除)、永不引入 CLA、永不变更许可证、永不以商标追究正常使用。
|
||||
- **安全与发布纪律** —— 每项安全修复配一篇公开公告;通常每一到两个月发布一版,最长不超过一个季度。请拿公开记录检验这两条。
|
||||
|
||||
延伸阅读:[MinIO已死](https://silo.pgsty.com/zh/blog/post/minio-is-dead/) · [谁能接盘?](https://silo.pgsty.com/zh/blog/post/minio-alternative/) · [MinIO 复生](https://silo.pgsty.com/zh/blog/post/minio-resurrect/) · [承诺兑现](https://silo.pgsty.com/zh/blog/post/minio-promise-kept/)
|
||||
|
||||
## 许可证与商标
|
||||
|
||||
Silo 采用 [AGPL-3.0-or-later](LICENSE),衍生自 [`minio/minio`](https://github.com/minio/minio),上游版权与第三方声明完整保留于 [`NOTICE`](NOTICE) 与 [`CREDITS`](CREDITS)。MinIO 是 MinIO, Inc. 的商标,此处使用仅为标识上游项目与兼容谱系。
|
||||
|
||||
详见:[许可证](https://silo.pgsty.com/zh/about/license/) · [署名归属](https://silo.pgsty.com/zh/about/attribution/) · [商标声明](https://silo.pgsty.com/zh/about/trademark/)
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
Silo is an independent, community-maintained object-storage server derived from
|
||||
the open-source MinIO server. Upstream MinIO security contacts do not handle
|
||||
Silo-specific fixes or release notes.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Security fixes are tracked on the active development branch and summarized in
|
||||
[docs/security/advisories.md](docs/security/advisories.md). Only the current
|
||||
Silo release line is supported unless an advisory says otherwise.
|
||||
|
||||
## Inherited Fix Evidence
|
||||
|
||||
The canonical ledger also records security fixes inherited from upstream when
|
||||
they are part of the Silo release baseline. Source and fork commits are linked
|
||||
separately even when the fork preserves the original commit object and SHA.
|
||||
|
||||
- [CVE-2025-62506](https://github.com/advisories/GHSA-jjjj-jwhf-8rgr):
|
||||
upstream [PR #21642](https://github.com/minio/minio/pull/21642) merged as
|
||||
[`minio/minio@c1a49490`](https://github.com/minio/minio/commit/c1a49490c78e9c3ebcad86ba0662319138ace190),
|
||||
inherited unchanged as
|
||||
[`pgsty/silo@c1a49490`](https://github.com/pgsty/silo/commit/c1a49490c78e9c3ebcad86ba0662319138ace190),
|
||||
and is present in every Silo community release beginning with
|
||||
[`RELEASE.2025-12-03T12-00-00Z`](https://github.com/pgsty/silo/releases/tag/RELEASE.2025-12-03T12-00-00Z).
|
||||
The inherited [service-account](https://github.com/pgsty/silo/blob/c1a49490c78e9c3ebcad86ba0662319138ace190/cmd/admin-handlers-users_test.go#L211-L212)
|
||||
and [STS](https://github.com/pgsty/silo/blob/c1a49490c78e9c3ebcad86ba0662319138ace190/cmd/sts-handlers_test.go#L45-L46)
|
||||
regression groups remain part of `go test ./cmd`; see the
|
||||
[canonical ledger](docs/security/advisories.md#inherited-upstream-advisory-baseline)
|
||||
for the operator-facing record.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
For vulnerabilities in this fork:
|
||||
|
||||
1. Follow the fork-specific expectations in [VULNERABILITY_REPORT.md](VULNERABILITY_REPORT.md).
|
||||
2. Prefer this repository's [private GitHub security advisory](https://github.com/pgsty/silo/security/advisories/new) workflow.
|
||||
3. If private reporting is unavailable, contact the maintainers through the
|
||||
repository without publishing exploit details until a private channel is
|
||||
established.
|
||||
4. If you confirm the issue also affects upstream `minio/minio`, report it upstream separately.
|
||||
|
||||
## Disclosure Process
|
||||
|
||||
Fork-specific fixes and user-visible upgrade notes are published in [docs/security/advisories.md](docs/security/advisories.md). The fork-specific triage and remediation process is described in [VULNERABILITY_REPORT.md](VULNERABILITY_REPORT.md).
|
||||
@@ -1,37 +0,0 @@
|
||||
# Vulnerability Management Policy
|
||||
|
||||
This document describes how the Silo maintainers investigate,
|
||||
assess, and remediate reported vulnerabilities affecting this fork, any
|
||||
directly shipped component, or a direct / indirect dependency used by this
|
||||
repository.
|
||||
|
||||
## Scope
|
||||
|
||||
This policy covers vulnerability reports opened by repository maintainers or
|
||||
external third parties against Silo itself, its release artifacts, or
|
||||
dependencies that materially affect this fork.
|
||||
|
||||
It defines the information needed for triage and the expected remediation
|
||||
workflow for supported fixes.
|
||||
|
||||
## Vulnerability Management Process
|
||||
|
||||
A useful vulnerability report should contain the following information:
|
||||
|
||||
- The project / component that contains the reported vulnerability.
|
||||
- A description of the vulnerability. In particular, the type of the
|
||||
reported vulnerability and how it might be exploited. Alternatively,
|
||||
a well-established vulnerability identifier, such as a CVE or GHSA ID, can
|
||||
be used instead.
|
||||
|
||||
Based on the report, the Silo maintainers investigate:
|
||||
|
||||
- Whether the reported vulnerability exists.
|
||||
- The conditions that are required such that the vulnerability can be exploited.
|
||||
- Which releases, branches, or deployment paths are affected.
|
||||
- The steps required to fix the vulnerability.
|
||||
|
||||
If the vulnerability exists in this fork itself, the maintainers will, when
|
||||
feasible, fix the issue or implement reasonable countermeasures such that the
|
||||
vulnerability can no longer be exploited. Fork-specific upgrade notes and
|
||||
security advisories are published in `docs/security/advisories.md`.
|
||||
@@ -1,76 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Fail closed before a release job can replace published or finalized assets.
|
||||
# An ordinary Draft is retry state; a finalized Draft contains GPG-derived
|
||||
# materials and must never be replaced by the build lane.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
release_tag="${1:-}"
|
||||
fixture="${2:-}"
|
||||
repository="${GITHUB_REPOSITORY:-pgsty/silo}"
|
||||
require_draft="${REQUIRE_DRAFT:-false}"
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "jq is required to inspect GitHub release state" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! "${release_tag}" =~ ^RELEASE\.[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}-[0-9]{2}-[0-9]{2}Z$ ]]; then
|
||||
echo "Invalid release tag format: ${release_tag:-<empty>}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "${fixture}" ]; then
|
||||
release_json="$(<"${fixture}")"
|
||||
else
|
||||
error_file="$(mktemp)"
|
||||
trap 'rm -f "${error_file}"' EXIT
|
||||
if ! release_json="$(
|
||||
gh api --paginate "repos/${repository}/releases?per_page=100" --jq '.[]' 2>"${error_file}" |
|
||||
jq --arg tag "${release_tag}" -s '[.[] | select(.tag_name == $tag)]'
|
||||
)"; then
|
||||
cat "${error_file}" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if ! jq -e 'type == "array" and all(.[]; type == "object" and (.tag_name | type == "string") and (.draft | type == "boolean"))' \
|
||||
<<<"${release_json}" >/dev/null 2>&1; then
|
||||
echo "Invalid release state response for ${release_tag}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! jq -e --arg tag "${release_tag}" 'all(.[]; .tag_name == $tag)' \
|
||||
<<<"${release_json}" >/dev/null 2>&1; then
|
||||
echo "Release state returned a tag other than ${release_tag}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
release_count="$(jq 'length' <<<"${release_json}")"
|
||||
if [ "${release_count}" -eq 0 ]; then
|
||||
if [ "${require_draft}" = "true" ]; then
|
||||
echo "Expected one Draft release for ${release_tag}, found none" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "No existing release for ${release_tag}."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${release_count}" -ne 1 ]; then
|
||||
echo "Refusing to choose among ${release_count} releases for ${release_tag}; clean duplicate Drafts first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$(jq -r '.[0].draft' <<<"${release_json}")" != "true" ]; then
|
||||
echo "Refusing to overwrite published release ${release_tag}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
finalize_markers="$(jq '[.[0].assets[]? | select(.name | endswith("_packages_provenance.sigstore.json"))] | length' <<<"${release_json}")"
|
||||
if [ "${finalize_markers}" -ne 0 ]; then
|
||||
echo "Refusing to replace finalized Draft ${release_tag}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Existing unfinalized Draft ${release_tag} will be replaced from scratch."
|
||||
@@ -1,68 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
checker="${script_dir}/check-release-state.sh"
|
||||
tag="RELEASE.2026-08-29T00-00-00Z"
|
||||
fixture="$(mktemp)"
|
||||
stdout_file="$(mktemp)"
|
||||
stderr_file="$(mktemp)"
|
||||
trap 'rm -f "${fixture}" "${stdout_file}" "${stderr_file}"' EXIT
|
||||
|
||||
expect_success() {
|
||||
if ! "${checker}" "$@" >"${stdout_file}" 2>"${stderr_file}"; then
|
||||
cat "${stderr_file}" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
expect_failure() {
|
||||
if "${checker}" "$@" >"${stdout_file}" 2>"${stderr_file}"; then
|
||||
echo "Expected release-state check to fail: $*" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
printf '[]\n' >"${fixture}"
|
||||
expect_success "${tag}" "${fixture}"
|
||||
grep -qF "No existing release for ${tag}." "${stdout_file}"
|
||||
|
||||
if REQUIRE_DRAFT=true "${checker}" "${tag}" "${fixture}" >"${stdout_file}" 2>"${stderr_file}"; then
|
||||
echo "Expected required-Draft check to fail when no release exists" >&2
|
||||
exit 1
|
||||
fi
|
||||
grep -qF "Expected one Draft release for ${tag}, found none" "${stderr_file}"
|
||||
|
||||
printf '[{"tag_name":"%s","draft":true,"assets":[]}]\n' "${tag}" >"${fixture}"
|
||||
expect_success "${tag}" "${fixture}"
|
||||
grep -qF "Existing unfinalized Draft ${tag} will be replaced from scratch." "${stdout_file}"
|
||||
if ! REQUIRE_DRAFT=true "${checker}" "${tag}" "${fixture}" >"${stdout_file}" 2>"${stderr_file}"; then
|
||||
cat "${stderr_file}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '[{"tag_name":"%s","draft":true,"assets":[{"name":"silo_20260829000000.0.0_packages_provenance.sigstore.json"}]}]\n' "${tag}" >"${fixture}"
|
||||
expect_failure "${tag}" "${fixture}"
|
||||
grep -qF "Refusing to replace finalized Draft ${tag}" "${stderr_file}"
|
||||
|
||||
printf '[{"tag_name":"%s","draft":false}]\n' "${tag}" >"${fixture}"
|
||||
expect_failure "${tag}" "${fixture}"
|
||||
grep -qF "Refusing to overwrite published release ${tag}" "${stderr_file}"
|
||||
|
||||
printf '[{"tag_name":"%s","draft":true},{"tag_name":"%s","draft":true}]\n' "${tag}" "${tag}" >"${fixture}"
|
||||
expect_failure "${tag}" "${fixture}"
|
||||
grep -qF "Refusing to choose among 2 releases" "${stderr_file}"
|
||||
|
||||
printf '[{"tag_name":"RELEASE.2026-08-28T00-00-00Z","draft":true}]\n' >"${fixture}"
|
||||
expect_failure "${tag}" "${fixture}"
|
||||
grep -qF "other than ${tag}" "${stderr_file}"
|
||||
|
||||
printf '{not-json}\n' >"${fixture}"
|
||||
expect_failure "${tag}" "${fixture}"
|
||||
grep -qF "Invalid release state response for ${tag}" "${stderr_file}"
|
||||
|
||||
expect_failure "not-a-release-tag" "${fixture}"
|
||||
grep -qF "Invalid release tag format" "${stderr_file}"
|
||||
|
||||
echo "release-state decision tests passed"
|
||||
@@ -1,143 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
|
||||
_init() {
|
||||
|
||||
shopt -s extglob
|
||||
|
||||
## Minimum required versions for build dependencies
|
||||
GIT_VERSION="1.0"
|
||||
GO_VERSION="1.27.1"
|
||||
OSX_VERSION="10.8"
|
||||
KNAME=$(uname -s)
|
||||
ARCH=$(uname -m)
|
||||
case "${KNAME}" in
|
||||
SunOS)
|
||||
ARCH=$(isainfo -k)
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
## FIXME:
|
||||
## In OSX, 'readlink -f' option does not exist, hence
|
||||
## we have our own readlink -f behavior here.
|
||||
## Once OSX has the option, below function is good enough.
|
||||
##
|
||||
## readlink() {
|
||||
## return /bin/readlink -f "$1"
|
||||
## }
|
||||
##
|
||||
readlink() {
|
||||
TARGET_FILE=$1
|
||||
|
||||
cd $(dirname $TARGET_FILE)
|
||||
TARGET_FILE=$(basename $TARGET_FILE)
|
||||
|
||||
# Iterate down a (possible) chain of symlinks
|
||||
while [ -L "$TARGET_FILE" ]; do
|
||||
TARGET_FILE=$(env readlink $TARGET_FILE)
|
||||
cd $(dirname $TARGET_FILE)
|
||||
TARGET_FILE=$(basename $TARGET_FILE)
|
||||
done
|
||||
|
||||
# Compute the canonicalized name by finding the physical path
|
||||
# for the directory we're in and appending the target file.
|
||||
PHYS_DIR=$(pwd -P)
|
||||
RESULT=$PHYS_DIR/$TARGET_FILE
|
||||
echo $RESULT
|
||||
}
|
||||
|
||||
## FIXME:
|
||||
## In OSX, 'sort -V' option does not exist, hence
|
||||
## we have our own version compare function.
|
||||
## Once OSX has the option, below function is good enough.
|
||||
##
|
||||
## check_minimum_version() {
|
||||
## versions=($(echo -e "$1\n$2" | sort -V))
|
||||
## return [ "$1" == "${versions[0]}" ]
|
||||
## }
|
||||
##
|
||||
check_minimum_version() {
|
||||
IFS='.' read -r -a varray1 <<<"$1"
|
||||
IFS='.' read -r -a varray2 <<<"$2"
|
||||
|
||||
for i in "${!varray1[@]}"; do
|
||||
if [[ ${varray1[i]} -lt ${varray2[i]} ]]; then
|
||||
return 0
|
||||
elif [[ ${varray1[i]} -gt ${varray2[i]} ]]; then
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
assert_is_supported_arch() {
|
||||
case "${ARCH}" in
|
||||
x86_64 | amd64 | aarch64 | ppc64le | arm* | s390x | loong64 | loongarch64 | riscv64)
|
||||
return
|
||||
;;
|
||||
*)
|
||||
echo "Arch '${ARCH}' is not supported. Supported Arch: [x86_64, amd64, aarch64, ppc64le, arm*, s390x, loong64, loongarch64, riscv64]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
assert_is_supported_os() {
|
||||
case "${KNAME}" in
|
||||
Linux | FreeBSD | OpenBSD | NetBSD | DragonFly | SunOS)
|
||||
return
|
||||
;;
|
||||
Darwin)
|
||||
osx_host_version=$(env sw_vers -productVersion)
|
||||
if ! check_minimum_version "${OSX_VERSION}" "${osx_host_version}"; then
|
||||
echo "OSX version '${osx_host_version}' is not supported. Minimum supported version: ${OSX_VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
return
|
||||
;;
|
||||
*)
|
||||
echo "OS '${KNAME}' is not supported. Supported OS: [Linux, FreeBSD, OpenBSD, NetBSD, Darwin, DragonFly]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
assert_check_golang_env() {
|
||||
if ! which go >/dev/null 2>&1; then
|
||||
echo "Cannot find go binary in your PATH configuration, please refer to Go installation document at https://golang.org/doc/install"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
installed_go_version=$(go version | sed 's/^.* go\([0-9.]*\).*$/\1/')
|
||||
if ! check_minimum_version "${GO_VERSION}" "${installed_go_version}"; then
|
||||
echo "Go runtime version '${installed_go_version}' is unsupported. Minimum supported version: ${GO_VERSION} to compile."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_check_deps() {
|
||||
# support unusual Git versions such as: 2.7.4 (Apple Git-66)
|
||||
installed_git_version=$(git version | perl -ne '$_ =~ m/git version (.*?)( |$)/; print "$1\n";')
|
||||
if ! check_minimum_version "${GIT_VERSION}" "${installed_git_version}"; then
|
||||
echo "Git version '${installed_git_version}' is not supported. Minimum supported version: ${GIT_VERSION}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
## Check for supported arch
|
||||
assert_is_supported_arch
|
||||
|
||||
## Check for supported os
|
||||
assert_is_supported_os
|
||||
|
||||
## Check for Go environment
|
||||
assert_check_golang_env
|
||||
|
||||
## Check for dependencies
|
||||
assert_check_deps
|
||||
}
|
||||
|
||||
_init && main "$@"
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -1,40 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
# Enable tracing if set.
|
||||
[ -n "$BASH_XTRACEFD" ] && set -x
|
||||
|
||||
function _init() {
|
||||
## All binaries are static make sure to disable CGO.
|
||||
export CGO_ENABLED=0
|
||||
|
||||
## Cross-compile only the OS/arch combinations we actually publish, kept in
|
||||
## sync with the goos/goarch matrix in .github/goreleaser.yml. Compile-checking
|
||||
## targets we never ship (ppc64le, s390x, mips*, riscv64, 386, arm, the BSDs)
|
||||
## spent CI minutes on unshipped code and timed the gate out on a cold cache.
|
||||
SUPPORTED_OSARCH="linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64"
|
||||
}
|
||||
|
||||
function _build() {
|
||||
local osarch=$1
|
||||
IFS=/ read -r -a arr <<<"$osarch"
|
||||
os="${arr[0]}"
|
||||
arch="${arr[1]}"
|
||||
package=$(go list -f '{{.ImportPath}}')
|
||||
printf -- "--> %15s:%s\n" "${osarch}" "${package}"
|
||||
|
||||
# go build -trimpath to build the binary.
|
||||
export GOOS=$os
|
||||
export GOARCH=$arch
|
||||
export GO111MODULE=on
|
||||
go build -trimpath -tags kqueue -o /dev/null
|
||||
}
|
||||
|
||||
function main() {
|
||||
echo "Testing builds for OS/Arch: ${SUPPORTED_OSARCH}"
|
||||
for each_osarch in ${SUPPORTED_OSARCH}; do
|
||||
_build "${each_osarch}"
|
||||
done
|
||||
}
|
||||
|
||||
_init && main "$@"
|
||||
@@ -1,123 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -x
|
||||
|
||||
export MINIO_CI_CD=1
|
||||
killall -9 silo
|
||||
|
||||
rm -rf ${HOME}/tmp/dist
|
||||
|
||||
scheme="http"
|
||||
nr_servers=4
|
||||
|
||||
addr="localhost"
|
||||
args=""
|
||||
for ((i = 0; i < $((nr_servers)); i++)); do
|
||||
args="$args $scheme://$addr:$((9100 + i))/${HOME}/tmp/dist/path1/$i"
|
||||
done
|
||||
|
||||
echo $args
|
||||
|
||||
for ((i = 0; i < $((nr_servers)); i++)); do
|
||||
(silo server --address ":$((9100 + i))" $args 2>&1 >/tmp/log$i.txt) &
|
||||
done
|
||||
|
||||
sleep 10s
|
||||
|
||||
if [ ! -f ./mc ]; then
|
||||
"$(git rev-parse --show-toplevel)/buildscripts/install-mcli.sh" ./mc
|
||||
fi
|
||||
|
||||
set +e
|
||||
|
||||
export MC_HOST_siloadm=http://minioadmin:minioadmin@localhost:9100/
|
||||
./mc ready siloadm
|
||||
|
||||
./mc ls siloadm/
|
||||
|
||||
./mc admin config set siloadm/ api root_access=off
|
||||
|
||||
sleep 3s # let things settle a little
|
||||
|
||||
./mc ls siloadm/
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "listing succeeded, 'minioadmin' was not disabled"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
killall -9 silo
|
||||
|
||||
export MINIO_API_ROOT_ACCESS=on
|
||||
for ((i = 0; i < $((nr_servers)); i++)); do
|
||||
(silo server --address ":$((9100 + i))" $args 2>&1 >/tmp/log$i.txt) &
|
||||
done
|
||||
|
||||
set +e
|
||||
|
||||
./mc ready siloadm/
|
||||
|
||||
./mc ls siloadm/
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "listing failed, 'minioadmin' should be enabled"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
killall -9 silo
|
||||
|
||||
rm -rf /tmp/multisitea/
|
||||
rm -rf /tmp/multisiteb/
|
||||
|
||||
echo "Setup site-replication and then disable root credentials"
|
||||
|
||||
silo server --address 127.0.0.1:9001 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
|
||||
"http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_1.log 2>&1 &
|
||||
silo server --address 127.0.0.1:9002 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
|
||||
"http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_2.log 2>&1 &
|
||||
|
||||
silo server --address 127.0.0.1:9003 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
|
||||
"http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_1.log 2>&1 &
|
||||
silo server --address 127.0.0.1:9004 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
|
||||
"http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_2.log 2>&1 &
|
||||
|
||||
export MC_HOST_sitea=http://minioadmin:minioadmin@127.0.0.1:9001
|
||||
export MC_HOST_siteb=http://minioadmin:minioadmin@127.0.0.1:9004
|
||||
|
||||
./mc ready sitea
|
||||
./mc ready siteb
|
||||
|
||||
./mc admin replicate add sitea siteb
|
||||
|
||||
./mc admin user add sitea foobar foo12345
|
||||
|
||||
./mc admin policy attach sitea/ consoleAdmin --user=foobar
|
||||
|
||||
./mc admin user info siteb foobar
|
||||
|
||||
killall -9 silo
|
||||
|
||||
echo "turning off root access, however site replication must continue"
|
||||
export MINIO_API_ROOT_ACCESS=off
|
||||
|
||||
silo server --address 127.0.0.1:9001 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
|
||||
"http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_1.log 2>&1 &
|
||||
silo server --address 127.0.0.1:9002 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \
|
||||
"http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_2.log 2>&1 &
|
||||
|
||||
silo server --address 127.0.0.1:9003 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
|
||||
"http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_1.log 2>&1 &
|
||||
silo server --address 127.0.0.1:9004 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \
|
||||
"http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_2.log 2>&1 &
|
||||
|
||||
export MC_HOST_sitea=http://foobar:foo12345@127.0.0.1:9001
|
||||
export MC_HOST_siteb=http://foobar:foo12345@127.0.0.1:9004
|
||||
|
||||
./mc ready sitea
|
||||
./mc ready siteb
|
||||
|
||||
./mc admin user add sitea foobar-admin foo12345
|
||||
|
||||
sleep 2s
|
||||
|
||||
./mc admin user info siteb foobar-admin
|
||||
@@ -1,125 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Regenerates CREDITS from the license text of every Go module linked into the
|
||||
# silo binary. The module set is what `go list -deps` reports for the main
|
||||
# package, so test-only and tool dependencies stay out: CREDITS documents what
|
||||
# a shipped binary actually contains. check-gen runs this and fails on a diff,
|
||||
# which keeps CREDITS from drifting when go.mod changes.
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_dir="$(cd "${script_dir}/.." && pwd)"
|
||||
cd "${repo_dir}"
|
||||
|
||||
out_file="${1:-${repo_dir}/CREDITS}"
|
||||
tmp_file="${out_file}.tmp"
|
||||
trap 'rm -f "${tmp_file}"' EXIT
|
||||
|
||||
rule_dash='----------------------------------------------------------------'
|
||||
rule_equal='================================================================'
|
||||
|
||||
# These modules repackage Go standard library code and publish no license
|
||||
# file; their source files carry the Go Authors' BSD-style header pointing at
|
||||
# the Go project license, so that text is reproduced for them.
|
||||
stdlib_derived='github.com/minio/colorjson github.com/minio/csvparser github.com/minio/filepath'
|
||||
|
||||
is_stdlib_derived() {
|
||||
case " ${stdlib_derived} " in
|
||||
*" $1 "*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Command substitution strips trailing newlines; printf adds exactly one back,
|
||||
# so every entry ends the same way regardless of how the license file ends.
|
||||
emit_text() {
|
||||
printf '%s\n' "$(cat "$1")"
|
||||
}
|
||||
|
||||
# The module cache must hold every dependency before .Dir can resolve.
|
||||
go mod download
|
||||
|
||||
# The Go project license text is taken from the pinned golang.org/x/sys module
|
||||
# rather than GOROOT: Homebrew's Go does not ship GOROOT/LICENSE, and the
|
||||
# module copy is version-locked by go.mod, so the output cannot vary with the
|
||||
# machine's toolchain packaging.
|
||||
go_license="$(go list -m -f '{{.Dir}}' golang.org/x/sys)/LICENSE"
|
||||
if [ ! -f "${go_license}" ]; then
|
||||
echo "Missing Go license text: ${go_license}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
{
|
||||
printf '%s\n' \
|
||||
'Silo bundles third-party software under the licenses reproduced below.' \
|
||||
'Generated by buildscripts/gen-credits.sh (make credits) from the Go' \
|
||||
'modules linked into the silo binary. Do not edit by hand.' \
|
||||
'' \
|
||||
"${rule_equal}" \
|
||||
''
|
||||
printf '%s\n%s\n%s\n' 'Go (the standard library)' 'https://golang.org/' "${rule_dash}"
|
||||
emit_text "${go_license}"
|
||||
printf '\n%s\n\n' "${rule_equal}"
|
||||
|
||||
# The dependency closure is GOOS/GOARCH-specific: platform-only modules
|
||||
# (darwin's go-m1cpu, windows' wmi, ...) enter and leave it with the host.
|
||||
# Pin the primary shipped target and the release build tags so regenerating
|
||||
# CREDITS produces identical output on every machine, including CI.
|
||||
GOOS=linux GOARCH=amd64 go list -deps -tags kqueue \
|
||||
-f '{{if and (not .Standard) .Module}}{{.Module.Path}}{{end}}' . \
|
||||
| LC_ALL=C sort -u \
|
||||
| grep -vx 'github.com/minio/minio' \
|
||||
| xargs go list -m -f '{{.Path}}|{{with .Replace}}{{.Path}}{{end}}|{{.Dir}}' \
|
||||
| while IFS='|' read -r path replacement dir; do
|
||||
if [ -z "${dir}" ] || [ ! -d "${dir}" ]; then
|
||||
echo "Module cache directory missing for ${path}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
name="${path}"
|
||||
url="https://${path}"
|
||||
# A same-path replace only pins a version; the annotation is for
|
||||
# dependencies actually served from a different repository.
|
||||
if [ -n "${replacement}" ] && [ "${replacement}" != "${path}" ]; then
|
||||
name="${path} (replaced by ${replacement})"
|
||||
url="https://${replacement}"
|
||||
fi
|
||||
|
||||
printf '%s\n%s\n%s\n' "${name}" "${url}" "${rule_dash}"
|
||||
|
||||
if is_stdlib_derived "${path}"; then
|
||||
printf '%s\n%s\n\n' \
|
||||
'This module repackages Go standard library code and publishes no' \
|
||||
'license file; the Go project license below applies per its file headers.'
|
||||
emit_text "${go_license}"
|
||||
else
|
||||
license_file=''
|
||||
for candidate in LICENSE LICENSE.txt LICENSE.md COPYING COPYING.txt LICENCE UNLICENSE; do
|
||||
if [ -f "${dir}/${candidate}" ]; then
|
||||
license_file="${dir}/${candidate}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "${license_file}" ]; then
|
||||
echo "No license file found for ${path} in ${dir}" >&2
|
||||
exit 1
|
||||
fi
|
||||
emit_text "${license_file}"
|
||||
fi
|
||||
|
||||
# Apache License 2.0 section 4(d) requires redistributing the NOTICE
|
||||
# file contents alongside the licensed work.
|
||||
for notice in NOTICE NOTICE.txt; do
|
||||
if [ -f "${dir}/${notice}" ]; then
|
||||
printf '\n%s\n\n' 'Bundled NOTICE file:'
|
||||
emit_text "${dir}/${notice}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
printf '\n%s\n\n' "${rule_equal}"
|
||||
done
|
||||
} > "${tmp_file}"
|
||||
|
||||
mv "${tmp_file}" "${out_file}"
|
||||
@@ -1,125 +0,0 @@
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func genLDFlags(version string) string {
|
||||
releaseTag, date := releaseTag(version)
|
||||
copyrightYear := strconv.Itoa(date.Year())
|
||||
ldflagsStr := "-s -w"
|
||||
ldflagsStr += " -X github.com/minio/minio/cmd.Version=" + version
|
||||
ldflagsStr += " -X github.com/minio/minio/cmd.CopyrightYear=" + copyrightYear
|
||||
ldflagsStr += " -X github.com/minio/minio/cmd.ReleaseTag=" + releaseTag
|
||||
ldflagsStr += " -X github.com/minio/minio/cmd.CommitID=" + commitID()
|
||||
ldflagsStr += " -X github.com/minio/minio/cmd.ShortCommitID=" + commitID()[:12]
|
||||
// GOPATH/GOROOT are deliberately not stamped in. They only seed the logger's
|
||||
// source-path trim list, which -trimpath already makes moot (paths are
|
||||
// relative in the binary, so there is no build-machine prefix left to trim),
|
||||
// and stamping them baked the builder's absolute paths into the released
|
||||
// binary - defeating -trimpath and reproducible builds. cmd.GOPATH/GOROOT
|
||||
// keep their empty defaults, exactly as a plain `go build` leaves them.
|
||||
return ldflagsStr
|
||||
}
|
||||
|
||||
// genReleaseTag prints release tag to the console for easy git tagging.
|
||||
func releaseTag(version string) (string, time.Time) {
|
||||
relPrefix := "DEVELOPMENT"
|
||||
if prefix := os.Getenv("MINIO_RELEASE"); prefix != "" {
|
||||
relPrefix = prefix
|
||||
}
|
||||
|
||||
relSuffix := ""
|
||||
if hotfix := os.Getenv("MINIO_HOTFIX"); hotfix != "" {
|
||||
relSuffix = hotfix
|
||||
}
|
||||
|
||||
relTag := strings.Replace(version, " ", "-", -1)
|
||||
relTag = strings.Replace(relTag, ":", "-", -1)
|
||||
t, err := time.Parse("2006-01-02T15-04-05Z", relTag)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
relTag = strings.Replace(relTag, ",", "", -1)
|
||||
relTag = relPrefix + "." + relTag
|
||||
if relSuffix != "" {
|
||||
relTag += "." + relSuffix
|
||||
}
|
||||
|
||||
return relTag, t
|
||||
}
|
||||
|
||||
// commitID returns the abbreviated commit-id hash of the last commit.
|
||||
func commitID() string {
|
||||
// git log --format="%H" -n1
|
||||
var (
|
||||
commit []byte
|
||||
err error
|
||||
)
|
||||
cmdName := "git"
|
||||
cmdArgs := []string{"log", "--format=%H", "-n1"}
|
||||
if commit, err = exec.Command(cmdName, cmdArgs...).Output(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error generating git commit-id: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(commit))
|
||||
}
|
||||
|
||||
func commitTime() time.Time {
|
||||
// git log --format=%cD -n1
|
||||
var (
|
||||
commitUnix []byte
|
||||
err error
|
||||
)
|
||||
cmdName := "git"
|
||||
cmdArgs := []string{"log", "--format=%cI", "-n1"}
|
||||
if commitUnix, err = exec.Command(cmdName, cmdArgs...).Output(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error generating git commit-time: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
t, err := time.Parse(time.RFC3339, strings.TrimSpace(string(commitUnix)))
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error generating git commit-time: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return t.UTC()
|
||||
}
|
||||
|
||||
func main() {
|
||||
var version string
|
||||
if len(os.Args) > 1 {
|
||||
version = os.Args[1]
|
||||
} else {
|
||||
version = commitTime().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
fmt.Println(genLDFlags(version))
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
set -E
|
||||
set -o pipefail
|
||||
set -x
|
||||
|
||||
WORK_DIR="$PWD/.verify-$RANDOM"
|
||||
SILO_CONFIG_DIR="$WORK_DIR/.silo"
|
||||
SILO=("$PWD/silo" --config-dir "$SILO_CONFIG_DIR" server)
|
||||
|
||||
if [ ! -x "$PWD/silo" ]; then
|
||||
echo "Silo executable binary not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x "$PWD/silo" ]; then
|
||||
echo "Silo executable binary not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function start_silo_4drive() {
|
||||
start_port=$1
|
||||
|
||||
export MINIO_ROOT_USER=silo
|
||||
export MINIO_ROOT_PASSWORD=silo1234
|
||||
export MC_HOST_silo="http://silo:silo1234@127.0.0.1:${start_port}/"
|
||||
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
|
||||
export MINIO_CI_CD=1
|
||||
|
||||
mkdir ${WORK_DIR}
|
||||
if [ ! -x "$PWD/mc" ]; then
|
||||
"$(git rev-parse --show-toplevel)/buildscripts/install-mcli.sh" "$PWD/mc"
|
||||
fi
|
||||
|
||||
"${SILO[@]}" --address ":$start_port" "${WORK_DIR}/disk{1...4}" >"${WORK_DIR}/server1.log" 2>&1 &
|
||||
pid=$!
|
||||
disown $pid
|
||||
sleep 5
|
||||
|
||||
if ! ps -p ${pid} 1>&2 >/dev/null; then
|
||||
echo "server1 log:"
|
||||
cat "${WORK_DIR}/server1.log"
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"${PWD}/mc" mb --with-versioning silo/bucket
|
||||
|
||||
for i in $(seq 1 4); do
|
||||
"${PWD}/mc" cp /etc/hosts silo/bucket/testobj
|
||||
|
||||
sudo chown -R root. "${WORK_DIR}/disk${i}"
|
||||
|
||||
"${PWD}/mc" cp /etc/hosts silo/bucket/testobj
|
||||
|
||||
sudo chown -R ${USER}. "${WORK_DIR}/disk${i}"
|
||||
done
|
||||
|
||||
for vid in $("${PWD}/mc" ls --json --versions silo/bucket/testobj | jq -r .versionId); do
|
||||
"${PWD}/mc" cat --vid "${vid}" silo/bucket/testobj | md5sum
|
||||
done
|
||||
|
||||
pkill silo
|
||||
sleep 3
|
||||
}
|
||||
|
||||
function main() {
|
||||
start_port=$(shuf -i 10000-65000 -n 1)
|
||||
|
||||
start_silo_4drive ${start_port}
|
||||
}
|
||||
|
||||
function purge() {
|
||||
rm -rf "$1"
|
||||
}
|
||||
|
||||
(main "$@")
|
||||
rv=$?
|
||||
purge "$WORK_DIR"
|
||||
exit "$rv"
|
||||
@@ -1,86 +0,0 @@
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
//
|
||||
// MinIO Object Storage (c) 2022 MinIO, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Note: YOUR-ACCESSKEYID, YOUR-SECRETACCESSKEY are
|
||||
// dummy values, please replace them with original values.
|
||||
|
||||
// API requests are secure (HTTPS) if secure=true and insecure (HTTP) otherwise.
|
||||
// New returns an MinIO Admin client object.
|
||||
madmClnt, err := madmin.New(os.Args[1], os.Args[2], os.Args[3], false)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
opts := madmin.HealOpts{
|
||||
Recursive: true, // recursively heal all objects at 'prefix'
|
||||
Remove: true, // remove content that has lost quorum and not recoverable
|
||||
ScanMode: madmin.HealNormalScan, // by default do not do 'deep' scanning
|
||||
}
|
||||
|
||||
start, _, err := madmClnt.Heal(context.Background(), "healing-rewrite-bucket", "", opts, "", false, false)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
fmt.Println("Healstart sequence ===")
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
if err = enc.Encode(&start); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
for {
|
||||
_, status, err := madmClnt.Heal(context.Background(), "healing-rewrite-bucket", "", opts, start.ClientToken, false, false)
|
||||
if status.Summary == "finished" {
|
||||
fmt.Println("Healstatus on items ===")
|
||||
for _, item := range status.Items {
|
||||
if err = enc.Encode(&item); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
if status.Summary == "stopped" {
|
||||
fmt.Println("Healstatus on items ===")
|
||||
fmt.Println("Heal failed with", status.FailureDetail)
|
||||
break
|
||||
}
|
||||
|
||||
for _, item := range status.Items {
|
||||
if err = enc.Encode(&item); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
@@ -1,331 +0,0 @@
|
||||
// Copyright 2026 PGSTY contributors.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// helm-migration-guard compares a rendered legacy MinIO chart with the Silo
|
||||
// upgrade candidate. Product labels, images, and commands may change; resource
|
||||
// identity, selectors, PVCs, storage mounts, ports, secrets, and service-account
|
||||
// references must not.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
type resource struct {
|
||||
key string
|
||||
doc map[string]any
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 3 {
|
||||
fatal(errors.New("usage: helm-migration-guard OLD_RENDER NEW_RENDER"))
|
||||
}
|
||||
oldResources, err := readResources(os.Args[1])
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
newResources, err := readResources(os.Args[2])
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
if err := compare(oldResources, newResources); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
fmt.Printf("Silo Helm migration identity is stable across %d rendered resources\n", len(oldResources))
|
||||
}
|
||||
|
||||
func readResources(path string) (map[string]resource, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open %s: %w", path, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
resources := make(map[string]resource)
|
||||
decoder := yaml.NewDecoder(file)
|
||||
for document := 1; ; document++ {
|
||||
var doc map[string]any
|
||||
err = decoder.Decode(&doc)
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode %s document %d: %w", path, document, err)
|
||||
}
|
||||
if len(doc) == 0 || text(doc["kind"]) == "" {
|
||||
continue
|
||||
}
|
||||
metadata := object(doc["metadata"])
|
||||
key := strings.Join([]string{text(doc["kind"]), text(metadata["namespace"]), text(metadata["name"])}, "/")
|
||||
if _, exists := resources[key]; exists {
|
||||
return nil, fmt.Errorf("%s contains duplicate resource %s", path, key)
|
||||
}
|
||||
resources[key] = resource{key: key, doc: doc}
|
||||
}
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
func compare(oldResources, newResources map[string]resource) error {
|
||||
for key := range oldResources {
|
||||
if _, ok := newResources[key]; !ok {
|
||||
return fmt.Errorf("legacy resource would be removed or renamed: %s", key)
|
||||
}
|
||||
}
|
||||
for key := range newResources {
|
||||
if _, ok := oldResources[key]; !ok {
|
||||
return fmt.Errorf("upgrade candidate unexpectedly adds a resource: %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
keys := make([]string, 0, len(oldResources))
|
||||
for key := range oldResources {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, key := range keys {
|
||||
oldDoc := oldResources[key].doc
|
||||
newDoc := newResources[key].doc
|
||||
kind := text(oldDoc["kind"])
|
||||
switch kind {
|
||||
case "Service":
|
||||
if err := same(key, "Service selector", at(oldDoc, "spec", "selector"), at(newDoc, "spec", "selector")); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := same(key, "Service ports", at(oldDoc, "spec", "ports"), at(newDoc, "spec", "ports")); err != nil {
|
||||
return err
|
||||
}
|
||||
case "Deployment", "StatefulSet":
|
||||
if err := same(key, "workload selector", at(oldDoc, "spec", "selector"), at(newDoc, "spec", "selector")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if kind == "StatefulSet" {
|
||||
if err := same(key, "StatefulSet serviceName", at(oldDoc, "spec", "serviceName"), at(newDoc, "spec", "serviceName")); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := same(key, "volume claim templates", claimTemplates(oldDoc), claimTemplates(newDoc)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if kind == "PersistentVolumeClaim" {
|
||||
if err := same(key, "PVC specification", at(oldDoc, "spec"), at(newDoc, "spec")); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if kind == "Secret" {
|
||||
if err := same(key, "Secret keys", secretKeys(oldDoc), secretKeys(newDoc)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if kind == "Deployment" || kind == "StatefulSet" || kind == "Job" {
|
||||
if err := comparePod(key, kind, oldDoc, newDoc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func comparePod(key, kind string, oldDoc, newDoc map[string]any) error {
|
||||
oldPod := object(at(oldDoc, "spec", "template", "spec"))
|
||||
newPod := object(at(newDoc, "spec", "template", "spec"))
|
||||
if err := same(key, "service account", oldPod["serviceAccountName"], newPod["serviceAccountName"]); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := same(key, "referenced volume sources", volumeSources(oldPod), volumeSources(newPod)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oldContainers := containers(oldPod)
|
||||
newContainers := containers(newPod)
|
||||
if err := same(key, "container identities", sortedKeys(oldContainers), sortedKeys(newContainers)); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, name := range sortedKeys(oldContainers) {
|
||||
oldContainer := oldContainers[name]
|
||||
newContainer := newContainers[name]
|
||||
if err := same(key, name+" ports", oldContainer["ports"], newContainer["ports"]); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := same(key, name+" environment", oldContainer["env"], newContainer["env"]); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := same(key, name+" envFrom", oldContainer["envFrom"], newContainer["envFrom"]); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := same(key, name+" storage mounts", normalizedMounts(oldContainer, oldPod), normalizedMounts(newContainer, newPod)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
image := text(newContainer["image"])
|
||||
if strings.HasPrefix(image, "pgsty/minio:") || strings.HasPrefix(image, "docker.io/pgsty/minio:") {
|
||||
return fmt.Errorf("%s container %s still uses frozen image %s", key, name, image)
|
||||
}
|
||||
command := commandText(newContainer)
|
||||
if strings.Contains(command, "/usr/bin/minio") {
|
||||
return fmt.Errorf("%s container %s still invokes /usr/bin/minio", key, name)
|
||||
}
|
||||
if (kind == "Deployment" || kind == "StatefulSet") && strings.Contains(image, "pgsty/silo:") {
|
||||
if !strings.Contains(command, "silo") || !strings.Contains(command, "server") {
|
||||
return fmt.Errorf("%s container %s does not invoke the Silo server: %q", key, name, command)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func claimTemplates(doc map[string]any) []string {
|
||||
var result []string
|
||||
for _, raw := range list(at(doc, "spec", "volumeClaimTemplates")) {
|
||||
claim := object(raw)
|
||||
metadata := object(claim["metadata"])
|
||||
result = append(result, text(metadata["name"])+"="+canonical(claim["spec"]))
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func secretKeys(doc map[string]any) []string {
|
||||
var result []string
|
||||
for _, section := range []string{"data", "stringData"} {
|
||||
for key := range object(doc[section]) {
|
||||
result = append(result, section+":"+key)
|
||||
}
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func volumeSources(pod map[string]any) []string {
|
||||
var result []string
|
||||
for _, raw := range list(pod["volumes"]) {
|
||||
volume := cloneObject(object(raw))
|
||||
delete(volume, "name")
|
||||
result = append(result, canonical(volume))
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func volumeSourceByName(pod map[string]any) map[string]string {
|
||||
result := make(map[string]string)
|
||||
for _, raw := range list(pod["volumes"]) {
|
||||
volume := cloneObject(object(raw))
|
||||
name := text(volume["name"])
|
||||
delete(volume, "name")
|
||||
result[name] = canonical(volume)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizedMounts(container, pod map[string]any) []string {
|
||||
sources := volumeSourceByName(pod)
|
||||
var result []string
|
||||
for _, raw := range list(container["volumeMounts"]) {
|
||||
mount := cloneObject(object(raw))
|
||||
name := text(mount["name"])
|
||||
delete(mount, "name")
|
||||
mount["source"] = sources[name]
|
||||
result = append(result, canonical(mount))
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func containers(pod map[string]any) map[string]map[string]any {
|
||||
result := make(map[string]map[string]any)
|
||||
for _, section := range []string{"initContainers", "containers"} {
|
||||
for _, raw := range list(pod[section]) {
|
||||
container := object(raw)
|
||||
result[section+":"+text(container["name"])] = container
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func commandText(container map[string]any) string {
|
||||
var parts []string
|
||||
for _, field := range []string{"command", "args"} {
|
||||
for _, value := range list(container[field]) {
|
||||
parts = append(parts, text(value))
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
func at(root map[string]any, path ...string) any {
|
||||
var current any = root
|
||||
for _, part := range path {
|
||||
current = object(current)[part]
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
func object(value any) map[string]any {
|
||||
if value == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
result, _ := value.(map[string]any)
|
||||
return result
|
||||
}
|
||||
|
||||
func cloneObject(value map[string]any) map[string]any {
|
||||
result := make(map[string]any, len(value))
|
||||
for key, item := range value {
|
||||
result[key] = item
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func list(value any) []any {
|
||||
result, _ := value.([]any)
|
||||
return result
|
||||
}
|
||||
|
||||
func text(value any) string {
|
||||
result, _ := value.(string)
|
||||
return result
|
||||
}
|
||||
|
||||
func sortedKeys[T any](values map[string]T) []string {
|
||||
result := make([]string, 0, len(values))
|
||||
for key := range values {
|
||||
result = append(result, key)
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func same(resourceKey, field string, oldValue, newValue any) error {
|
||||
oldCanonical := canonical(oldValue)
|
||||
newCanonical := canonical(newValue)
|
||||
if oldCanonical != newCanonical {
|
||||
return fmt.Errorf("%s changes %s\nold: %s\nnew: %s", resourceKey, field, oldCanonical, newCanonical)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func canonical(value any) string {
|
||||
data, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("<unmarshalable %T: %v>", value, err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func fatal(err error) {
|
||||
fmt.Fprintf(os.Stderr, "Silo Helm migration check failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "usage: $0 TARGET" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
target=$1
|
||||
target_dir=$(dirname "${target}")
|
||||
if [ ! -d "${target_dir}" ]; then
|
||||
echo "target directory does not exist: ${target_dir}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sha256_file() {
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "$1" | awk '{print $1}'
|
||||
else
|
||||
shasum -a 256 "$1" | awk '{print $1}'
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -n "${MCLI_BIN:-}" ]; then
|
||||
if [ ! -f "${MCLI_BIN}" ]; then
|
||||
echo "MCLI_BIN is not a regular file: ${MCLI_BIN}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! printf '%s\n' "${MCLI_SHA256:-}" | grep -Eq '^[0-9a-fA-F]{64}$'; then
|
||||
echo "MCLI_SHA256 must contain the expected SHA-256 for MCLI_BIN" >&2
|
||||
exit 1
|
||||
fi
|
||||
actual=$(sha256_file "${MCLI_BIN}")
|
||||
if [ "${actual}" != "${MCLI_SHA256,,}" ]; then
|
||||
echo "MCLI_BIN checksum mismatch: expected ${MCLI_SHA256,,}, got ${actual}" >&2
|
||||
exit 1
|
||||
fi
|
||||
install -m 0755 "${MCLI_BIN}" "${target}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
release=${MCLI_RELEASE:-RELEASE.2026-09-03T07-13-05Z}
|
||||
version_hyphen=${release#RELEASE.}
|
||||
package_version=$(printf '%s\n' "${version_hyphen}" | sed -E 's/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2})-([0-9]{2})-([0-9]{2})Z$/\1\2\3\4\5\6.0.0/')
|
||||
if [ "${package_version}" = "${version_hyphen}" ]; then
|
||||
echo "invalid MCLI_RELEASE: ${release}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case $(uname -s) in
|
||||
Linux) os=linux ;;
|
||||
Darwin) os=darwin ;;
|
||||
*) echo "unsupported mcli host OS: $(uname -s)" >&2; exit 1 ;;
|
||||
esac
|
||||
case $(uname -m) in
|
||||
x86_64 | amd64) arch=amd64 ;;
|
||||
aarch64 | arm64) arch=arm64 ;;
|
||||
*) echo "unsupported mcli host architecture: $(uname -m)" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
archive="mcli_${package_version}_${os}_${arch}.tar.gz"
|
||||
checksums="mcli_${package_version}_checksums.txt"
|
||||
base_url="https://github.com/pgsty/mc/releases/download/${release}"
|
||||
tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/silo-mcli.XXXXXX")
|
||||
trap 'rm -rf "${tmp_dir}"' EXIT
|
||||
|
||||
curl --fail --location --retry 3 --silent --show-error \
|
||||
"${base_url}/${checksums}" --output "${tmp_dir}/${checksums}"
|
||||
curl --fail --location --retry 3 --silent --show-error \
|
||||
"${base_url}/${archive}" --output "${tmp_dir}/${archive}"
|
||||
|
||||
expected=$(awk -v asset="${archive}" '
|
||||
{
|
||||
name=$2
|
||||
sub(/^\*/, "", name)
|
||||
if (name == asset && length($1) == 64 && $1 ~ /^[0-9a-fA-F]+$/) print tolower($1)
|
||||
}
|
||||
' "${tmp_dir}/${checksums}")
|
||||
if ! printf '%s\n' "${expected}" | grep -Eq '^[0-9a-f]{64}$'; then
|
||||
echo "checksum manifest does not contain exactly one valid entry for ${archive}" >&2
|
||||
exit 1
|
||||
fi
|
||||
actual=$(sha256_file "${tmp_dir}/${archive}")
|
||||
if [ "${actual}" != "${expected}" ]; then
|
||||
echo "downloaded ${archive} checksum mismatch" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tar -xzf "${tmp_dir}/${archive}" -C "${tmp_dir}" mcli
|
||||
install -m 0755 "${tmp_dir}/mcli" "${target}"
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$#" -ne 3 ]; then
|
||||
echo "usage: $0 SOURCE SHA256 TARGET" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
source_ref=$1
|
||||
expected=${2,,}
|
||||
target=$3
|
||||
if ! printf '%s\n' "${expected}" | grep -Eq '^[0-9a-f]{64}$'; then
|
||||
echo "expected checksum must be a lowercase SHA-256 digest" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -d "$(dirname "${target}")" ]; then
|
||||
echo "target directory does not exist: $(dirname "${target}")" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmp_file=$(mktemp "${TMPDIR:-/tmp}/silo-fixture.XXXXXX")
|
||||
trap 'rm -f "${tmp_file}"' EXIT
|
||||
case ${source_ref} in
|
||||
https://*)
|
||||
curl --fail --location --retry 3 --silent --show-error \
|
||||
"${source_ref}" --output "${tmp_file}"
|
||||
;;
|
||||
*)
|
||||
if [ ! -f "${source_ref}" ]; then
|
||||
echo "fixture is not a regular file: ${source_ref}" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "${source_ref}" "${tmp_file}"
|
||||
;;
|
||||
esac
|
||||
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
actual=$(sha256sum "${tmp_file}" | awk '{print $1}')
|
||||
else
|
||||
actual=$(shasum -a 256 "${tmp_file}" | awk '{print $1}')
|
||||
fi
|
||||
if [ "${actual}" != "${expected}" ]; then
|
||||
echo "fixture checksum mismatch: expected ${expected}, got ${actual}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install -m 0755 "${tmp_file}" "${target}"
|
||||
@@ -1,127 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script tests IAM migration from an old MinIO compatibility fixture into
|
||||
# the current Silo server.
|
||||
#
|
||||
# To run it locally, start the LDAP server in github.com/minio/minio-iam-testing
|
||||
# repo (e.g. make podman-run), and then run this script.
|
||||
#
|
||||
# This script assumes that LDAP server is at:
|
||||
#
|
||||
# `localhost:389`
|
||||
#
|
||||
# if this is not the case, set the environment variable
|
||||
# `_MINIO_LDAP_TEST_SERVER`.
|
||||
|
||||
OLD_VERSION=RELEASE.2024-03-26T22-10-45Z
|
||||
OLD_BINARY_LINK=https://dl.min.io/server/minio/release/linux-amd64/archive/minio.${OLD_VERSION}
|
||||
OLD_BINARY_SHA256=2050199d89e3057571620a1d453118fed5bd2de9d4f3b266b11365fdf984d676
|
||||
|
||||
__init__() {
|
||||
if which curl &>/dev/null; then
|
||||
echo "curl is already installed"
|
||||
else
|
||||
echo "Installing curl:"
|
||||
sudo apt install curl -y
|
||||
fi
|
||||
|
||||
export GOPATH=/tmp/gopath
|
||||
export PATH="${PATH}":"${GOPATH}"/bin
|
||||
|
||||
if [ ! -x "${GOPATH}/bin/mc" ]; then
|
||||
echo "Installing verified compatible client fixture"
|
||||
mkdir -p "${GOPATH}/bin"
|
||||
"$(git rev-parse --show-toplevel)/buildscripts/install-mcli.sh" "${GOPATH}/bin/mc"
|
||||
fi
|
||||
|
||||
if [ ! -x ./minio.${OLD_VERSION} ]; then
|
||||
echo "Installing verified upstream compatibility fixture minio.${OLD_VERSION}"
|
||||
"$(git rev-parse --show-toplevel)/buildscripts/install-verified-fixture.sh" \
|
||||
"${OLD_BINARY_LINK}" "${OLD_BINARY_SHA256}" "minio.${OLD_VERSION}"
|
||||
fi
|
||||
|
||||
if [ -z "$_MINIO_LDAP_TEST_SERVER" ]; then
|
||||
export _MINIO_LDAP_TEST_SERVER=localhost:389
|
||||
echo "Using default LDAP endpoint: $_MINIO_LDAP_TEST_SERVER"
|
||||
fi
|
||||
|
||||
rm -rf /tmp/data
|
||||
}
|
||||
|
||||
create_iam_content_in_old_minio() {
|
||||
echo "Creating IAM content in the old MinIO compatibility fixture."
|
||||
|
||||
MINIO_CI_CD=1 ./minio.${OLD_VERSION} server /tmp/data/{1...4} &
|
||||
sleep 5
|
||||
|
||||
set -x
|
||||
mc alias set old-minio http://localhost:9000 minioadmin minioadmin
|
||||
mc ready old-minio
|
||||
mc idp ldap add old-minio \
|
||||
server_addr=localhost:389 \
|
||||
server_insecure=on \
|
||||
lookup_bind_dn=cn=admin,dc=min,dc=io \
|
||||
lookup_bind_password=admin \
|
||||
user_dn_search_base_dn=dc=min,dc=io \
|
||||
user_dn_search_filter="(uid=%s)" \
|
||||
group_search_base_dn=ou=swengg,dc=min,dc=io \
|
||||
group_search_filter="(&(objectclass=groupOfNames)(member=%d))"
|
||||
mc admin service restart old-minio
|
||||
|
||||
mc idp ldap policy attach old-minio readwrite --user=UID=dillon,ou=people,ou=swengg,dc=min,dc=io
|
||||
mc idp ldap policy attach old-minio readwrite --group=CN=project.c,ou=groups,ou=swengg,dc=min,dc=io
|
||||
|
||||
mc idp ldap policy entities old-minio
|
||||
|
||||
mc admin cluster iam export old-minio
|
||||
set +x
|
||||
|
||||
mc admin service stop old-minio
|
||||
}
|
||||
|
||||
import_iam_content_in_new_minio() {
|
||||
echo "Importing IAM content into the current Silo instance."
|
||||
# Assume the current Silo binary exists.
|
||||
MINIO_CI_CD=1 ./silo server /tmp/data/{1...4} &
|
||||
sleep 5
|
||||
|
||||
set -x
|
||||
mc alias set new-minio http://localhost:9000 minioadmin minioadmin
|
||||
echo "BEFORE IMPORT mappings:"
|
||||
mc ready new-minio
|
||||
mc idp ldap policy entities new-minio
|
||||
mc admin cluster iam import new-minio ./old-minio-iam-info.zip
|
||||
echo "AFTER IMPORT mappings:"
|
||||
mc idp ldap policy entities new-minio
|
||||
set +x
|
||||
|
||||
# mc admin service stop new-minio
|
||||
}
|
||||
|
||||
verify_iam_content_in_new_minio() {
|
||||
output=$(mc idp ldap policy entities new-minio --json)
|
||||
|
||||
groups=$(echo "$output" | jq -r '.result.policyMappings[] | select(.policy == "readwrite") | .groups[]')
|
||||
if [ "$groups" != "cn=project.c,ou=groups,ou=swengg,dc=min,dc=io" ]; then
|
||||
echo "Failed to verify groups: $groups"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
users=$(echo "$output" | jq -r '.result.policyMappings[] | select(.policy == "readwrite") | .users[]')
|
||||
if [ "$users" != "uid=dillon,ou=people,ou=swengg,dc=min,dc=io" ]; then
|
||||
echo "Failed to verify users: $users"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mc admin service stop new-minio
|
||||
}
|
||||
|
||||
main() {
|
||||
create_iam_content_in_old_minio
|
||||
|
||||
import_iam_content_in_new_minio
|
||||
|
||||
verify_iam_content_in_new_minio
|
||||
}
|
||||
|
||||
(__init__ "$@" && main "$@")
|
||||
@@ -1,124 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Exercise both directions of the on-disk compatibility contract using an
|
||||
# immutable pre-rebrand image and a container built from the current checkout.
|
||||
# Every Docker resource is uniquely named and removed explicitly; this test
|
||||
# never prunes unrelated images, containers, networks, or volumes.
|
||||
|
||||
repo_dir="$(git rev-parse --show-toplevel)"
|
||||
old_image="${OLD_IMAGE:-docker.io/pgsty/minio@sha256:b6bfe7239bfc83fb90d31612d9704d86039dd714f7904b3f1ad68f211e602372}"
|
||||
new_image="${NEW_IMAGE:-silo-upgrade-test:dev}"
|
||||
suffix="$(date +%s)-$$"
|
||||
network="silo-upgrade-net-${suffix}"
|
||||
volume="silo-upgrade-data-${suffix}"
|
||||
old_container="silo-upgrade-old-${suffix}"
|
||||
new_container="silo-upgrade-new-${suffix}"
|
||||
rollback_container="silo-upgrade-rollback-${suffix}"
|
||||
root_user=silo-upgrade-admin
|
||||
root_password=silo-upgrade-secret-123
|
||||
|
||||
cleanup() {
|
||||
status=$?
|
||||
trap - EXIT
|
||||
if [ "${status}" -ne 0 ]; then
|
||||
for name in "${old_container}" "${new_container}" "${rollback_container}"; do
|
||||
docker logs "${name}" 2>/dev/null | tail -n 80 >&2 || true
|
||||
done
|
||||
fi
|
||||
if [ "${KEEP_UPGRADE_TEST_RESOURCES:-0}" = 1 ]; then
|
||||
printf 'Retained Docker resources for inspection: %s %s\n' "${network}" "${volume}" >&2
|
||||
exit "${status}"
|
||||
fi
|
||||
docker rm -f "${old_container}" "${new_container}" "${rollback_container}" >/dev/null 2>&1 || true
|
||||
docker network rm "${network}" >/dev/null 2>&1 || true
|
||||
docker volume rm "${volume}" >/dev/null 2>&1 || true
|
||||
exit "${status}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 130' INT TERM
|
||||
|
||||
wait_ready() {
|
||||
name="$1"
|
||||
ready=""
|
||||
for _ in $(seq 1 90); do
|
||||
if docker logs "${name}" 2>&1 | grep -q 'API:'; then
|
||||
ready=1
|
||||
break
|
||||
fi
|
||||
if [ "$(docker inspect -f '{{.State.Running}}' "${name}")" != true ]; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [ -z "${ready}" ]; then
|
||||
echo "Server did not become ready: ${name}" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
start_server() {
|
||||
name="$1"
|
||||
image="$2"
|
||||
shift 2
|
||||
docker run -d --name "${name}" --network "${network}" \
|
||||
--mount "source=${volume},target=/data" \
|
||||
-e MINIO_CI_CD=1 -e MINIO_ROOT_USER="${root_user}" -e MINIO_ROOT_PASSWORD="${root_password}" \
|
||||
"${image}" "$@" >/dev/null
|
||||
wait_ready "${name}"
|
||||
docker exec "${name}" mcli alias set local http://127.0.0.1:9000 "${root_user}" "${root_password}" >/dev/null
|
||||
}
|
||||
|
||||
stop_server() {
|
||||
name="$1"
|
||||
docker stop -t 20 "${name}" >/dev/null
|
||||
test "$(docker inspect -f '{{.State.ExitCode}}' "${name}")" = 0
|
||||
docker logs "${name}" 2>&1 | grep -q 'Exiting on signal'
|
||||
docker rm "${name}" >/dev/null
|
||||
}
|
||||
|
||||
command -v docker >/dev/null
|
||||
docker info >/dev/null
|
||||
if ! docker image inspect "${old_image}" >/dev/null 2>&1; then
|
||||
docker pull "${old_image}"
|
||||
fi
|
||||
|
||||
if [ "${SILO_UPGRADE_SKIP_BUILD:-0}" != 1 ]; then
|
||||
make -C "${repo_dir}" docker TAG="${new_image}"
|
||||
fi
|
||||
docker image inspect "${new_image}" >/dev/null
|
||||
|
||||
docker network create "${network}" >/dev/null
|
||||
docker volume create "${volume}" >/dev/null
|
||||
|
||||
start_server "${old_container}" "${old_image}" minio server /data --address :9000
|
||||
docker exec "${old_container}" mcli mb local/compat >/dev/null
|
||||
docker exec "${old_container}" mcli version enable local/compat >/dev/null
|
||||
printf 'old-version-1\n' | docker exec -i "${old_container}" mcli pipe local/compat/versioned.txt >/dev/null
|
||||
printf 'old-version-2\n' | docker exec -i "${old_container}" mcli pipe local/compat/versioned.txt >/dev/null
|
||||
test "$(docker exec "${old_container}" mcli ls --versions local/compat/versioned.txt | grep -c 'versioned.txt')" -ge 2
|
||||
docker exec "${old_container}" mcli mb --with-lock local/locked >/dev/null
|
||||
printf 'locked-by-old\n' | docker exec -i "${old_container}" mcli pipe local/locked/object.txt >/dev/null
|
||||
dd if=/dev/zero bs=1048576 count=70 2>/dev/null | docker exec -i "${old_container}" mcli pipe local/compat/multipart.bin >/dev/null
|
||||
test "$(docker exec "${old_container}" mcli stat --json local/compat/multipart.bin | jq -r '.size')" = 73400320
|
||||
docker exec "${old_container}" mcli admin user add local migration-user migration-secret-123 >/dev/null
|
||||
docker exec "${old_container}" mcli admin policy attach local readwrite --user migration-user >/dev/null
|
||||
stop_server "${old_container}"
|
||||
|
||||
start_server "${new_container}" "${new_image}" silo server /data --address :9000
|
||||
test "$(docker exec "${new_container}" mcli cat local/compat/versioned.txt)" = old-version-2
|
||||
test "$(docker exec "${new_container}" mcli cat local/locked/object.txt)" = locked-by-old
|
||||
test "$(docker exec "${new_container}" mcli stat --json local/compat/multipart.bin | jq -r '.size')" = 73400320
|
||||
docker exec "${new_container}" mcli admin user info local migration-user >/dev/null
|
||||
docker exec "${new_container}" mcli alias set migrated http://127.0.0.1:9000 migration-user migration-secret-123 >/dev/null
|
||||
printf 'written-by-silo\n' | docker exec -i "${new_container}" mcli pipe migrated/compat/silo.txt >/dev/null
|
||||
stop_server "${new_container}"
|
||||
|
||||
start_server "${rollback_container}" "${old_image}" minio server /data --address :9000
|
||||
test "$(docker exec "${rollback_container}" mcli cat local/compat/versioned.txt)" = old-version-2
|
||||
test "$(docker exec "${rollback_container}" mcli cat local/compat/silo.txt)" = written-by-silo
|
||||
test "$(docker exec "${rollback_container}" mcli stat --json local/compat/multipart.bin | jq -r '.size')" = 73400320
|
||||
stop_server "${rollback_container}"
|
||||
|
||||
echo "MinIO-to-Silo data upgrade and rollback checks passed"
|
||||
@@ -1,115 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -n "$TEST_DEBUG" ]; then
|
||||
set -x
|
||||
fi
|
||||
|
||||
WORK_DIR="$PWD/.verify-$RANDOM"
|
||||
SILO_CONFIG_DIR="$WORK_DIR/.silo"
|
||||
SILO=("$PWD/silo" --config-dir "$SILO_CONFIG_DIR" server)
|
||||
|
||||
if [ ! -x "$PWD/silo" ]; then
|
||||
echo "Silo executable binary not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x "$PWD/silo" ]; then
|
||||
echo "Silo executable binary not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
trap 'catch $LINENO' ERR
|
||||
|
||||
function purge() {
|
||||
rm -rf "$1"
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2120
|
||||
catch() {
|
||||
if [ $# -ne 0 ]; then
|
||||
echo "error on line $1"
|
||||
fi
|
||||
|
||||
echo "Cleaning up instances of Silo"
|
||||
pkill silo || true
|
||||
pkill -9 silo || true
|
||||
purge "$WORK_DIR"
|
||||
if [ $# -ne 0 ]; then
|
||||
exit $#
|
||||
fi
|
||||
}
|
||||
|
||||
catch
|
||||
|
||||
function start_silo_10drive() {
|
||||
start_port=$1
|
||||
|
||||
export MINIO_ROOT_USER=silo
|
||||
export MINIO_ROOT_PASSWORD=silo1234
|
||||
export MC_HOST_silo="http://silo:silo1234@127.0.0.1:${start_port}/"
|
||||
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
|
||||
export MINIO_CI_CD=1
|
||||
|
||||
mkdir ${WORK_DIR}
|
||||
if [ ! -x "$PWD/mc" ]; then
|
||||
"$(git rev-parse --show-toplevel)/buildscripts/install-mcli.sh" "$PWD/mc"
|
||||
fi
|
||||
|
||||
"${SILO[@]}" --address ":$start_port" "${WORK_DIR}/disk{1...10}" >"${WORK_DIR}/server1.log" 2>&1 &
|
||||
pid=$!
|
||||
disown $pid
|
||||
sleep 5
|
||||
|
||||
if ! ps -p ${pid} 1>&2 >/dev/null; then
|
||||
echo "server1 log:"
|
||||
cat "${WORK_DIR}/server1.log"
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"${PWD}/mc" mb --with-versioning silo/bucket
|
||||
|
||||
export AWS_ACCESS_KEY_ID=silo
|
||||
export AWS_SECRET_ACCESS_KEY=silo1234
|
||||
aws --endpoint-url http://localhost:"$start_port" s3api create-multipart-upload --bucket bucket --key obj-1 >upload-id.json
|
||||
uploadId=$(jq -r '.UploadId' upload-id.json)
|
||||
|
||||
truncate -s 5MiB file-5mib
|
||||
for i in {1..2}; do
|
||||
aws --endpoint-url http://localhost:"$start_port" s3api upload-part \
|
||||
--upload-id "$uploadId" --bucket bucket --key obj-1 \
|
||||
--part-number "$i" --body ./file-5mib
|
||||
done
|
||||
for i in {1..6}; do
|
||||
find ${WORK_DIR}/disk${i}/.minio.sys/multipart/ -type f -name "part.1" -delete
|
||||
done
|
||||
cat <<EOF >parts.json
|
||||
{
|
||||
"Parts": [
|
||||
{
|
||||
"PartNumber": 1,
|
||||
"ETag": "5f363e0e58a95f06cbe9bbc662c5dfb6"
|
||||
},
|
||||
{
|
||||
"PartNumber": 2,
|
||||
"ETag": "5f363e0e58a95f06cbe9bbc662c5dfb6"
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
err=$(aws --endpoint-url http://localhost:"$start_port" s3api complete-multipart-upload --upload-id "$uploadId" --bucket bucket --key obj-1 --multipart-upload file://./parts.json 2>&1)
|
||||
rv=$?
|
||||
if [ $rv -eq 0 ]; then
|
||||
echo "Failed to receive an error"
|
||||
exit 1
|
||||
fi
|
||||
echo "Received an error during complete-multipart as expected: $err"
|
||||
}
|
||||
|
||||
function main() {
|
||||
start_port=$(shuf -i 10000-65000 -n 1)
|
||||
start_silo_10drive ${start_port}
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,157 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_dir="$(cd "${script_dir}/.." && pwd)"
|
||||
dist_dir="${DIST_DIR:-${repo_dir}/dist}"
|
||||
nfpm_config="${NFPM_CONFIG:-${repo_dir}/.github/nfpm.yml}"
|
||||
|
||||
if [ -z "${PKG_VERSION:-}" ]; then
|
||||
echo "PKG_VERSION is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Re-validate the shape release.yml derived from the tag. The packages are
|
||||
# named from this, so a malformed value would ship under a name no repository
|
||||
# can order against.
|
||||
if ! [[ "${PKG_VERSION}" =~ ^[0-9]{14}\.0\.0$ ]]; then
|
||||
echo "Invalid PKG_VERSION: ${PKG_VERSION}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The PGSTY release segment, PGDG-style. sign-release-rpms.sh declares the
|
||||
# same value as expected_release, and test-release.yml asserts the two agree.
|
||||
PKG_RELEASE="1PGSTY"
|
||||
|
||||
if ! command -v nfpm >/dev/null 2>&1; then
|
||||
echo "nfpm is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "${nfpm_config}" ]; then
|
||||
echo "Missing nFPM config: ${nfpm_config}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# nfpm resolves a relative content src against the current directory, not
|
||||
# against the config file, so the unit path is passed in absolute. Otherwise
|
||||
# this only works when invoked from the repository root and fails elsewhere on
|
||||
# a message that names the file rather than the cause.
|
||||
unit_file="${repo_dir}/silo.service"
|
||||
defaults_file="${repo_dir}/silo.env"
|
||||
sysusers_file="${repo_dir}/silo.sysusers"
|
||||
license_file="${repo_dir}/LICENSE"
|
||||
notice_file="${repo_dir}/NOTICE"
|
||||
postinstall_file="${repo_dir}/buildscripts/package/postinstall.sh"
|
||||
preremove_file="${repo_dir}/buildscripts/package/preremove.sh"
|
||||
if [ ! -f "${unit_file}" ]; then
|
||||
echo "Missing systemd unit: ${unit_file}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "${defaults_file}" ]; then
|
||||
echo "Missing defaults file: ${defaults_file}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "${sysusers_file}" ]; then
|
||||
echo "Missing sysusers file: ${sysusers_file}" >&2
|
||||
exit 1
|
||||
fi
|
||||
for distributed_doc in "${license_file}" "${notice_file}"; do
|
||||
if [ ! -s "${distributed_doc}" ]; then
|
||||
echo "Missing license material: ${distributed_doc}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
for lifecycle_script in "${postinstall_file}" "${preremove_file}"; do
|
||||
if [ ! -x "${lifecycle_script}" ]; then
|
||||
echo "Missing executable package lifecycle script: ${lifecycle_script}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
packages_dir="${dist_dir}/packages"
|
||||
mkdir -p "${packages_dir}"
|
||||
|
||||
# Two spaces, no trailing newline: sign-release-rpms.sh parses these files to
|
||||
# check download integrity before it signs, and regenerates them afterwards in
|
||||
# the same shape.
|
||||
sha256_file() {
|
||||
local file="$1"
|
||||
local digest
|
||||
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
digest="$(sha256sum "${file}" | awk '{print $1}')"
|
||||
else
|
||||
digest="$(shasum -a 256 "${file}" | awk '{print $1}')"
|
||||
fi
|
||||
|
||||
printf '%s %s' "${digest}" "$(basename "${file}")" > "${file}.sha256sum"
|
||||
}
|
||||
|
||||
find_binary() {
|
||||
local goarch="$1"
|
||||
local matches
|
||||
local count
|
||||
|
||||
# Must resolve to exactly one binary. Picking the first of several build
|
||||
# variants (an added goamd64 level, a stale dist entry) would silently ship a
|
||||
# package whose contents do not match its name.
|
||||
matches="$(find "${dist_dir}" -maxdepth 2 -type f \
|
||||
-path "${dist_dir}/silo_linux_${goarch}*/silo" | sort)"
|
||||
count="$(printf '%s' "${matches}" | grep -c . || true)"
|
||||
|
||||
if [ "${count}" -eq 0 ]; then
|
||||
echo "Missing GoReleaser binary for linux/${goarch}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "${count}" -ne 1 ]; then
|
||||
echo "Expected exactly one GoReleaser binary for linux/${goarch}, found ${count}:" >&2
|
||||
printf '%s\n' "${matches}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '%s\n' "${matches}"
|
||||
}
|
||||
|
||||
build_arch() {
|
||||
local goarch="$1"
|
||||
local rpm_arch="$2"
|
||||
local deb_arch="$3"
|
||||
local apk_arch="$4"
|
||||
local source
|
||||
local rpm_file
|
||||
local deb_file
|
||||
local apk_file
|
||||
|
||||
source="$(find_binary "${goarch}")"
|
||||
|
||||
# These names are the public download names and must not drift; RPM and DEB
|
||||
# carry the PGSTY release number (nfpm renders it as the RPM Release tag and
|
||||
# as the Debian revision after a dash). APK stays bare: Alpine pkgrel only
|
||||
# admits -r<integer>, so a lettered release cannot ride along there.
|
||||
rpm_file="${packages_dir}/silo-${PKG_VERSION}-${PKG_RELEASE}.${rpm_arch}.rpm"
|
||||
deb_file="${packages_dir}/silo_${PKG_VERSION}-${PKG_RELEASE}_${deb_arch}.deb"
|
||||
apk_file="${packages_dir}/silo_${PKG_VERSION}_${apk_arch}.apk"
|
||||
|
||||
(
|
||||
cd "${repo_dir}"
|
||||
export NFPM_UNIT="${unit_file}" NFPM_DEFAULTS="${defaults_file}" NFPM_SYSUSERS="${sysusers_file}" \
|
||||
NFPM_LICENSE="${license_file}" NFPM_NOTICE="${notice_file}"
|
||||
PKG_VERSION="${PKG_VERSION}" NFPM_RELEASE="${PKG_RELEASE}" NFPM_ARCH="${goarch}" NFPM_SOURCE="${source}" \
|
||||
nfpm package --config "${nfpm_config}" --packager rpm --target "${rpm_file}"
|
||||
PKG_VERSION="${PKG_VERSION}" NFPM_RELEASE="${PKG_RELEASE}" NFPM_ARCH="${goarch}" NFPM_SOURCE="${source}" \
|
||||
nfpm package --config "${nfpm_config}" --packager deb --target "${deb_file}"
|
||||
PKG_VERSION="${PKG_VERSION}" NFPM_RELEASE='' NFPM_ARCH="${goarch}" NFPM_SOURCE="${source}" \
|
||||
nfpm package --config "${nfpm_config}" --packager apk --target "${apk_file}"
|
||||
)
|
||||
|
||||
sha256_file "${rpm_file}"
|
||||
sha256_file "${deb_file}"
|
||||
sha256_file "${apk_file}"
|
||||
}
|
||||
|
||||
build_arch amd64 x86_64 amd64 x86_64
|
||||
build_arch arm64 aarch64 arm64 aarch64
|
||||
|
||||
find "${packages_dir}" -maxdepth 1 -type f | sort
|
||||
@@ -1,172 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_dir="$(cd "${script_dir}/../.." && pwd)"
|
||||
postinstall="${script_dir}/postinstall.sh"
|
||||
preremove="${script_dir}/preremove.sh"
|
||||
test_dir="$(mktemp -d)"
|
||||
fakebin="${test_dir}/bin"
|
||||
log_file="${test_dir}/calls.log"
|
||||
useradd_shell=/usr/sbin/nologin
|
||||
[ -x "${useradd_shell}" ] || useradd_shell=/sbin/nologin
|
||||
busybox_shell=/sbin/nologin
|
||||
[ -x "${busybox_shell}" ] || busybox_shell=/bin/false
|
||||
|
||||
cleanup() {
|
||||
rm -rf "${test_dir}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "${fakebin}"
|
||||
touch "${log_file}"
|
||||
|
||||
# One dispatcher represents every external command used by the lifecycle
|
||||
# scripts. The tested scripts run with no host utilities in PATH, so a green
|
||||
# result cannot create a real account or touch the host service manager.
|
||||
cat > "${fakebin}/fake-command" <<'EOF'
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
command_name=${0##*/}
|
||||
case "${command_name}" in
|
||||
id)
|
||||
[ "${PACKAGE_TEST_USER_EXISTS:-0}" = 1 ]
|
||||
;;
|
||||
getent)
|
||||
[ "${PACKAGE_TEST_GROUP_EXISTS:-0}" = 1 ]
|
||||
;;
|
||||
systemd-sysusers|useradd|addgroup|adduser|systemctl)
|
||||
{
|
||||
printf '%s' "${command_name}"
|
||||
for argument in "$@"; do
|
||||
printf ' %s' "${argument}"
|
||||
done
|
||||
printf '\n'
|
||||
} >> "${PACKAGE_TEST_LOG}"
|
||||
;;
|
||||
*)
|
||||
echo "unexpected fake command: ${command_name}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
EOF
|
||||
chmod +x "${fakebin}/fake-command"
|
||||
|
||||
link_command() {
|
||||
ln -sf fake-command "${fakebin}/$1"
|
||||
}
|
||||
|
||||
unlink_optional_commands() {
|
||||
rm -f \
|
||||
"${fakebin}/systemd-sysusers" \
|
||||
"${fakebin}/useradd" \
|
||||
"${fakebin}/adduser" \
|
||||
"${fakebin}/addgroup"
|
||||
}
|
||||
|
||||
reset_log() {
|
||||
: > "${log_file}"
|
||||
}
|
||||
|
||||
run_postinstall() {
|
||||
PACKAGE_TEST_LOG="${log_file}" \
|
||||
PACKAGE_TEST_USER_EXISTS="${1}" \
|
||||
PACKAGE_TEST_GROUP_EXISTS="${2}" \
|
||||
PATH="${fakebin}" \
|
||||
/bin/sh "${postinstall}"
|
||||
}
|
||||
|
||||
run_preremove() {
|
||||
PACKAGE_TEST_LOG="${log_file}" PATH="${fakebin}" \
|
||||
/bin/sh "${preremove}" "$@"
|
||||
}
|
||||
|
||||
assert_log_line() {
|
||||
grep -Fx -- "$1" "${log_file}" >/dev/null
|
||||
}
|
||||
|
||||
reject_log_text() {
|
||||
if grep -F -- "$1" "${log_file}" >/dev/null; then
|
||||
echo "unexpected lifecycle call containing '$1':" >&2
|
||||
cat "${log_file}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
link_command id
|
||||
link_command getent
|
||||
link_command systemctl
|
||||
|
||||
# Clean install through systemd-sysusers. Side-by-side safety is represented
|
||||
# by the fact that the only service-manager operation is daemon-reload: no old
|
||||
# service is stopped, disabled, enabled, masked, or restarted.
|
||||
unlink_optional_commands
|
||||
link_command systemd-sysusers
|
||||
reset_log
|
||||
run_postinstall 0 0
|
||||
assert_log_line "systemd-sysusers /usr/lib/sysusers.d/silo.conf"
|
||||
assert_log_line "systemctl daemon-reload"
|
||||
test "$(wc -l < "${log_file}" | tr -d ' ')" -eq 2
|
||||
|
||||
# An existing service account is preserved without modification.
|
||||
reset_log
|
||||
run_postinstall 1 0
|
||||
test "$(cat "${log_file}")" = "systemctl daemon-reload"
|
||||
|
||||
# useradd creates a private group only when one does not already exist. An
|
||||
# administrator may pre-create group silo with the legacy GID; that group must
|
||||
# be reused rather than causing installation to fail.
|
||||
unlink_optional_commands
|
||||
link_command useradd
|
||||
reset_log
|
||||
run_postinstall 0 0
|
||||
assert_log_line "useradd --system --user-group --no-create-home --shell ${useradd_shell} --comment Silo object storage service silo"
|
||||
reject_log_text "--gid silo"
|
||||
|
||||
reset_log
|
||||
run_postinstall 0 1
|
||||
assert_log_line "useradd --system --gid silo --no-create-home --shell ${useradd_shell} --comment Silo object storage service silo"
|
||||
reject_log_text "--user-group"
|
||||
|
||||
# BusyBox follows the same existing-group contract.
|
||||
unlink_optional_commands
|
||||
link_command adduser
|
||||
link_command addgroup
|
||||
reset_log
|
||||
run_postinstall 0 0
|
||||
assert_log_line "addgroup -S silo"
|
||||
assert_log_line "adduser -S -D -H -G silo -s ${busybox_shell} silo"
|
||||
|
||||
reset_log
|
||||
run_postinstall 0 1
|
||||
reject_log_text "addgroup"
|
||||
assert_log_line "adduser -S -D -H -G silo -s ${busybox_shell} silo"
|
||||
|
||||
# Debian remove, RPM erase, and Alpine deinstall stop the Silo unit. Upgrade
|
||||
# arguments must leave the running service alone.
|
||||
for removal_argument in remove 0 20260214120000.0.0-r0; do
|
||||
reset_log
|
||||
run_preremove "${removal_argument}"
|
||||
test "$(cat "${log_file}")" = "systemctl disable --now silo.service"
|
||||
done
|
||||
|
||||
for upgrade_argument in upgrade 1; do
|
||||
reset_log
|
||||
run_preremove "${upgrade_argument}"
|
||||
test ! -s "${log_file}"
|
||||
done
|
||||
|
||||
# The package deliberately leaves legacy ownership changes to an explicit
|
||||
# systemd drop-in. Lifecycle scripts must never rewrite ownership or touch the
|
||||
# old unit, and the base unit must expose overridable User/Group directives.
|
||||
grep -Fx 'User=silo' "${repo_dir}/silo.service" >/dev/null
|
||||
grep -Fx 'Group=silo' "${repo_dir}/silo.service" >/dev/null
|
||||
if grep -Ein '\b(chown|chgrp|usermod|groupmod)\b|minio\.service' \
|
||||
"${postinstall}" "${preremove}"; then
|
||||
echo "package lifecycle scripts must not mutate data ownership or the legacy service" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Silo package lifecycle checks passed"
|
||||
@@ -1,44 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
sysusers_file=/usr/lib/sysusers.d/silo.conf
|
||||
|
||||
group_exists() {
|
||||
if command -v getent >/dev/null 2>&1; then
|
||||
getent group silo >/dev/null 2>&1
|
||||
return
|
||||
fi
|
||||
|
||||
[ -r /etc/group ] || return 1
|
||||
while IFS=: read -r group_name _; do
|
||||
[ "${group_name}" = silo ] && return 0
|
||||
done < /etc/group
|
||||
return 1
|
||||
}
|
||||
|
||||
if ! id -u silo >/dev/null 2>&1; then
|
||||
if command -v systemd-sysusers >/dev/null 2>&1; then
|
||||
systemd-sysusers "${sysusers_file}"
|
||||
elif command -v useradd >/dev/null 2>&1; then
|
||||
nologin_shell=/usr/sbin/nologin
|
||||
[ -x "${nologin_shell}" ] || nologin_shell=/sbin/nologin
|
||||
if group_exists; then
|
||||
useradd --system --gid silo --no-create-home --shell "${nologin_shell}" --comment "Silo object storage service" silo
|
||||
else
|
||||
useradd --system --user-group --no-create-home --shell "${nologin_shell}" --comment "Silo object storage service" silo
|
||||
fi
|
||||
elif command -v adduser >/dev/null 2>&1 && command -v addgroup >/dev/null 2>&1; then
|
||||
nologin_shell=/sbin/nologin
|
||||
[ -x "${nologin_shell}" ] || nologin_shell=/bin/false
|
||||
group_exists || addgroup -S silo
|
||||
adduser -S -D -H -G silo -s "${nologin_shell}" silo
|
||||
else
|
||||
echo "Unable to create the silo system account: systemd-sysusers, useradd, or BusyBox adduser is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||
fi
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
# Debian passes "remove" for an actual removal, RPM passes 0 to %preun, and
|
||||
# Alpine runs pre-deinstall only for removal and passes the old dotted version.
|
||||
# Upgrade paths deliberately leave the running service untouched.
|
||||
case "${1:-}" in
|
||||
remove|0|*.*)
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl disable --now silo.service >/dev/null 2>&1 || true
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
@@ -1,52 +0,0 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
mQINBGaV5PwBEACbErI+7yOrsXTT3mR83O6Fw9WyHJqozhyNPF3dA1gAtWpfWqd4
|
||||
S9x6vBjVwUbIRn21jYgov0hDiaLABNQhRzifvVr0r1IjBW8lhA8zJGaO42Uz0aBW
|
||||
YIkajOklsXgYMX+gSmy5WXzM31sDQVMnzptHh9dwW067hMM5pJKDslu2pLMwSb9K
|
||||
QgIFcYsaR0taBkcDg4dNu1gncriD/GcdXIS0/V4R82DIYeIqj2S0lt0jDTACbUz3
|
||||
C6esrTw2XerCeHKHb9c/V+KMhqvLJOOpy/aJWLrTGBoaH7xw6v0qg32OYiBxlUj9
|
||||
VEzoQbDfbRkR+jlxiuYP3scUs/ziKrSh+0mshVbeuLRSNfuHLa7C4xTEnATcgD1J
|
||||
MZeMaJXIcDt+DN+1aHVQjY5YNvr5wA3ykxW51uReZf7/odgqVW3+1rhW5pd8NQKQ
|
||||
qoVUHOtIrC9KaiGfrczEtJTNUxcNZV9eBgcKHYDXB2hmR2pIf7WvydgXTs/qIsXg
|
||||
SIzfKjisi795Dd5GrvdLYXVnu9YzylWlkJ5rjod1wnSxkI/CcCJaoPLnXZA9KV7A
|
||||
cpMWWaUEXP/XBIwIU+vxDd1taBIaPIOv1KIdzvG7QqAQtf5Lphi5HfaGvBud/CVt
|
||||
mvWhRPJMr1J0ER2xAgU2iZR7dN0vSF6zDqc0W09RAoC0nDS3tupDX2BrOwARAQAB
|
||||
tCRSdW9oYW5nIEZlbmcgKFBpZ3N0eSkgPHJoQHZvbm5nLmNvbT6JAlEEEwEIADsW
|
||||
IQSVkqe8emguczM3bgnnk12Nub2LIAUCZpXk/AIbAwULCQgHAgIiAgYVCgkICwIE
|
||||
FgIDAQIeBwIXgAAKCRDnk12Nub2LIOMuEACBLVc09O4icFwc45R3KMvOMu14Egpn
|
||||
UkpmBKhErjup0TIunzI0zZH6HG8LGuf6XEdH4ItCJeLg5349UE00BUHNmxk2coo2
|
||||
u4Wtu28LPqmxb6sqpuRAaefedU6vqfs7YN6WWp52pVF1KdOHkIOcgAQ9z3ZHdosM
|
||||
I/Y/UxO2t4pjdCAfJHOmGPrbgLcHSMpoLLxjuf3YIwS5NSfjNDd0Y8sKFUcMGLCF
|
||||
5P0lv5feLLdZvh2Una34UmHKhZlXC5E3vlY9bf/LgsRzXRFQosD0RsCXbz3Tk+zF
|
||||
+j/eP3WhUvJshqIDuY6eJYCzMjiA8sM5gety+htVJuD0mewp+qAhjxE0d4bIr4qO
|
||||
BKQzBt9tT2ackCPdgW42VPS+IZymm1oMET0hgZfKiVpwsKO6qxeWn4RW2jJ0zkUJ
|
||||
MsrrxOPFdZQAtuFcLwa5PUAHHs6XQT2vzxDpeE9lInQ14lshofU5ZKIeb9sbvb/w
|
||||
P+xnDqvZ1pcotEIBvDK0S0jHbHHqtioIUdDFvdCBlBlYP1TQRNPlJ7TJDBBvhj8i
|
||||
fmjQsYSV1u36aHOJVGYNHv+SyJpVd3nHCZn97ADM9qHnDm7xljyHXPzIx4FMmBGJ
|
||||
UTiLH5yxa1xhWr42Iv3TykaQJVbpydmBuegFR8WbWitAvVqI3HvRG+FalLsjJruc
|
||||
8YDAf7gHdj/937kCDQRmleT8ARAAmJxscC76NZzqFBiaeq2+aJxOt1HGPqKb4pbz
|
||||
jLKRX9sFkeXuzhfZaNDljnr2yrnQ75rit9Aah/loEhbSHanNUDCNmvOeSEISr9yA
|
||||
yfOnqlcVOtcwWQK57n6MvlCSM8Js3jdoSmCFHVtdFFwxejE5ok0dk1VFYDIg6DRk
|
||||
ZBMuxGO7ZJW7TzCxhK4AL+NNYA2wX6b+IVMn6CA9kwNwCNrrnGHR1sblSxZp7lPo
|
||||
+GsqzYY0LXGR2eEicgKd4lk38gaO8Q4d1mlpX95vgdhGKxR+CM26y9QU0qrO1hXP
|
||||
Fw6lX9HfIUkVNrqAa1mzgneYXivnLvcj8gc7bFAdweX4MyBHsmiPm32WqjUJFAmw
|
||||
kcKYaiyfDJ+1wusa/b+7RCnshWc8B9udYbXfvcpOGgphpUuvomKT8at3ToJfEWmR
|
||||
BzToYYTsgAAX8diY/X53BHCE/+MhLccglEUYNZyBRkTwDLrS9QgNkhrADaTwxsv1
|
||||
8PwnVKve/ZxwOU0QGf4ZOhA2YQOE5hkRDR5uY2OHsOS5vHsd9Y6kNNnO8EBy99d1
|
||||
QiBJOW3AP0nr4Cj1/NhdigAujsYRKiCAuPT7dgqART58VU4bZ3PgonMlziLe7+ht
|
||||
YYxV+wyP6LVqicDd0MLLvG7r/JOiWuABOUxsFFaRecehoPJjeAEQxnWJjedokXKL
|
||||
HVOFaEkAEQEAAYkCNgQYAQgAIBYhBJWSp7x6aC5zMzduCeeTXY25vYsgBQJmleT8
|
||||
AhsMAAoJEOeTXY25vYsgG8sP/3UdsWuiwTsf/x4BTW82K+Uk9YwZDnUNH+4dUMED
|
||||
bKT1C6CbuSZ7Mnbi2rVsmGzOMs9MehIx6Ko8/iCR2OCeWi8Q+wM+iffAfWuT1GK6
|
||||
7f/VIfoYBUWEa+kvDcPgEbd5Tu7ZdUO/jROVBSlXRSjzK9LpIj7GozBTJ8Vqy5x7
|
||||
oqbWPPEYtGDVHime8o6f5/wfhNgL3mFnoq6srK7KhwACwfTXlNqAlGiXGa30Yj+b
|
||||
Cj6IvmxoII49E67/ovMEmzDCb3RXiaL6OATy25P+HQJvWvAam7Qq5Xn+bZg65Mup
|
||||
vXq3zoX0a7EKXc5vsJVNtTlXO1ATdYszKP5uNzkHrNAN52VRYaowq1vPy/MVMbSI
|
||||
rL/hTFKr7ZNhmC7jmS3OuJyCYQsfEerubtBUuc/W6JDc2oTI3xOG1S2Zj8f4PxLl
|
||||
H7vMG4E+p6eOrUGw6VQXjFsH9GtwhkPh/ZGMKENb2+JztJ02674Cok4s5c/lZFKz
|
||||
mmRUcNjX2bm2K0GfGG5/hAog/CHCeUZvwIh4hZLkdeJ1QsIYpN8xbvY7QP6yh4VB
|
||||
XrL18+2sontZ45MsGResrRibB35x7IrCrxZsVtRJZthHqshiORPatgy+AiWcAtEv
|
||||
UWEnnC1xBSasNebw4fSE8AJg9JMCRw+3GAetlotOeW9q7PN6yrXD9rGuV/QquQNd
|
||||
/c7w
|
||||
=4rRi
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
export GORACE="history_size=7"
|
||||
export MINIO_API_REQUESTS_MAX=10000
|
||||
|
||||
for d in $(go list ./...); do
|
||||
CGO_ENABLED=1 go test -v -race --timeout 100m "$d"
|
||||
done
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,406 +0,0 @@
|
||||
// Copyright 2026 PGSTY contributors.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// rebrand-guard records the compatibility identifiers that a product rebrand
|
||||
// must not accidentally rename. It intentionally excludes product branding and
|
||||
// delivery names, which are validated by buildscripts/verify-rebrand.sh.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const manifestVersion = 4
|
||||
|
||||
var (
|
||||
minioImportRE = regexp.MustCompile(`github\.com/minio/[A-Za-z0-9_./-]+`)
|
||||
envRE = regexp.MustCompile(`\b_?MINIO_[A-Z0-9_]+\b`)
|
||||
metricRE = regexp.MustCompile(`\bminio_[A-Za-z0-9_]+\b`)
|
||||
headerRE = regexp.MustCompile(`(?i)\bx-minio-[a-z0-9_-]+\b`)
|
||||
routeRE = regexp.MustCompile(`^/[A-Za-z0-9._~!$&'()*+,;=:@%/?{}=-]*`)
|
||||
storageRE = regexp.MustCompile(`\.minio\.sys(?:/[A-Za-z0-9._${}-]+)*`)
|
||||
policyRE = regexp.MustCompile(`(?:arn:minio|minio:s3)[A-Za-z0-9_:/.*${}-]*`)
|
||||
brandRE = regexp.MustCompile(`(?i)(^|[^a-z0-9_])minio([^a-z0-9_]|$)`)
|
||||
)
|
||||
|
||||
type manifest struct {
|
||||
Version int `json:"version"`
|
||||
ModulePath string `json:"module_path"`
|
||||
MinioImports []string `json:"minio_imports"`
|
||||
Environment []string `json:"environment"`
|
||||
Metrics []string `json:"metrics"`
|
||||
Headers []string `json:"headers"`
|
||||
Routes []string `json:"routes"`
|
||||
RouteRoots []string `json:"route_roots"`
|
||||
GridRoutes []string `json:"grid_routes"`
|
||||
StorageMarkers []string `json:"storage_markers"`
|
||||
PolicyValues []string `json:"policy_values"`
|
||||
BrandAllowlist []string `json:"brand_allowlist"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
write := flag.Bool("write", false, "replace the checked-in compatibility baseline")
|
||||
flag.Parse()
|
||||
|
||||
repo, err := gitOutput("rev-parse", "--show-toplevel")
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
repo = strings.TrimSpace(repo)
|
||||
baselinePath := filepath.Join(repo, "buildscripts", "rebrand-guard", "compat-baseline.json")
|
||||
|
||||
current, err := collect(repo)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
if *write {
|
||||
if err := writeManifest(baselinePath, current); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
fmt.Printf("wrote %s\n", baselinePath)
|
||||
printSummary(current)
|
||||
return
|
||||
}
|
||||
|
||||
want, err := readManifest(baselinePath)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
if err := compare(want, current); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
printSummary(current)
|
||||
fmt.Println("Silo rebrand compatibility baseline is unchanged")
|
||||
}
|
||||
|
||||
func collect(repo string) (manifest, error) {
|
||||
files, err := trackedFiles(repo)
|
||||
if err != nil {
|
||||
return manifest{}, err
|
||||
}
|
||||
|
||||
sets := map[string]map[string]struct{}{
|
||||
"imports": {},
|
||||
"env": {},
|
||||
"metrics": {},
|
||||
"headers": {},
|
||||
"routes": {},
|
||||
"roots": {},
|
||||
"grid": {},
|
||||
"storage": {},
|
||||
"policy": {},
|
||||
"brand": {},
|
||||
}
|
||||
modulePath := ""
|
||||
fset := token.NewFileSet()
|
||||
|
||||
for _, rel := range files {
|
||||
// Investigation artifacts contain synthetic routes and archived configurations.
|
||||
if rel == "SILO_REBRANDING_MIGRATION.md" ||
|
||||
strings.HasPrefix(rel, "docs/investigations/") ||
|
||||
strings.HasPrefix(rel, "buildscripts/rebrand-guard/") ||
|
||||
strings.HasPrefix(rel, "buildscripts/helm-migration-guard/") {
|
||||
continue
|
||||
}
|
||||
path := filepath.Join(repo, filepath.FromSlash(rel))
|
||||
data, err := os.ReadFile(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return manifest{}, fmt.Errorf("read %s: %w", rel, err)
|
||||
}
|
||||
if bytes.IndexByte(data, 0) >= 0 {
|
||||
continue
|
||||
}
|
||||
text := string(data)
|
||||
|
||||
addMatches(sets["env"], envRE, text, false)
|
||||
addMatches(sets["headers"], headerRE, text, true)
|
||||
addMatches(sets["storage"], storageRE, text, false)
|
||||
addMatches(sets["policy"], policyRE, text, false)
|
||||
if strings.HasSuffix(rel, ".go") && (strings.HasPrefix(rel, "cmd/") || strings.HasPrefix(rel, "internal/")) {
|
||||
addMatches(sets["metrics"], metricRE, text, false)
|
||||
}
|
||||
if rel == "go.mod" {
|
||||
addMatches(sets["imports"], minioImportRE, text, false)
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 2 && fields[0] == "module" {
|
||||
modulePath = fields[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(rel, ".go") {
|
||||
file, err := parser.ParseFile(fset, path, data, parser.SkipObjectResolution)
|
||||
if err != nil {
|
||||
return manifest{}, fmt.Errorf("parse %s: %w", rel, err)
|
||||
}
|
||||
for _, spec := range file.Imports {
|
||||
value, err := strconv.Unquote(spec.Path.Value)
|
||||
if err == nil && strings.HasPrefix(value, "github.com/minio/") {
|
||||
sets["imports"][value] = struct{}{}
|
||||
}
|
||||
}
|
||||
if !strings.HasSuffix(rel, "_test.go") {
|
||||
// Test files hold request paths for fixtures, not served routes.
|
||||
collectStringMatches(sets["routes"], routeRE, file)
|
||||
if strings.HasPrefix(rel, "cmd/") || strings.HasPrefix(rel, "internal/") {
|
||||
collectBrandStrings(sets["brand"], rel, file)
|
||||
}
|
||||
}
|
||||
collectNamedStringValues(sets["roots"], rel, file, "minioReservedBucket")
|
||||
if rel == "internal/grid/manager.go" {
|
||||
collectStringMatches(sets["grid"], routeRE, file)
|
||||
}
|
||||
}
|
||||
}
|
||||
// This was a shell-local PID variable in the generated inspect script,
|
||||
// never a supported environment setting.
|
||||
delete(sets["env"], "MINIO_SRVR_PID")
|
||||
|
||||
if modulePath == "" {
|
||||
return manifest{}, errors.New("go.mod module path was not found")
|
||||
}
|
||||
return manifest{
|
||||
Version: manifestVersion,
|
||||
ModulePath: modulePath,
|
||||
MinioImports: sorted(sets["imports"]),
|
||||
Environment: sorted(sets["env"]),
|
||||
Metrics: sorted(sets["metrics"]),
|
||||
Headers: sorted(sets["headers"]),
|
||||
Routes: sorted(sets["routes"]),
|
||||
RouteRoots: sorted(sets["roots"]),
|
||||
GridRoutes: sorted(sets["grid"]),
|
||||
StorageMarkers: sorted(sets["storage"]),
|
||||
PolicyValues: sorted(sets["policy"]),
|
||||
BrandAllowlist: sorted(sets["brand"]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func collectBrandStrings(dst map[string]struct{}, rel string, file *ast.File) {
|
||||
ast.Inspect(file, func(node ast.Node) bool {
|
||||
literal, ok := node.(*ast.BasicLit)
|
||||
if !ok || literal.Kind != token.STRING {
|
||||
return true
|
||||
}
|
||||
value, err := strconv.Unquote(literal.Value)
|
||||
if err != nil || !brandRE.MatchString(value) || strings.HasPrefix(value, "github.com/minio/") {
|
||||
return true
|
||||
}
|
||||
dst[filepath.ToSlash(rel)+"="+strconv.Quote(value)] = struct{}{}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func collectNamedStringValues(dst map[string]struct{}, rel string, file *ast.File, names ...string) {
|
||||
wanted := make(map[string]struct{}, len(names))
|
||||
for _, name := range names {
|
||||
wanted[name] = struct{}{}
|
||||
}
|
||||
for _, decl := range file.Decls {
|
||||
gen, ok := decl.(*ast.GenDecl)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, rawSpec := range gen.Specs {
|
||||
spec, ok := rawSpec.(*ast.ValueSpec)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for i, name := range spec.Names {
|
||||
if _, ok := wanted[name.Name]; !ok || i >= len(spec.Values) {
|
||||
continue
|
||||
}
|
||||
literal, ok := spec.Values[i].(*ast.BasicLit)
|
||||
if !ok || literal.Kind != token.STRING {
|
||||
continue
|
||||
}
|
||||
value, err := strconv.Unquote(literal.Value)
|
||||
if err == nil {
|
||||
dst[filepath.ToSlash(rel)+":"+name.Name+"="+value] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func collectStringMatches(dst map[string]struct{}, re *regexp.Regexp, file *ast.File) {
|
||||
ast.Inspect(file, func(node ast.Node) bool {
|
||||
literal, ok := node.(*ast.BasicLit)
|
||||
if !ok || literal.Kind != token.STRING {
|
||||
return true
|
||||
}
|
||||
value, err := strconv.Unquote(literal.Value)
|
||||
if err == nil {
|
||||
addMatches(dst, re, value, false)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func trackedFiles(repo string) ([]string, error) {
|
||||
cmd := exec.Command("git", "-C", repo, "ls-files", "--cached", "-z")
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("git ls-files: %w", err)
|
||||
}
|
||||
parts := bytes.Split(out, []byte{0})
|
||||
files := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
if len(part) > 0 {
|
||||
files = append(files, string(part))
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func addMatches(dst map[string]struct{}, re *regexp.Regexp, text string, lower bool) {
|
||||
for _, match := range re.FindAllString(text, -1) {
|
||||
if lower {
|
||||
match = strings.ToLower(match)
|
||||
}
|
||||
dst[match] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func sorted(set map[string]struct{}) []string {
|
||||
values := make([]string, 0, len(set))
|
||||
for value := range set {
|
||||
values = append(values, value)
|
||||
}
|
||||
sort.Strings(values)
|
||||
return values
|
||||
}
|
||||
|
||||
func writeManifest(path string, value manifest) error {
|
||||
data, err := json.MarshalIndent(value, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = append(data, '\n')
|
||||
return os.WriteFile(path, data, 0o644)
|
||||
}
|
||||
|
||||
func readManifest(path string) (manifest, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return manifest{}, fmt.Errorf("read compatibility baseline (run go run ./buildscripts/rebrand-guard --write once): %w", err)
|
||||
}
|
||||
var value manifest
|
||||
if err := json.Unmarshal(data, &value); err != nil {
|
||||
return manifest{}, err
|
||||
}
|
||||
if value.Version != manifestVersion {
|
||||
return manifest{}, fmt.Errorf("unsupported compatibility baseline version %d", value.Version)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func compare(want, got manifest) error {
|
||||
var failures []string
|
||||
if want.ModulePath != got.ModulePath {
|
||||
failures = append(failures, fmt.Sprintf("module_path: want %q, got %q", want.ModulePath, got.ModulePath))
|
||||
}
|
||||
checks := []struct {
|
||||
name string
|
||||
want, got []string
|
||||
}{
|
||||
{"minio_imports", want.MinioImports, got.MinioImports},
|
||||
{"environment", want.Environment, got.Environment},
|
||||
{"metrics", want.Metrics, got.Metrics},
|
||||
{"headers", want.Headers, got.Headers},
|
||||
{"routes", want.Routes, got.Routes},
|
||||
{"route_roots", want.RouteRoots, got.RouteRoots},
|
||||
{"grid_routes", want.GridRoutes, got.GridRoutes},
|
||||
{"storage_markers", want.StorageMarkers, got.StorageMarkers},
|
||||
{"policy_values", want.PolicyValues, got.PolicyValues},
|
||||
{"brand_allowlist", want.BrandAllowlist, got.BrandAllowlist},
|
||||
}
|
||||
for _, check := range checks {
|
||||
if missing, added := setDiff(check.want, check.got); len(missing) > 0 || len(added) > 0 {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "%s compatibility set changed", check.name)
|
||||
for _, value := range missing {
|
||||
fmt.Fprintf(&b, "\n - %s", value)
|
||||
}
|
||||
for _, value := range added {
|
||||
fmt.Fprintf(&b, "\n + %s", value)
|
||||
}
|
||||
failures = append(failures, b.String())
|
||||
}
|
||||
}
|
||||
if len(failures) > 0 {
|
||||
return errors.New(strings.Join(failures, "\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setDiff(want, got []string) (missing, added []string) {
|
||||
wantSet := make(map[string]struct{}, len(want))
|
||||
gotSet := make(map[string]struct{}, len(got))
|
||||
for _, value := range want {
|
||||
wantSet[value] = struct{}{}
|
||||
}
|
||||
for _, value := range got {
|
||||
gotSet[value] = struct{}{}
|
||||
}
|
||||
for _, value := range want {
|
||||
if _, ok := gotSet[value]; !ok {
|
||||
missing = append(missing, value)
|
||||
}
|
||||
}
|
||||
for _, value := range got {
|
||||
if _, ok := wantSet[value]; !ok {
|
||||
added = append(added, value)
|
||||
}
|
||||
}
|
||||
return missing, added
|
||||
}
|
||||
|
||||
func printSummary(value manifest) {
|
||||
fmt.Printf("compatibility manifest: imports=%d env=%d metrics=%d headers=%d routes=%d roots=%d grid=%d storage=%d policy=%d brand=%d sha256=%s\n",
|
||||
len(value.MinioImports), len(value.Environment), len(value.Metrics), len(value.Headers),
|
||||
len(value.Routes), len(value.RouteRoots), len(value.GridRoutes), len(value.StorageMarkers), len(value.PolicyValues),
|
||||
len(value.BrandAllowlist), manifestDigest(value))
|
||||
}
|
||||
|
||||
func manifestDigest(value manifest) string {
|
||||
data, _ := json.Marshal(value)
|
||||
sum := sha256.Sum256(data)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func gitOutput(args ...string) (string, error) {
|
||||
out, err := exec.Command("git", args...).CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("git %s: %s: %w", strings.Join(args, " "), strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
func fatal(err error) {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
set -E
|
||||
set -o pipefail
|
||||
set -x
|
||||
set -e
|
||||
|
||||
WORK_DIR="$PWD/.verify-$RANDOM"
|
||||
SILO_CONFIG_DIR="$WORK_DIR/.silo"
|
||||
SILO=("$PWD/silo" --config-dir "$SILO_CONFIG_DIR" server)
|
||||
|
||||
if [ ! -x "$PWD/silo" ]; then
|
||||
echo "Silo executable binary not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function start_silo_5drive() {
|
||||
start_port=$1
|
||||
|
||||
export MINIO_ROOT_USER=silo
|
||||
export MINIO_ROOT_PASSWORD=silo1234
|
||||
export MC_HOST_silo="http://silo:silo1234@127.0.0.1:${start_port}/"
|
||||
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
|
||||
export MINIO_CI_CD=1
|
||||
|
||||
mkdir -p "${WORK_DIR}"
|
||||
"$(git rev-parse --show-toplevel)/buildscripts/install-mcli.sh" "${WORK_DIR}/mc"
|
||||
|
||||
"${WORK_DIR}/mc" cp --quiet -r "buildscripts/cicd-corpus/" "${WORK_DIR}/cicd-corpus/"
|
||||
|
||||
"${SILO[@]}" --address ":$start_port" "${WORK_DIR}/cicd-corpus/disk{1...5}" >"${WORK_DIR}/server1.log" 2>&1 &
|
||||
pid=$!
|
||||
disown $pid
|
||||
sleep 5
|
||||
|
||||
if ! ps -p ${pid} 1>&2 >/dev/null; then
|
||||
echo "server1 log:"
|
||||
cat "${WORK_DIR}/server1.log"
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"${WORK_DIR}/mc" stat silo/bucket/testobj
|
||||
|
||||
pkill silo
|
||||
sleep 3
|
||||
}
|
||||
|
||||
function main() {
|
||||
start_port=$(shuf -i 10000-65000 -n 1)
|
||||
|
||||
start_silo_5drive ${start_port}
|
||||
}
|
||||
|
||||
function purge() {
|
||||
rm -rf "$1"
|
||||
}
|
||||
|
||||
(main "$@")
|
||||
rv=$?
|
||||
purge "$WORK_DIR"
|
||||
exit "$rv"
|
||||
@@ -1,151 +0,0 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
set -E
|
||||
set -o pipefail
|
||||
set -x
|
||||
|
||||
WORK_DIR="$PWD/.verify-$RANDOM"
|
||||
SILO_CONFIG_DIR="$WORK_DIR/.silo"
|
||||
MINIO_OLD=("$PWD/minio.RELEASE.2020-10-28T08-16-50Z" --config-dir "$SILO_CONFIG_DIR" server)
|
||||
SILO=("$PWD/silo" --config-dir "$SILO_CONFIG_DIR" server)
|
||||
|
||||
if [ ! -x "$PWD/silo" ]; then
|
||||
echo "Silo executable binary not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function download_old_release() {
|
||||
if [ ! -x minio.RELEASE.2020-10-28T08-16-50Z ]; then
|
||||
: "${SILO_LEGACY_FIXTURE_2020:?set SILO_LEGACY_FIXTURE_2020 to the audited legacy binary}"
|
||||
: "${SILO_LEGACY_SHA256_2020:?set SILO_LEGACY_SHA256_2020 to its audited SHA-256}"
|
||||
"$(git rev-parse --show-toplevel)/buildscripts/install-verified-fixture.sh" \
|
||||
"${SILO_LEGACY_FIXTURE_2020}" "${SILO_LEGACY_SHA256_2020}" \
|
||||
"minio.RELEASE.2020-10-28T08-16-50Z"
|
||||
fi
|
||||
}
|
||||
|
||||
function verify_rewrite() {
|
||||
start_port=$1
|
||||
|
||||
export MINIO_ACCESS_KEY=silo
|
||||
export MINIO_SECRET_KEY=silo1234
|
||||
export MC_HOST_silo="http://silo:silo1234@127.0.0.1:${start_port}/"
|
||||
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
|
||||
export MINIO_CI_CD=1
|
||||
|
||||
mkdir -p "${WORK_DIR}"
|
||||
"$(git rev-parse --show-toplevel)/buildscripts/install-mcli.sh" "${WORK_DIR}/mc"
|
||||
|
||||
"${MINIO_OLD[@]}" --address ":$start_port" "${WORK_DIR}/xl{1...16}" >"${WORK_DIR}/server1.log" 2>&1 &
|
||||
pid=$!
|
||||
disown $pid
|
||||
|
||||
"${WORK_DIR}/mc" ready silo/
|
||||
|
||||
if ! ps -p ${pid} 1>&2 >/dev/null; then
|
||||
echo "server1 log:"
|
||||
cat "${WORK_DIR}/server1.log"
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"${WORK_DIR}/mc" mb silo/healing-rewrite-bucket --quiet --with-lock
|
||||
"${WORK_DIR}/mc" cp \
|
||||
buildscripts/verify-build.sh \
|
||||
silo/healing-rewrite-bucket/ \
|
||||
--disable-multipart --quiet
|
||||
|
||||
"${WORK_DIR}/mc" cp \
|
||||
buildscripts/verify-build.sh \
|
||||
silo/healing-rewrite-bucket/ \
|
||||
--disable-multipart --quiet
|
||||
|
||||
"${WORK_DIR}/mc" cp \
|
||||
buildscripts/verify-build.sh \
|
||||
silo/healing-rewrite-bucket/ \
|
||||
--disable-multipart --quiet
|
||||
|
||||
kill ${pid}
|
||||
sleep 3
|
||||
|
||||
"${SILO[@]}" --address ":$start_port" "${WORK_DIR}/xl{1...16}" >"${WORK_DIR}/server1.log" 2>&1 &
|
||||
pid=$!
|
||||
disown $pid
|
||||
|
||||
"${WORK_DIR}/mc" ready silo/
|
||||
|
||||
if ! ps -p ${pid} 1>&2 >/dev/null; then
|
||||
echo "server1 log:"
|
||||
cat "${WORK_DIR}/server1.log"
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! ./s3-check-md5 \
|
||||
-debug \
|
||||
-versions \
|
||||
-access-key silo \
|
||||
-secret-key silo1234 \
|
||||
-endpoint "http://127.0.0.1:${start_port}/" 2>&1 | grep INTACT; then
|
||||
echo "server1 log:"
|
||||
cat "${WORK_DIR}/server1.log"
|
||||
echo "FAILED"
|
||||
mkdir -p inspects
|
||||
(
|
||||
cd inspects
|
||||
"${WORK_DIR}/mc" admin inspect silo/healing-rewrite-bucket/verify-build.sh/**
|
||||
)
|
||||
|
||||
"${WORK_DIR}/mc" mb play/inspects
|
||||
"${WORK_DIR}/mc" mirror inspects play/inspects
|
||||
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
go run ./buildscripts/heal-manual.go "127.0.0.1:${start_port}" "silo" "silo1234"
|
||||
sleep 1
|
||||
|
||||
if ! ./s3-check-md5 \
|
||||
-debug \
|
||||
-versions \
|
||||
-access-key silo \
|
||||
-secret-key silo1234 \
|
||||
-endpoint http://127.0.0.1:${start_port}/ 2>&1 | grep INTACT; then
|
||||
echo "server1 log:"
|
||||
cat "${WORK_DIR}/server1.log"
|
||||
echo "FAILED"
|
||||
mkdir -p inspects
|
||||
(
|
||||
cd inspects
|
||||
"${WORK_DIR}/mc" admin inspect silo/healing-rewrite-bucket/verify-build.sh/**
|
||||
)
|
||||
|
||||
"${WORK_DIR}/mc" mb play/inspects
|
||||
"${WORK_DIR}/mc" mirror inspects play/inspects
|
||||
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
kill ${pid}
|
||||
}
|
||||
|
||||
function main() {
|
||||
download_old_release
|
||||
|
||||
start_port=$(shuf -i 10000-65000 -n 1)
|
||||
|
||||
verify_rewrite ${start_port}
|
||||
}
|
||||
|
||||
function purge() {
|
||||
rm -rf "$1"
|
||||
}
|
||||
|
||||
(main "$@")
|
||||
rv=$?
|
||||
purge "$WORK_DIR"
|
||||
exit "$rv"
|
||||
@@ -1,287 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# These are the single source of truth for the package identity: .github/nfpm.yml
|
||||
# must agree with them, and test-release.yml asserts that it does. Drift the
|
||||
# other way round would only surface here, on the maintainer's machine, after
|
||||
# the build has already run and uploaded.
|
||||
expected_fingerprint="9592A7BC7A682E7333376E09E7935D8DB9BD8B20"
|
||||
expected_release="1PGSTY"
|
||||
expected_vendor="PGSTY"
|
||||
expected_packager="Ruohang Feng (@Vonng) <rh@vonng.com>"
|
||||
expected_url="https://silo.pgsty.com"
|
||||
expected_summary="S3-Interface Libre Object Storage, a community-maintained S3-compatible server."
|
||||
expected_description="S3-Interface Libre Object Storage, a community-maintained S3-compatible server."
|
||||
expected_license="AGPL-3.0-or-later"
|
||||
expected_group="Applications/File"
|
||||
expected_payload="/etc/default/silo
|
||||
/usr/bin/silo
|
||||
/usr/lib/systemd/system/silo.service
|
||||
/usr/lib/sysusers.d/silo.conf
|
||||
/usr/share/doc/silo/LICENSE
|
||||
/usr/share/doc/silo/NOTICE"
|
||||
repository="${GH_REPO:-pgsty/silo}"
|
||||
container="${DNFUPDATE_CONTAINER:-dnfupdate}"
|
||||
upload=false
|
||||
release_tag=""
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: buildscripts/sign-release-rpms.sh RELEASE.TAG [--upload] [--repo OWNER/REPO] [--container NAME]
|
||||
|
||||
Downloads the two unsigned RPMs from a Draft GitHub Release, signs them with
|
||||
the expected Pigsty key in the local dnfupdate container, verifies the result,
|
||||
and regenerates their .sha256sum files. Nothing is uploaded unless --upload is
|
||||
provided.
|
||||
EOF
|
||||
}
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--upload)
|
||||
upload=true
|
||||
;;
|
||||
--repo)
|
||||
shift
|
||||
if [ "$#" -eq 0 ]; then
|
||||
echo "--repo requires OWNER/REPO" >&2
|
||||
exit 1
|
||||
fi
|
||||
repository="$1"
|
||||
;;
|
||||
--container)
|
||||
shift
|
||||
if [ "$#" -eq 0 ]; then
|
||||
echo "--container requires a name" >&2
|
||||
exit 1
|
||||
fi
|
||||
container="$1"
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
-*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
if [ -n "${release_tag}" ]; then
|
||||
echo "Only one release tag may be specified" >&2
|
||||
exit 1
|
||||
fi
|
||||
release_tag="$1"
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [ -z "${release_tag}" ]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for command in docker gh; do
|
||||
if ! command -v "${command}" >/dev/null 2>&1; then
|
||||
echo "${command} is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
version_hyphen="${release_tag#RELEASE.}"
|
||||
package_version="$(printf '%s\n' "${version_hyphen}" | sed -E \
|
||||
's/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2})-([0-9]{2})-([0-9]{2})Z$/\1\2\3\4\5\6.0.0/')"
|
||||
if [ "${package_version}" = "${version_hyphen}" ]; then
|
||||
echo "Invalid release tag: ${release_tag}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$(gh release view "${release_tag}" --repo "${repository}" --json isDraft --jq .isDraft)" != "true" ]; then
|
||||
echo "Refusing to sign: ${release_tag} is not a Draft release" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$(docker inspect --format '{{.State.Running}}' "${container}" 2>/dev/null || true)" != "true" ]; then
|
||||
echo "Signing container is not running: ${container}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
secret_fingerprints="$(docker exec "${container}" \
|
||||
gpg --batch --with-colons --list-secret-keys 2>/dev/null |
|
||||
awk -F: '$1 == "fpr" { print toupper($10) }')"
|
||||
if ! printf '%s\n' "${secret_fingerprints}" | grep -Fxq "${expected_fingerprint}"; then
|
||||
echo "Expected signing key is not available in ${container}: ${expected_fingerprint}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_dir="$(cd "${script_dir}/.." && pwd)"
|
||||
work_root="${SIGN_WORKDIR:-${repo_dir}/.release-sign}"
|
||||
mkdir -p "${work_root}"
|
||||
work_dir="$(mktemp -d "${work_root}/${release_tag}.XXXXXX")"
|
||||
unsigned_dir="${work_dir}/unsigned"
|
||||
signed_dir="${work_dir}/signed"
|
||||
mkdir -p "${unsigned_dir}" "${signed_dir}"
|
||||
chmod 700 "${work_dir}" "${unsigned_dir}" "${signed_dir}"
|
||||
|
||||
rpm_files=(
|
||||
"silo-${package_version}-${expected_release}.x86_64.rpm"
|
||||
"silo-${package_version}-${expected_release}.aarch64.rpm"
|
||||
)
|
||||
|
||||
download_patterns=()
|
||||
for rpm_file in "${rpm_files[@]}"; do
|
||||
download_patterns+=(--pattern "${rpm_file}" --pattern "${rpm_file}.sha256sum")
|
||||
done
|
||||
|
||||
echo "Downloading RPMs from Draft release ${repository}@${release_tag}"
|
||||
gh release download "${release_tag}" --repo "${repository}" \
|
||||
--dir "${unsigned_dir}" "${download_patterns[@]}"
|
||||
|
||||
sha256_digest() {
|
||||
local file="$1"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "${file}" | awk '{print $1}'
|
||||
else
|
||||
shasum -a 256 "${file}" | awk '{print $1}'
|
||||
fi
|
||||
}
|
||||
|
||||
for rpm_file in "${rpm_files[@]}"; do
|
||||
rpm_path="${unsigned_dir}/${rpm_file}"
|
||||
checksum_path="${rpm_path}.sha256sum"
|
||||
test -s "${rpm_path}"
|
||||
test -s "${checksum_path}"
|
||||
|
||||
actual_line="$(sha256_digest "${rpm_path}") ${rpm_file}"
|
||||
published_line="$(tr -d '\n' < "${checksum_path}")"
|
||||
if [ "${actual_line}" != "${published_line}" ]; then
|
||||
echo "Checksum mismatch for ${rpm_file}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
safe_tag="$(printf '%s' "${release_tag}" | tr -c 'A-Za-z0-9._-' '_')"
|
||||
container_dir="/tmp/silo-sign-${safe_tag}-$$"
|
||||
docker exec "${container}" mkdir -p "${container_dir}"
|
||||
|
||||
cleanup_container() {
|
||||
local rpm_file
|
||||
for rpm_file in "${rpm_files[@]}"; do
|
||||
docker exec "${container}" rm -f "${container_dir}/${rpm_file}" >/dev/null 2>&1 || true
|
||||
done
|
||||
docker exec "${container}" rmdir "${container_dir}" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup_container EXIT
|
||||
|
||||
assert_rpm_tag() {
|
||||
local rpm_path="$1"
|
||||
local tag="$2"
|
||||
local expected="$3"
|
||||
local actual
|
||||
|
||||
actual="$(docker exec "${container}" rpm -qp --queryformat "%{${tag}}" "${rpm_path}")"
|
||||
if [ "${actual}" != "${expected}" ]; then
|
||||
echo "Unexpected RPM ${tag}: ${actual}" >&2
|
||||
echo "Expected RPM ${tag}: ${expected}" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
for rpm_file in "${rpm_files[@]}"; do
|
||||
case "${rpm_file}" in
|
||||
*.x86_64.rpm)
|
||||
expected_arch="x86_64"
|
||||
;;
|
||||
*.aarch64.rpm)
|
||||
expected_arch="aarch64"
|
||||
;;
|
||||
*)
|
||||
echo "Unexpected RPM filename: ${rpm_file}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Signing ${rpm_file} with ${expected_fingerprint}"
|
||||
docker cp "${unsigned_dir}/${rpm_file}" "${container}:${container_dir}/${rpm_file}" >/dev/null
|
||||
container_rpm="${container_dir}/${rpm_file}"
|
||||
|
||||
assert_rpm_tag "${container_rpm}" NAME silo
|
||||
assert_rpm_tag "${container_rpm}" VERSION "${package_version}"
|
||||
assert_rpm_tag "${container_rpm}" RELEASE "${expected_release}"
|
||||
assert_rpm_tag "${container_rpm}" ARCH "${expected_arch}"
|
||||
assert_rpm_tag "${container_rpm}" VENDOR "${expected_vendor}"
|
||||
assert_rpm_tag "${container_rpm}" PACKAGER "${expected_packager}"
|
||||
assert_rpm_tag "${container_rpm}" URL "${expected_url}"
|
||||
assert_rpm_tag "${container_rpm}" LICENSE "${expected_license}"
|
||||
assert_rpm_tag "${container_rpm}" GROUP "${expected_group}"
|
||||
assert_rpm_tag "${container_rpm}" SUMMARY "${expected_summary}"
|
||||
assert_rpm_tag "${container_rpm}" DESCRIPTION "${expected_description}"
|
||||
|
||||
rpm_payload="$(docker exec "${container}" rpm -qpl "${container_rpm}")"
|
||||
if [ "${rpm_payload}" != "${expected_payload}" ]; then
|
||||
echo "Unexpected RPM payload for ${rpm_file}:" >&2
|
||||
printf '%s\n' "${rpm_payload}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker exec "${container}" rpmsign \
|
||||
--define "_gpg_name ${expected_fingerprint}" \
|
||||
--addsign "${container_rpm}"
|
||||
|
||||
signature_output="$(docker exec "${container}" rpmkeys --checksig --verbose "${container_rpm}")"
|
||||
printf '%s\n' "${signature_output}"
|
||||
if ! printf '%s\n' "${signature_output}" | tr '[:upper:]' '[:lower:]' | grep -q 'key id b9bd8b20: ok'; then
|
||||
echo "Signature verification failed for ${rpm_file}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
docker cp "${container}:${container_dir}/${rpm_file}" "${signed_dir}/${rpm_file}" >/dev/null
|
||||
signed_digest="$(sha256_digest "${signed_dir}/${rpm_file}")"
|
||||
printf '%s %s' "${signed_digest}" "${rpm_file}" > "${signed_dir}/${rpm_file}.sha256sum"
|
||||
|
||||
docker exec "${container}" rpm -qp --queryformat \
|
||||
$'Name: %{NAME}\nVersion: %{VERSION}-%{RELEASE}\nArch: %{ARCH}\nVendor: %{VENDOR}\nPackager: %{PACKAGER}\nURL: %{URL}\n' \
|
||||
"${container_rpm}"
|
||||
echo "SHA256: ${signed_digest}"
|
||||
done
|
||||
|
||||
if [ "${upload}" = true ]; then
|
||||
upload_files=()
|
||||
for rpm_file in "${rpm_files[@]}"; do
|
||||
upload_files+=("${signed_dir}/${rpm_file}" "${signed_dir}/${rpm_file}.sha256sum")
|
||||
done
|
||||
|
||||
echo "Replacing RPMs in Draft release ${release_tag}"
|
||||
gh release upload "${release_tag}" --repo "${repository}" --clobber "${upload_files[@]}"
|
||||
|
||||
for rpm_file in "${rpm_files[@]}"; do
|
||||
for asset in "${rpm_file}" "${rpm_file}.sha256sum"; do
|
||||
local_digest="sha256:$(sha256_digest "${signed_dir}/${asset}")"
|
||||
remote_digest=""
|
||||
for attempt in 1 2 3 4 5; do
|
||||
remote_digest="$(gh release view "${release_tag}" --repo "${repository}" --json assets \
|
||||
--jq ".assets[] | select(.name == \"${asset}\") | .digest")"
|
||||
if [ "${local_digest}" = "${remote_digest}" ]; then
|
||||
break
|
||||
fi
|
||||
if [ "${attempt}" -lt 5 ]; then
|
||||
sleep 2
|
||||
fi
|
||||
done
|
||||
if [ "${local_digest}" != "${remote_digest}" ]; then
|
||||
echo "GitHub asset digest mismatch for ${asset}" >&2
|
||||
echo "Local: ${local_digest}" >&2
|
||||
echo "Remote: ${remote_digest}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Verified GitHub asset: ${asset} ${remote_digest}"
|
||||
done
|
||||
done
|
||||
else
|
||||
echo
|
||||
echo "Signed RPMs are ready for review in: ${signed_dir}"
|
||||
echo "Re-run with --upload to replace the RPM assets in the Draft release."
|
||||
fi
|
||||
@@ -1,126 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -n "$TEST_DEBUG" ]; then
|
||||
set -x
|
||||
fi
|
||||
|
||||
WORK_DIR="$PWD/.verify-$RANDOM"
|
||||
SILO_CONFIG_DIR="$WORK_DIR/.silo"
|
||||
SILO=("$PWD/silo" --config-dir "$SILO_CONFIG_DIR" server)
|
||||
|
||||
if [ ! -x "$PWD/silo" ]; then
|
||||
echo "Silo executable binary not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x "$PWD/silo" ]; then
|
||||
echo "Silo executable binary not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
trap 'catch $LINENO' ERR
|
||||
|
||||
function purge() {
|
||||
rm -rf "$1"
|
||||
}
|
||||
|
||||
# shellcheck disable=SC2120
|
||||
catch() {
|
||||
if [ $# -ne 0 ]; then
|
||||
echo "error on line $1"
|
||||
fi
|
||||
|
||||
echo "Cleaning up instances of Silo"
|
||||
pkill silo || true
|
||||
pkill -9 silo || true
|
||||
purge "$WORK_DIR"
|
||||
if [ $# -ne 0 ]; then
|
||||
exit $#
|
||||
fi
|
||||
}
|
||||
|
||||
catch
|
||||
|
||||
function gen_put_request() {
|
||||
hdr_sleep=$1
|
||||
body_sleep=$2
|
||||
|
||||
echo "PUT /testbucket/testobject HTTP/1.1"
|
||||
sleep $hdr_sleep
|
||||
echo "Host: foo-header"
|
||||
echo "User-Agent: curl/8.2.1"
|
||||
echo "Accept: */*"
|
||||
echo "Content-Length: 30"
|
||||
echo ""
|
||||
|
||||
sleep $body_sleep
|
||||
echo "random line 0"
|
||||
echo "random line 1"
|
||||
echo ""
|
||||
echo ""
|
||||
}
|
||||
|
||||
function send_put_object_request() {
|
||||
hdr_timeout=$1
|
||||
body_timeout=$2
|
||||
|
||||
start=$(date +%s)
|
||||
timeout 5m bash -c "gen_put_request $hdr_timeout $body_timeout | netcat 127.0.0.1 $start_port | read" || return -1
|
||||
[ $(($(date +%s) - start)) -gt $((srv_hdr_timeout + srv_idle_timeout + 1)) ] && return -1
|
||||
return 0
|
||||
}
|
||||
|
||||
function test_silo_with_timeout() {
|
||||
start_port=$1
|
||||
|
||||
export MINIO_ROOT_USER=silo
|
||||
export MINIO_ROOT_PASSWORD=silo1234
|
||||
export MC_HOST_silo="http://silo:silo1234@127.0.0.1:${start_port}/"
|
||||
export MINIO_CI_CD=1
|
||||
|
||||
mkdir ${WORK_DIR}
|
||||
if [ ! -x "$PWD/mc" ]; then
|
||||
"$(git rev-parse --show-toplevel)/buildscripts/install-mcli.sh" "$PWD/mc"
|
||||
fi
|
||||
|
||||
"${SILO[@]}" --address ":$start_port" --read-header-timeout ${srv_hdr_timeout}s --idle-timeout ${srv_idle_timeout}s "${WORK_DIR}/disk/" >"${WORK_DIR}/server1.log" 2>&1 &
|
||||
pid=$!
|
||||
disown $pid
|
||||
sleep 1
|
||||
|
||||
if ! ps -p ${pid} 1>&2 >/dev/null; then
|
||||
echo "server1 log:"
|
||||
cat "${WORK_DIR}/server1.log"
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
"${PWD}/mc" mb silo/testbucket
|
||||
"${PWD}/mc" anonymous set public silo/testbucket
|
||||
|
||||
# slow header writing
|
||||
send_put_object_request 20 0 && exit -1
|
||||
"${PWD}/mc" stat silo/testbucket/testobject && exit -1
|
||||
|
||||
# quick header write and slow bodywrite
|
||||
send_put_object_request 0 40 && exit -1
|
||||
"${PWD}/mc" stat silo/testbucket/testobject && exit -1
|
||||
|
||||
# quick header and body write
|
||||
send_put_object_request 1 1 || exit -1
|
||||
"${PWD}/mc" stat silo/testbucket/testobject || exit -1
|
||||
}
|
||||
|
||||
function main() {
|
||||
export start_port=$(shuf -i 10000-65000 -n 1)
|
||||
export srv_hdr_timeout=5
|
||||
export srv_idle_timeout=5
|
||||
export -f gen_put_request
|
||||
|
||||
test_silo_with_timeout ${start_port}
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,51 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Asserts that every binary GoReleaser produced is stamped by the Go toolchain
|
||||
# as built from this exact commit with a clean working tree. A stray untracked
|
||||
# file (for example an un-ignored dist/) silently turns every release binary
|
||||
# into a "+dirty" pseudo-version, which destroys the link between a published
|
||||
# artifact and its tag. Catch that here instead of after publishing.
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_dir="$(cd "${script_dir}/.." && pwd)"
|
||||
dist_dir="${DIST_DIR:-${repo_dir}/dist}"
|
||||
expected_count="${EXPECTED_BINARY_COUNT:-6}"
|
||||
|
||||
if ! command -v go >/dev/null 2>&1; then
|
||||
echo "go is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "${dist_dir}" ]; then
|
||||
echo "Missing GoReleaser dist directory: ${dist_dir}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
revision="$(git -C "${repo_dir}" rev-parse HEAD)"
|
||||
|
||||
count=0
|
||||
while IFS= read -r binary; do
|
||||
count=$((count + 1))
|
||||
info="$(go version -m "${binary}")"
|
||||
|
||||
if ! grep -qF "vcs.revision=${revision}" <<< "${info}"; then
|
||||
echo "Unexpected vcs.revision in ${binary} (expected ${revision})" >&2
|
||||
grep -F 'vcs.' <<< "${info}" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -qF 'vcs.modified=false' <<< "${info}"; then
|
||||
echo "Binary was built from a dirty working tree: ${binary}" >&2
|
||||
grep -F 'vcs.' <<< "${info}" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
done < <(find "${dist_dir}" -maxdepth 2 -type f \( -name 'silo' -o -name 'silo.exe' \) | sort)
|
||||
|
||||
if [ "${count}" -ne "${expected_count}" ]; then
|
||||
echo "Expected ${expected_count} release binaries, found ${count}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Verified ${count} binaries built from ${revision} with a clean tree"
|
||||
@@ -1,287 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
|
||||
set -e
|
||||
set -E
|
||||
set -o pipefail
|
||||
|
||||
if [ ! -x "$PWD/silo" ]; then
|
||||
echo "Silo executable binary not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK_DIR="$PWD/.verify-$RANDOM"
|
||||
|
||||
export MINT_MODE=core
|
||||
export MINT_DATA_DIR="$WORK_DIR/data"
|
||||
export SERVER_ENDPOINT="127.0.0.1:9000"
|
||||
export MC_HOST_verify="http://silo:silo1234@${SERVER_ENDPOINT}/"
|
||||
export MC_HOST_verify_ipv6="http://silo:silo1234@[::1]:9000/"
|
||||
export ACCESS_KEY="silo"
|
||||
export SECRET_KEY="silo1234"
|
||||
export ENABLE_HTTPS=0
|
||||
export GO111MODULE=on
|
||||
export GOGC=25
|
||||
export ENABLE_ADMIN=1
|
||||
export MINIO_CI_CD=1
|
||||
|
||||
SILO_CONFIG_DIR="$WORK_DIR/.silo"
|
||||
SILO=("$PWD/silo" --config-dir "$SILO_CONFIG_DIR")
|
||||
|
||||
FILE_1_MB="$MINT_DATA_DIR/datafile-1-MB"
|
||||
FILE_65_MB="$MINT_DATA_DIR/datafile-65-MB"
|
||||
|
||||
FUNCTIONAL_TESTS="$WORK_DIR/functional-tests.sh"
|
||||
|
||||
function start_silo_fs() {
|
||||
export MINIO_ROOT_USER=$ACCESS_KEY
|
||||
export MINIO_ROOT_PASSWORD=$SECRET_KEY
|
||||
"${SILO[@]}" server "${WORK_DIR}/fs-disk" >"$WORK_DIR/fs-silo.log" 2>&1 &
|
||||
|
||||
"${WORK_DIR}/mc" ready verify
|
||||
}
|
||||
|
||||
function start_silo_erasure() {
|
||||
"${SILO[@]}" server "${WORK_DIR}/erasure-disk1" "${WORK_DIR}/erasure-disk2" "${WORK_DIR}/erasure-disk3" "${WORK_DIR}/erasure-disk4" >"$WORK_DIR/erasure-silo.log" 2>&1 &
|
||||
|
||||
"${WORK_DIR}/mc" ready verify
|
||||
}
|
||||
|
||||
function start_silo_erasure_sets() {
|
||||
export MINIO_ENDPOINTS="${WORK_DIR}/erasure-disk-sets{1...32}"
|
||||
"${SILO[@]}" server >"$WORK_DIR/erasure-silo-sets.log" 2>&1 &
|
||||
|
||||
"${WORK_DIR}/mc" ready verify
|
||||
}
|
||||
|
||||
function start_silo_pool_erasure_sets() {
|
||||
export MINIO_ROOT_USER=$ACCESS_KEY
|
||||
export MINIO_ROOT_PASSWORD=$SECRET_KEY
|
||||
export MINIO_ENDPOINTS="http://127.0.0.1:9000${WORK_DIR}/pool-disk-sets{1...4} http://127.0.0.1:9001${WORK_DIR}/pool-disk-sets{5...8}"
|
||||
"${SILO[@]}" server --address ":9000" >"$WORK_DIR/pool-silo-9000.log" 2>&1 &
|
||||
"${SILO[@]}" server --address ":9001" >"$WORK_DIR/pool-silo-9001.log" 2>&1 &
|
||||
|
||||
"${WORK_DIR}/mc" ready verify
|
||||
}
|
||||
|
||||
function start_silo_pool_erasure_sets_ipv6() {
|
||||
export MINIO_ROOT_USER=$ACCESS_KEY
|
||||
export MINIO_ROOT_PASSWORD=$SECRET_KEY
|
||||
export MINIO_ENDPOINTS="http://[::1]:9000${WORK_DIR}/pool-disk-sets-ipv6{1...4} http://[::1]:9001${WORK_DIR}/pool-disk-sets-ipv6{5...8}"
|
||||
"${SILO[@]}" server --address="[::1]:9000" >"$WORK_DIR/pool-silo-ipv6-9000.log" 2>&1 &
|
||||
"${SILO[@]}" server --address="[::1]:9001" >"$WORK_DIR/pool-silo-ipv6-9001.log" 2>&1 &
|
||||
|
||||
"${WORK_DIR}/mc" ready verify_ipv6
|
||||
}
|
||||
|
||||
function start_silo_dist_erasure() {
|
||||
export MINIO_ROOT_USER=$ACCESS_KEY
|
||||
export MINIO_ROOT_PASSWORD=$SECRET_KEY
|
||||
export MINIO_ENDPOINTS="http://127.0.0.1:9000${WORK_DIR}/dist-disk1 http://127.0.0.1:9001${WORK_DIR}/dist-disk2 http://127.0.0.1:9002${WORK_DIR}/dist-disk3 http://127.0.0.1:9003${WORK_DIR}/dist-disk4"
|
||||
for i in $(seq 0 3); do
|
||||
"${SILO[@]}" server --address ":900${i}" >"$WORK_DIR/dist-silo-900${i}.log" 2>&1 &
|
||||
done
|
||||
|
||||
"${WORK_DIR}/mc" ready verify
|
||||
}
|
||||
|
||||
function run_test_fs() {
|
||||
start_silo_fs
|
||||
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
rv=$?
|
||||
|
||||
pkill silo
|
||||
sleep 3
|
||||
|
||||
if [ "$rv" -ne 0 ]; then
|
||||
cat "$WORK_DIR/fs-silo.log"
|
||||
fi
|
||||
rm -f "$WORK_DIR/fs-silo.log"
|
||||
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function run_test_erasure_sets() {
|
||||
start_silo_erasure_sets
|
||||
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
rv=$?
|
||||
|
||||
pkill silo
|
||||
sleep 3
|
||||
|
||||
if [ "$rv" -ne 0 ]; then
|
||||
cat "$WORK_DIR/erasure-silo-sets.log"
|
||||
fi
|
||||
rm -f "$WORK_DIR/erasure-silo-sets.log"
|
||||
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function run_test_pool_erasure_sets() {
|
||||
start_silo_pool_erasure_sets
|
||||
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
rv=$?
|
||||
|
||||
pkill silo
|
||||
sleep 3
|
||||
|
||||
if [ "$rv" -ne 0 ]; then
|
||||
for i in $(seq 0 1); do
|
||||
echo "server$i log:"
|
||||
cat "$WORK_DIR/pool-silo-900$i.log"
|
||||
done
|
||||
fi
|
||||
|
||||
for i in $(seq 0 1); do
|
||||
rm -f "$WORK_DIR/pool-silo-900$i.log"
|
||||
done
|
||||
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function run_test_pool_erasure_sets_ipv6() {
|
||||
start_silo_pool_erasure_sets_ipv6
|
||||
|
||||
export SERVER_ENDPOINT="[::1]:9000"
|
||||
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
rv=$?
|
||||
|
||||
pkill silo
|
||||
sleep 3
|
||||
|
||||
if [ "$rv" -ne 0 ]; then
|
||||
for i in $(seq 0 1); do
|
||||
echo "server$i log:"
|
||||
cat "$WORK_DIR/pool-silo-ipv6-900$i.log"
|
||||
done
|
||||
fi
|
||||
|
||||
for i in $(seq 0 1); do
|
||||
rm -f "$WORK_DIR/pool-silo-ipv6-900$i.log"
|
||||
done
|
||||
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function run_test_erasure() {
|
||||
start_silo_erasure
|
||||
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
rv=$?
|
||||
|
||||
pkill silo
|
||||
sleep 3
|
||||
|
||||
if [ "$rv" -ne 0 ]; then
|
||||
cat "$WORK_DIR/erasure-silo.log"
|
||||
fi
|
||||
rm -f "$WORK_DIR/erasure-silo.log"
|
||||
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function run_test_dist_erasure() {
|
||||
start_silo_dist_erasure
|
||||
|
||||
(cd "$WORK_DIR" && "$FUNCTIONAL_TESTS")
|
||||
rv=$?
|
||||
|
||||
pkill silo
|
||||
sleep 3
|
||||
|
||||
if [ "$rv" -ne 0 ]; then
|
||||
echo "server1 log:"
|
||||
cat "$WORK_DIR/dist-silo-9000.log"
|
||||
echo "server2 log:"
|
||||
cat "$WORK_DIR/dist-silo-9001.log"
|
||||
echo "server3 log:"
|
||||
cat "$WORK_DIR/dist-silo-9002.log"
|
||||
echo "server4 log:"
|
||||
cat "$WORK_DIR/dist-silo-9003.log"
|
||||
fi
|
||||
|
||||
rm -f "$WORK_DIR/dist-silo-9000.log" "$WORK_DIR/dist-silo-9001.log" "$WORK_DIR/dist-silo-9002.log" "$WORK_DIR/dist-silo-9003.log"
|
||||
|
||||
return "$rv"
|
||||
}
|
||||
|
||||
function purge() {
|
||||
rm -rf "$1"
|
||||
}
|
||||
|
||||
function __init__() {
|
||||
echo "Initializing environment"
|
||||
mkdir -p "$WORK_DIR"
|
||||
mkdir -p "$SILO_CONFIG_DIR"
|
||||
mkdir -p "$MINT_DATA_DIR"
|
||||
|
||||
"$(git rev-parse --show-toplevel)/buildscripts/install-mcli.sh" "${WORK_DIR}/mc"
|
||||
|
||||
shred -n 1 -s 1M - 1>"$FILE_1_MB" 2>/dev/null
|
||||
shred -n 1 -s 65M - 1>"$FILE_65_MB" 2>/dev/null
|
||||
|
||||
## version is purposefully set to '3' for minio to migrate configuration file
|
||||
echo '{"version": "3", "credential": {"accessKey": "silo", "secretKey": "silo1234"}, "region": "us-east-1"}' >"$SILO_CONFIG_DIR/config.json"
|
||||
|
||||
"$(git rev-parse --show-toplevel)/buildscripts/install-verified-fixture.sh" \
|
||||
https://raw.githubusercontent.com/pgsty/mc/4c4dcc4b55baf238cd0c81030d77945b3828f157/functional-tests.sh \
|
||||
9b98c8152b294d567b9bc732869226dd4f65d0f4d84092dc066a900a66a9e22c \
|
||||
"$FUNCTIONAL_TESTS"
|
||||
|
||||
sed -i 's|-sS|-sSg|g' "$FUNCTIONAL_TESTS"
|
||||
chmod a+x "$FUNCTIONAL_TESTS"
|
||||
}
|
||||
|
||||
function main() {
|
||||
echo "Testing in FS setup"
|
||||
if ! run_test_fs; then
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing in Erasure setup"
|
||||
if ! run_test_erasure; then
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing in Distributed Erasure setup"
|
||||
if ! run_test_dist_erasure; then
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing in Erasure setup as sets"
|
||||
if ! run_test_erasure_sets; then
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing in Distributed Eraure expanded setup"
|
||||
if ! run_test_pool_erasure_sets; then
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Testing in Distributed Erasure expanded setup with ipv6"
|
||||
if ! run_test_pool_erasure_sets_ipv6; then
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
purge "$WORK_DIR"
|
||||
}
|
||||
|
||||
trap 'purge "$WORK_DIR"' EXIT
|
||||
__init__ "$@"
|
||||
main "$@"
|
||||
@@ -1,150 +0,0 @@
|
||||
#!/bin/bash -e
|
||||
#
|
||||
|
||||
set -E
|
||||
set -o pipefail
|
||||
|
||||
if [ ! -x "$PWD/silo" ]; then
|
||||
echo "Silo executable binary not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK_DIR="$PWD/.verify-$RANDOM"
|
||||
SILO_CONFIG_DIR="$WORK_DIR/.silo"
|
||||
SILO=("$PWD/silo" --config-dir "$SILO_CONFIG_DIR" server)
|
||||
|
||||
function start_silo_3_node() {
|
||||
export MINIO_ROOT_USER=silo
|
||||
export MINIO_ROOT_PASSWORD=silo1234
|
||||
export MINIO_ERASURE_SET_DRIVE_COUNT=6
|
||||
export MINIO_CI_CD=1
|
||||
|
||||
start_port=$1
|
||||
args=""
|
||||
for i in $(seq 1 3); do
|
||||
args="$args http://127.0.0.1:$((start_port + i))${WORK_DIR}/$i/1/ http://127.0.0.1:$((start_port + i))${WORK_DIR}/$i/2/ http://127.0.0.1:$((start_port + i))${WORK_DIR}/$i/3/ http://127.0.0.1:$((start_port + i))${WORK_DIR}/$i/4/ http://127.0.0.1:$((start_port + i))${WORK_DIR}/$i/5/ http://127.0.0.1:$((start_port + i))${WORK_DIR}/$i/6/"
|
||||
done
|
||||
|
||||
"${SILO[@]}" --address ":$((start_port + 1))" $args >"${WORK_DIR}/dist-silo-server1.log" 2>&1 &
|
||||
pid1=$!
|
||||
disown ${pid1}
|
||||
|
||||
"${SILO[@]}" --address ":$((start_port + 2))" $args >"${WORK_DIR}/dist-silo-server2.log" 2>&1 &
|
||||
pid2=$!
|
||||
disown $pid2
|
||||
|
||||
"${SILO[@]}" --address ":$((start_port + 3))" $args >"${WORK_DIR}/dist-silo-server3.log" 2>&1 &
|
||||
pid3=$!
|
||||
disown $pid3
|
||||
|
||||
export MC_HOST_mysilo="http://silo:silo1234@127.0.0.1:$((start_port + 1))"
|
||||
|
||||
timeout 15m /tmp/mc ready mysilo || fail
|
||||
|
||||
# Wait for all drives to be online and formatted
|
||||
while [ $(/tmp/mc admin info --json mysilo | jq '.info.servers[].drives[].state | select(. != "ok")' | wc -l) -gt 0 ]; do sleep 1; done
|
||||
# Wait for all drives to be healed
|
||||
while [ $(/tmp/mc admin info --json mysilo | jq '.info.servers[].drives[].healing | select(. != null) | select(. == true)' | wc -l) -gt 0 ]; do sleep 1; done
|
||||
|
||||
# Wait for Status: in MinIO output
|
||||
while true; do
|
||||
rv=$(check_online)
|
||||
if [ "$rv" != "1" ]; then
|
||||
# success
|
||||
break
|
||||
fi
|
||||
|
||||
# Check if we should retry
|
||||
retry=$((retry + 1))
|
||||
if [ $retry -le 20 ]; then
|
||||
sleep 5
|
||||
continue
|
||||
fi
|
||||
|
||||
# Failure
|
||||
fail
|
||||
done
|
||||
|
||||
if ! ps -p $pid1 1>&2 >/dev/null; then
|
||||
echo "silo-server-1 is not running." && fail
|
||||
fi
|
||||
|
||||
if ! ps -p $pid2 1>&2 >/dev/null; then
|
||||
echo "silo-server-2 is not running." && fail
|
||||
fi
|
||||
|
||||
if ! ps -p $pid3 1>&2 >/dev/null; then
|
||||
echo "silo-server-3 is not running." && fail
|
||||
fi
|
||||
|
||||
if ! pkill silo; then
|
||||
fail
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
if pgrep silo; then
|
||||
# forcibly killing, to proceed further properly.
|
||||
if ! pkill -9 silo; then
|
||||
echo "no Silo process running anymore, proceed."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function fail() {
|
||||
for i in $(seq 1 3); do
|
||||
echo "server$i log:"
|
||||
cat "${WORK_DIR}/dist-silo-server$i.log"
|
||||
done
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
}
|
||||
|
||||
function check_online() {
|
||||
if ! grep -q 'API:' ${WORK_DIR}/dist-silo-*.log; then
|
||||
echo "1"
|
||||
fi
|
||||
}
|
||||
|
||||
function purge() {
|
||||
echo rm -rf "$1"
|
||||
}
|
||||
|
||||
function __init__() {
|
||||
echo "Initializing environment"
|
||||
mkdir -p "$WORK_DIR"
|
||||
mkdir -p "$SILO_CONFIG_DIR"
|
||||
|
||||
## version is purposefully set to '3' for minio to migrate configuration file
|
||||
echo '{"version": "3", "credential": {"accessKey": "silo", "secretKey": "silo1234"}, "region": "us-east-1"}' >"$SILO_CONFIG_DIR/config.json"
|
||||
|
||||
if [ ! -f /tmp/mc ]; then
|
||||
"$(git rev-parse --show-toplevel)/buildscripts/install-mcli.sh" /tmp/mc
|
||||
fi
|
||||
}
|
||||
|
||||
function perform_test() {
|
||||
start_silo_3_node $2
|
||||
|
||||
echo "Testing Distributed Erasure setup healing of drives"
|
||||
echo "Remove the contents of the disks belonging to '${1}' erasure set"
|
||||
|
||||
rm -rf ${WORK_DIR}/${1}/*/
|
||||
|
||||
set -x
|
||||
start_silo_3_node $2
|
||||
}
|
||||
|
||||
function main() {
|
||||
# use same ports for all tests
|
||||
start_port=$(shuf -i 10000-65000 -n 1)
|
||||
|
||||
perform_test "2" ${start_port}
|
||||
perform_test "1" ${start_port}
|
||||
perform_test "3" ${start_port}
|
||||
}
|
||||
|
||||
(__init__ "$@" && main "$@")
|
||||
rv=$?
|
||||
purge "$WORK_DIR"
|
||||
exit "$rv"
|
||||
@@ -1,96 +0,0 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
set -E
|
||||
set -o pipefail
|
||||
set -x
|
||||
|
||||
if [ ! -x "$PWD/silo" ]; then
|
||||
echo "Silo executable binary not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
SILO_CONFIG_DIR="$WORK_DIR/.silo"
|
||||
SILO=("$PWD/silo" --config-dir "$SILO_CONFIG_DIR" server)
|
||||
|
||||
function start_silo() {
|
||||
start_port=$1
|
||||
|
||||
export MINIO_ROOT_USER=silo
|
||||
export MINIO_ROOT_PASSWORD=silo1234
|
||||
unset MINIO_KMS_AUTO_ENCRYPTION # do not auto-encrypt objects
|
||||
unset MINIO_CI_CD
|
||||
unset CI
|
||||
|
||||
args=()
|
||||
for i in $(seq 1 4); do
|
||||
args+=("http://localhost:$((start_port + i))${WORK_DIR}/mnt/disk$i/ ")
|
||||
done
|
||||
|
||||
for i in $(seq 1 4); do
|
||||
"${SILO[@]}" --address ":$((start_port + i))" ${args[@]} 2>&1 >"${WORK_DIR}/server$i.log" &
|
||||
done
|
||||
|
||||
# Wait until all nodes return 403
|
||||
for i in $(seq 1 4); do
|
||||
while [ "$(curl -m 1 -s -o /dev/null -w "%{http_code}" http://localhost:$((start_port + i)))" -ne "403" ]; do
|
||||
echo -n "."
|
||||
sleep 1
|
||||
done
|
||||
done
|
||||
|
||||
}
|
||||
|
||||
# Prepare fake disks with losetup
|
||||
function prepare_block_devices() {
|
||||
set -e
|
||||
mkdir -p ${WORK_DIR}/disks/ ${WORK_DIR}/mnt/
|
||||
sudo modprobe loop
|
||||
for i in 1 2 3 4; do
|
||||
dd if=/dev/zero of=${WORK_DIR}/disks/img.${i} bs=1M count=2000
|
||||
device=$(sudo losetup --find --show ${WORK_DIR}/disks/img.${i})
|
||||
sudo mkfs.ext4 -F ${device}
|
||||
mkdir -p ${WORK_DIR}/mnt/disk${i}/
|
||||
sudo mount ${device} ${WORK_DIR}/mnt/disk${i}/
|
||||
sudo chown "$(id -u):$(id -g)" ${device} ${WORK_DIR}/mnt/disk${i}/
|
||||
done
|
||||
set +e
|
||||
}
|
||||
|
||||
# Start a distributed MinIO setup, unmount one disk and check if it is formatted
|
||||
function main() {
|
||||
start_port=$(shuf -i 10000-65000 -n 1)
|
||||
start_silo ${start_port}
|
||||
|
||||
# Unmount the disk, after the unmount the device id
|
||||
# /tmp/xxx/mnt/disk4 will be the same as '/' and it
|
||||
# will be detected as root disk
|
||||
while [ "$u" != "0" ]; do
|
||||
sudo umount ${WORK_DIR}/mnt/disk4/
|
||||
u=$?
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Wait until MinIO self heal kicks in
|
||||
sleep 60
|
||||
|
||||
if [ -f ${WORK_DIR}/mnt/disk4/.minio.sys/format.json ]; then
|
||||
echo "A root disk is formatted unexpectedely"
|
||||
cat "${WORK_DIR}/server4.log"
|
||||
exit -1
|
||||
fi
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
pkill silo
|
||||
sudo umount ${WORK_DIR}/mnt/disk{1..3}/
|
||||
sudo rm /dev/minio-loopdisk*
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
|
||||
(prepare_block_devices)
|
||||
(main "$@")
|
||||
rv=$?
|
||||
|
||||
cleanup
|
||||
exit "$rv"
|
||||
@@ -1,166 +0,0 @@
|
||||
#!/bin/bash -e
|
||||
#
|
||||
|
||||
set -E
|
||||
set -o pipefail
|
||||
|
||||
if [ ! -x "$PWD/silo" ]; then
|
||||
echo "Silo executable binary not found in current directory"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK_DIR="$PWD/.verify-$RANDOM"
|
||||
SILO_CONFIG_DIR="$WORK_DIR/.silo"
|
||||
SILO=("$PWD/silo" --config-dir "$SILO_CONFIG_DIR" server)
|
||||
GOPATH=/tmp/gopath
|
||||
|
||||
function start_silo_3_node() {
|
||||
for i in $(seq 1 3); do
|
||||
rm "${WORK_DIR}/dist-silo-server$i.log"
|
||||
done
|
||||
|
||||
export MINIO_ROOT_USER=silo
|
||||
export MINIO_ROOT_PASSWORD=silo1234
|
||||
export MINIO_ERASURE_SET_DRIVE_COUNT=6
|
||||
export MINIO_CI_CD=1
|
||||
|
||||
first_time=$(find ${WORK_DIR}/ | grep format.json | wc -l)
|
||||
|
||||
start_port=$1
|
||||
args=""
|
||||
for d in $(seq 1 3 5); do
|
||||
args="$args http://127.0.0.1:$((start_port + 1))${WORK_DIR}/1/${d}/ http://127.0.0.1:$((start_port + 2))${WORK_DIR}/2/${d}/ http://127.0.0.1:$((start_port + 3))${WORK_DIR}/3/${d}/ "
|
||||
d=$((d + 1))
|
||||
args="$args http://127.0.0.1:$((start_port + 1))${WORK_DIR}/1/${d}/ http://127.0.0.1:$((start_port + 2))${WORK_DIR}/2/${d}/ http://127.0.0.1:$((start_port + 3))${WORK_DIR}/3/${d}/ "
|
||||
done
|
||||
|
||||
"${SILO[@]}" --address ":$((start_port + 1))" $args >"${WORK_DIR}/dist-silo-server1.log" 2>&1 &
|
||||
pid1=$!
|
||||
disown ${pid1}
|
||||
|
||||
"${SILO[@]}" --address ":$((start_port + 2))" $args >"${WORK_DIR}/dist-silo-server2.log" 2>&1 &
|
||||
pid2=$!
|
||||
disown $pid2
|
||||
|
||||
"${SILO[@]}" --address ":$((start_port + 3))" $args >"${WORK_DIR}/dist-silo-server3.log" 2>&1 &
|
||||
pid3=$!
|
||||
disown $pid3
|
||||
|
||||
export MC_HOST_mysilo="http://silo:silo1234@127.0.0.1:$((start_port + 1))"
|
||||
timeout 15m /tmp/mc ready mysilo || fail
|
||||
|
||||
[ ${first_time} -eq 0 ] && upload_objects
|
||||
[ ${first_time} -ne 0 ] && sleep 120
|
||||
|
||||
if ! ps -p $pid1 1>&2 >/dev/null; then
|
||||
echo "silo server 1 is not running" && fail
|
||||
fi
|
||||
|
||||
if ! ps -p $pid2 1>&2 >/dev/null; then
|
||||
echo "silo server 2 is not running" && fail
|
||||
fi
|
||||
|
||||
if ! ps -p $pid3 1>&2 >/dev/null; then
|
||||
echo "silo server 3 is not running" && fail
|
||||
fi
|
||||
|
||||
if ! pkill silo; then
|
||||
fail
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
if pgrep silo; then
|
||||
# forcibly killing, to proceed further properly.
|
||||
if ! pkill -9 silo; then
|
||||
echo "no Silo process running anymore, proceed."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function check_heal() {
|
||||
if ! grep -q 'API:' ${WORK_DIR}/dist-silo-*.log; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
for ((i = 0; i < 20; i++)); do
|
||||
test -f ${WORK_DIR}/$1/1/.minio.sys/format.json
|
||||
v1=$?
|
||||
nextInES=$(($1 + 1)) && [ $nextInES -gt 3 ] && nextInES=1
|
||||
foundFiles1=$(find ${WORK_DIR}/$1/1/ | grep -v .minio.sys | grep xl.meta | wc -l)
|
||||
foundFiles2=$(find ${WORK_DIR}/$nextInES/1/ | grep -v .minio.sys | grep xl.meta | wc -l)
|
||||
test $foundFiles1 -eq $foundFiles2
|
||||
v2=$?
|
||||
[ $v1 == 0 -a $v2 == 0 ] && return 0
|
||||
sleep 10
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
function purge() {
|
||||
rm -rf "$1"
|
||||
}
|
||||
|
||||
function fail() {
|
||||
for i in $(seq 1 3); do
|
||||
echo "server$i log:"
|
||||
cat "${WORK_DIR}/dist-silo-server$i.log"
|
||||
done
|
||||
pkill -9 silo
|
||||
echo "FAILED"
|
||||
purge "$WORK_DIR"
|
||||
exit 1
|
||||
}
|
||||
|
||||
function __init__() {
|
||||
echo "Initializing environment"
|
||||
mkdir -p "$WORK_DIR"
|
||||
mkdir -p "$SILO_CONFIG_DIR"
|
||||
|
||||
## version is purposefully set to '3' for minio to migrate configuration file
|
||||
echo '{"version": "3", "credential": {"accessKey": "silo", "secretKey": "silo1234"}, "region": "us-east-1"}' >"$SILO_CONFIG_DIR/config.json"
|
||||
|
||||
if [ ! -f /tmp/mc ]; then
|
||||
"$(git rev-parse --show-toplevel)/buildscripts/install-mcli.sh" /tmp/mc
|
||||
fi
|
||||
}
|
||||
|
||||
function upload_objects() {
|
||||
/tmp/mc mb mysilo/testbucket/
|
||||
for ((i = 0; i < 20; i++)); do
|
||||
echo "my content" | /tmp/mc pipe mysilo/testbucket/file-$i
|
||||
done
|
||||
}
|
||||
|
||||
function perform_test() {
|
||||
start_port=$2
|
||||
|
||||
start_silo_3_node $start_port
|
||||
|
||||
echo "Testing Distributed Erasure setup healing of drives"
|
||||
echo "Remove the contents of the disks belonging to '${1}' node"
|
||||
|
||||
rm -rf ${WORK_DIR}/${1}/*/
|
||||
|
||||
set -x
|
||||
start_silo_3_node $start_port
|
||||
|
||||
check_heal ${1}
|
||||
rv=$?
|
||||
if [ "$rv" == "1" ]; then
|
||||
fail
|
||||
fi
|
||||
}
|
||||
|
||||
function main() {
|
||||
# use same ports for all tests
|
||||
start_port=$(shuf -i 10000-65000 -n 1)
|
||||
|
||||
perform_test "2" ${start_port}
|
||||
perform_test "1" ${start_port}
|
||||
perform_test "3" ${start_port}
|
||||
}
|
||||
|
||||
(__init__ "$@" && main "$@")
|
||||
rv=$?
|
||||
purge "$WORK_DIR"
|
||||
exit "$rv"
|
||||
@@ -1,122 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_dir="$(cd "${script_dir}/.." && pwd)"
|
||||
baseline_commit="${HELM_LEGACY_COMMIT:-d88f46cce}"
|
||||
helm_image="${HELM_IMAGE:-alpine/helm:3.18.6@sha256:c6d8088ddb279625a2e1ca3b08b22c18c946d1f65c8b810f28f1597435a1134c}"
|
||||
work_dir="$(mktemp -d "${TMPDIR:-/tmp}/silo-helm.XXXXXX")"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "${work_dir}"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
cd "${repo_dir}"
|
||||
git cat-file -e "${baseline_commit}^{commit}"
|
||||
git archive "${baseline_commit}" helm/minio | tar -x -C "${work_dir}"
|
||||
|
||||
if command -v helm >/dev/null 2>&1; then
|
||||
new_chart="${repo_dir}/helm/silo"
|
||||
old_chart="${work_dir}/helm/minio"
|
||||
output_dir="${work_dir}"
|
||||
helm_run() {
|
||||
helm "$@"
|
||||
}
|
||||
else
|
||||
command -v docker >/dev/null 2>&1 || {
|
||||
echo "helm or docker is required" >&2
|
||||
exit 1
|
||||
}
|
||||
new_chart=/repo/helm/silo
|
||||
old_chart=/check/helm/minio
|
||||
output_dir=/check
|
||||
helm_run() {
|
||||
docker run --rm \
|
||||
-v "${repo_dir}:/repo:ro" \
|
||||
-v "${work_dir}:/check" \
|
||||
"${helm_image}" "$@"
|
||||
}
|
||||
fi
|
||||
|
||||
helm_run lint "${new_chart}"
|
||||
helm_run template silo "${new_chart}" \
|
||||
--namespace silo \
|
||||
--set rootUser=silo-admin \
|
||||
--set rootPassword=test-password-123456 >/dev/null
|
||||
helm_run template silo "${new_chart}" \
|
||||
--namespace silo \
|
||||
--set mode=standalone \
|
||||
--set replicas=1 \
|
||||
--set persistence.enabled=false \
|
||||
--set rootUser=silo-admin \
|
||||
--set rootPassword=test-password-123456 >/dev/null
|
||||
|
||||
# Exercise optional templates that the default render leaves dormant.
|
||||
helm_run template silo-all "${new_chart}" \
|
||||
--namespace silo \
|
||||
--set rootUser=silo-admin \
|
||||
--set rootPassword=test-password-123456 \
|
||||
--set tls.enabled=true \
|
||||
--set tls.certSecret=silo-tls \
|
||||
--set trustedCertsSecret=silo-trusted-ca \
|
||||
--set ingress.enabled=true \
|
||||
--set consoleIngress.enabled=true \
|
||||
--set networkPolicy.enabled=true \
|
||||
--set podDisruptionBudget.enabled=true \
|
||||
--set metrics.serviceMonitor.enabled=true \
|
||||
--set metrics.serviceMonitor.includeNode=true \
|
||||
--set 'buckets[0].name=chart-test' \
|
||||
--set 'buckets[0].policy=none' \
|
||||
--set 'buckets[0].purge=false' >/dev/null
|
||||
|
||||
# Existing values commonly address the historical myminio target. Render the
|
||||
# custom-command path explicitly so both the new and compatibility aliases are
|
||||
# protected by the release gate rather than only by a source-text assertion.
|
||||
custom_render="${work_dir}/custom-command.yaml"
|
||||
helm_run template silo-custom "${new_chart}" \
|
||||
--namespace silo \
|
||||
--set rootUser=silo-admin \
|
||||
--set rootPassword=test-password-123456 \
|
||||
--set-string 'customCommands[0].command=admin info myminio' \
|
||||
--show-only templates/configmap.yaml >"${custom_render}"
|
||||
for expected in \
|
||||
'alias set mysilo' \
|
||||
'alias set myminio' \
|
||||
'runCommand admin info myminio'; do
|
||||
grep -F -- "${expected}" "${custom_render}" >/dev/null || {
|
||||
echo "rendered custom command is missing: ${expected}" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
|
||||
old_render="${work_dir}/legacy.yaml"
|
||||
new_render="${work_dir}/candidate.yaml"
|
||||
helm_run template my-release "${old_chart}" \
|
||||
--namespace my-namespace \
|
||||
--set rootUser=legacy-admin \
|
||||
--set rootPassword=legacy-password-123456 >"${old_render}"
|
||||
helm_run template my-release "${new_chart}" \
|
||||
--namespace my-namespace \
|
||||
-f "${old_chart}/values.yaml" \
|
||||
--set rootUser=legacy-admin \
|
||||
--set rootPassword=legacy-password-123456 \
|
||||
--set nameOverride=minio \
|
||||
--set fullnameOverride=my-release-minio \
|
||||
--set serviceAccount.name=minio-sa \
|
||||
--set image.repository=pgsty/silo \
|
||||
--set mcImage.repository=pgsty/silo \
|
||||
--set-string image.tag=RELEASE.2026-08-04T00-00-00Z \
|
||||
--set-string mcImage.tag=RELEASE.2026-08-04T00-00-00Z >"${new_render}"
|
||||
|
||||
go run ./buildscripts/helm-migration-guard "${old_render}" "${new_render}"
|
||||
|
||||
helm_run package "${new_chart}" --destination "${output_dir}" >/dev/null
|
||||
test -s "${work_dir}/silo-7.0.2.tgz"
|
||||
if find "${work_dir}" -maxdepth 1 -type f -name 'minio-*.tgz' | grep -q .; then
|
||||
echo "Helm packaging emitted a legacy MinIO chart name" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Silo Helm lint, render, legacy-upgrade, and package checks passed"
|
||||
@@ -1,220 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo_dir="$(cd "${script_dir}/.." && pwd)"
|
||||
cd "${repo_dir}"
|
||||
|
||||
fail() {
|
||||
echo "Silo rebrand verification failed: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_file() {
|
||||
[ -f "$1" ] || fail "missing required file: $1"
|
||||
}
|
||||
|
||||
require_text() {
|
||||
local file="$1"
|
||||
local text="$2"
|
||||
grep -Fq -- "${text}" "${file}" || fail "${file} does not contain: ${text}"
|
||||
}
|
||||
|
||||
reject_text() {
|
||||
local file="$1"
|
||||
local text="$2"
|
||||
if grep -Fq -- "${text}" "${file}"; then
|
||||
fail "${file} still contains forbidden delivery text: ${text}"
|
||||
fi
|
||||
}
|
||||
|
||||
for file in \
|
||||
.github/goreleaser.yml \
|
||||
.github/nfpm.yml \
|
||||
buildscripts/package/lifecycle_test.sh \
|
||||
buildscripts/verify-helm-migration.sh \
|
||||
Dockerfile.goreleaser \
|
||||
Dockerfile.distroless \
|
||||
dockerscripts/download-static-curl.sh \
|
||||
dockerscripts/docker-entrypoint.sh \
|
||||
helm/silo/Chart.yaml \
|
||||
helm/silo/values.yaml \
|
||||
silo.service \
|
||||
silo.env \
|
||||
silo.sysusers; do
|
||||
require_file "${file}"
|
||||
done
|
||||
|
||||
for retired_path in \
|
||||
CNAME _config.yml index.yaml helm-reindex.sh helm/minio helm-releases \
|
||||
Dockerfile Dockerfile.cicd Dockerfile.hotfix Dockerfile.release \
|
||||
Dockerfile.release.old_cpu Dockerfile.scratch docker-buildx.sh \
|
||||
minio.service cmd/callhome.go buildscripts/upgrade-tests .github/logo.svg \
|
||||
docs/federation/lookup/bucket-lookup.png \
|
||||
docs/screenshots/Minio_Cloud_Native_Arch.jpg \
|
||||
docs/screenshots/Minio_Cloud_Native_Arch.png \
|
||||
docs/screenshots/Minio_Cloud_Native_Arch.svg \
|
||||
docs/screenshots/Architecture-diagram_distributed_8.jpg \
|
||||
docs/screenshots/Architecture-diagram_distributed_8.png \
|
||||
docs/screenshots/Architecture-diagram_distributed_8.svg \
|
||||
docs/screenshots/Architecture-diagram_distributed_16.jpg \
|
||||
docs/screenshots/Architecture-diagram_distributed_16.png \
|
||||
docs/screenshots/Architecture-diagram_distributed_16.svg \
|
||||
docs/screenshots/Architecture-diagram_distributed_nm.png \
|
||||
docs/screenshots/Example-1.jpg docs/screenshots/Example-1.png \
|
||||
docs/screenshots/Example-2.jpg docs/screenshots/Example-2.png \
|
||||
docs/screenshots/Example-3.jpg docs/screenshots/Example-3.png \
|
||||
docs/screenshots/pic1.png docs/screenshots/pic2.png \
|
||||
docs/metrics/prometheus/grafana/grafana-minio.png \
|
||||
docs/metrics/prometheus/grafana/bucket/grafana-bucket.png \
|
||||
docs/metrics/prometheus/grafana/node/grafana-node.png \
|
||||
docs/metrics/prometheus/grafana/replication/grafana-replication-cluster.png \
|
||||
docs/metrics/prometheus/grafana/replication/grafana-replication-node.png; do
|
||||
[ ! -e "${retired_path}" ] || fail "retired upstream delivery path remains: ${retired_path}"
|
||||
done
|
||||
|
||||
require_text .github/goreleaser.yml "binary: silo"
|
||||
require_text .github/goreleaser.yml 'name_template: "silo_{{ .Env.PKG_VERSION }}_{{ .Os }}_{{ .Arch }}"'
|
||||
require_text .github/goreleaser.yml "sboms:"
|
||||
require_text .github/goreleaser.yml "artifacts: archive"
|
||||
require_text .github/goreleaser.yml "cmd: cosign"
|
||||
# shellcheck disable=SC2016 # Match the literal GoReleaser template variable.
|
||||
require_text .github/goreleaser.yml 'signature: "${artifact}.sigstore.json"'
|
||||
require_text .github/nfpm.yml "name: silo"
|
||||
require_text .github/nfpm.yml "dst: /usr/bin/silo"
|
||||
require_text .github/nfpm.yml "dst: /etc/default/silo"
|
||||
require_text .github/nfpm.yml "dst: /usr/lib/sysusers.d/silo.conf"
|
||||
require_text buildscripts/package/lifecycle_test.sh "Silo package lifecycle checks passed"
|
||||
require_text buildscripts/verify-helm-migration.sh "Silo Helm lint, render, legacy-upgrade, and package checks passed"
|
||||
require_text silo.service "Conflicts=minio.service"
|
||||
require_text silo.service "EnvironmentFile=-/etc/default/minio"
|
||||
require_text silo.service "EnvironmentFile=-/etc/default/silo"
|
||||
# shellcheck disable=SC2016 # Match the literal service environment variables.
|
||||
require_text silo.service 'ExecStart=/usr/bin/silo server $MINIO_OPTS $MINIO_VOLUMES'
|
||||
require_text README.md "/etc/systemd/system/silo.service.d/10-legacy-user.conf"
|
||||
require_text README_ZH.md "/etc/systemd/system/silo.service.d/10-legacy-user.conf"
|
||||
require_text Dockerfile.goreleaser "COPY silo /usr/bin/silo"
|
||||
require_text Dockerfile.goreleaser 'CMD ["silo"]'
|
||||
require_text Dockerfile.goreleaser "MC_AMD64_SHA256="
|
||||
require_text Dockerfile.goreleaser "Published checksum drift"
|
||||
require_text Dockerfile.distroless 'COPY --chmod=0755 silo /usr/bin/silo'
|
||||
require_text Dockerfile.distroless 'ENTRYPOINT ["/usr/bin/silo"]'
|
||||
require_text Dockerfile.distroless '"/usr/bin/silo", "healthcheck", "ready"'
|
||||
require_text dockerscripts/download-static-curl.sh "sha256sum -c"
|
||||
require_text helm/silo/Chart.yaml "name: silo"
|
||||
require_text helm/silo/values.yaml "repository: pgsty/silo"
|
||||
require_text helm/silo/templates/deployment.yaml "/usr/bin/docker-entrypoint.sh silo server"
|
||||
require_text helm/silo/templates/statefulset.yaml "/usr/bin/docker-entrypoint.sh silo server"
|
||||
require_text docs/orchestration/docker-compose/docker-compose.yaml 'http://silo{1...4}/data{1...2}'
|
||||
require_text docs/resiliency/docker-compose.yaml 'http://silo{1...4}/data{1...8}'
|
||||
require_text docs/distributed/DECOMMISSION.md 'systemctl restart silo'
|
||||
# shellcheck disable=SC2016 # Match the literal shell variable.
|
||||
require_text docs/resiliency/resiliency-tests.sh 'docker exec resiliency-silo$NODE-1'
|
||||
require_text .github/workflows/release.yml "Attest downloadable release artifacts"
|
||||
require_text .github/workflows/release.yml "packages_checksums.txt"
|
||||
require_text .github/workflows/docker-release.yml "Attest multi-architecture image provenance"
|
||||
require_text .github/workflows/docker-release.yml "index.docker.io/pgsty/silo"
|
||||
|
||||
# Copyright notices credit both parties with fixed terms: upstream MinIO
|
||||
# development ends at its own last year, and the fork's own term starts when
|
||||
# the fork did. Deriving the upstream end year from the clock would extend
|
||||
# MinIO's copyright term every January.
|
||||
require_text cmd/build-constants.go 'upstreamCopyrightEndYear = "2025"'
|
||||
require_text cmd/build-constants.go 'forkCopyrightStartYear = "2025"'
|
||||
require_text cmd/main.go 'upstreamCopyrightEndYear'
|
||||
reject_text cmd/main.go 'CopyrightYear = strconv.Itoa(time.Now().Year())'
|
||||
require_text NOTICE 'MinIO Project, (C) 2015-2025 MinIO, Inc.'
|
||||
require_text NOTICE 'Silo Project modifications, (C) 2025-2026 PGSTY.'
|
||||
|
||||
# Contribution policy: no CLA, inbound=outbound, DCO sign-off enforced in CI.
|
||||
require_file .github/workflows/dco.yml
|
||||
require_text .github/workflows/dco.yml "Signed-off-by"
|
||||
require_text CONTRIBUTING.md "developercertificate.org"
|
||||
require_text CONTRIBUTING.md "No CLA"
|
||||
|
||||
for file in .github/nfpm.yml Dockerfile.goreleaser silo.service; do
|
||||
reject_text "${file}" "/usr/bin/minio"
|
||||
reject_text "${file}" "/usr/local/bin/minio"
|
||||
done
|
||||
reject_text Dockerfile.goreleaser "MINIO_UPDATE_MINISIGN_PUBKEY"
|
||||
reject_text buildscripts/minio-upgrade.sh "docker system prune"
|
||||
reject_text buildscripts/minio-upgrade.sh "docker volume prune"
|
||||
reject_text docs/orchestration/docker-compose/docker-compose.yaml 'http://minio{1...4}'
|
||||
reject_text docs/resiliency/docker-compose.yaml 'http://minio{1...4}'
|
||||
reject_text docs/distributed/DECOMMISSION.md 'systemctl restart minio'
|
||||
reject_text docs/resiliency/resiliency-tests.sh 'resiliency-minio'
|
||||
reject_text docs/resiliency/resiliency-tests.sh 'docker system prune'
|
||||
reject_text docs/resiliency/resiliency-tests.sh 'docker image prune'
|
||||
reject_text docs/resiliency/resiliency-tests.sh 'docker ps -q'
|
||||
|
||||
if grep -Ev '^[[:space:]]*(#|$)' silo.env | grep -q '='; then
|
||||
fail "silo.env must not contain active assignments that shadow /etc/default/minio"
|
||||
fi
|
||||
|
||||
if rg -n 'pgsty/minio:' .github/workflows Dockerfile.goreleaser helm/silo; then
|
||||
fail "an active delivery surface still publishes the frozen pgsty/minio image"
|
||||
fi
|
||||
|
||||
# The repository and its default branch are pgsty/silo and main. The invariant
|
||||
# is that the old name is never a live target, not that it is never spoken: the
|
||||
# READMEs have to name it to explain the rename and to point at the archived
|
||||
# artifacts, which is the opposite of stranding a reader on it. CONTRIBUTORS.md
|
||||
# also quotes historical issue titles.
|
||||
#
|
||||
# So two rules. First, no live URL may resolve to the old repository anywhere,
|
||||
# READMEs and CONTRIBUTORS.md included.
|
||||
old_repo_pattern='pgsty/minio(\.git)?([^[:alnum:]_.-]|$)'
|
||||
stale_repo_url="$(rg -n -e "github\.com/${old_repo_pattern}" -e "hub\.docker\.com/r/${old_repo_pattern}" \
|
||||
--glob '!.git/**' --glob '!dist/**' \
|
||||
--glob '!SILO_REBRANDING_MIGRATION.md' \
|
||||
--glob '!buildscripts/rebrand-guard/compat-baseline.json' . |
|
||||
sed 's#^\./##' | grep -v '^buildscripts/verify-rebrand\.sh:' || true)"
|
||||
if [ -n "${stale_repo_url}" ]; then
|
||||
printf '%s\n' "${stale_repo_url}" >&2
|
||||
fail "a link still resolves to the pre-rename pgsty/minio repository"
|
||||
fi
|
||||
|
||||
# Second, the bare name may only appear where it is deliberate: the pinned
|
||||
# pre-rebrand image digest in the upgrade test, the two guards that refuse a
|
||||
# legacy image, the two READMEs that document the rename and the archived
|
||||
# minio branch, and historical issue titles in CONTRIBUTORS.md.
|
||||
repo_guard_allowlist='^(buildscripts/minio-upgrade\.sh|buildscripts/verify-rebrand\.sh|buildscripts/helm-migration-guard/main\.go|README\.md|README_ZH\.md|CONTRIBUTORS\.md):'
|
||||
stale_repo="$(rg -n "${old_repo_pattern}" --glob '!.git/**' --glob '!dist/**' \
|
||||
--glob '!SILO_REBRANDING_MIGRATION.md' \
|
||||
--glob '!buildscripts/rebrand-guard/compat-baseline.json' . |
|
||||
sed 's#^\./##' | grep -Ev "${repo_guard_allowlist}" || true)"
|
||||
if [ -n "${stale_repo}" ]; then
|
||||
printf '%s\n' "${stale_repo}" >&2
|
||||
fail "a source reference still names the pre-rename pgsty/minio repository"
|
||||
fi
|
||||
|
||||
stale_branch="$(rg -n 'pgsty/silo/(blob/|tree/|raw/)?master' \
|
||||
--glob '!.git/**' --glob '!dist/**' . || true)"
|
||||
if [ -n "${stale_branch}" ]; then
|
||||
printf '%s\n' "${stale_branch}" >&2
|
||||
fail "a link still targets the retired master branch; raw and Actions URLs do not follow a branch rename"
|
||||
fi
|
||||
|
||||
for workflow in .github/workflows/go.yml .github/workflows/vulncheck.yml; do
|
||||
if rg -q '^\s+- master$' "${workflow}"; then
|
||||
fail "${workflow} still filters on master and would go silently dormant on main"
|
||||
fi
|
||||
require_text "${workflow}" " - main"
|
||||
done
|
||||
|
||||
network_hits="$(rg -n --glob '*.go' --glob '!**/*_test.go' \
|
||||
'https?://[^"`[:space:]]*(dl\.min\.io|subnet\.min\.io|api\.min\.io|slack\.min\.io|play\.min\.io)' \
|
||||
cmd internal || true)"
|
||||
network_hits="$(printf '%s\n' "${network_hits}" | grep -Ev '^[^:]+:[0-9]+:[[:space:]]*//' || true)"
|
||||
if [ -n "${network_hits}" ]; then
|
||||
printf '%s\n' "${network_hits}" >&2
|
||||
fail "runtime code still contains an upstream MinIO service endpoint"
|
||||
fi
|
||||
|
||||
require_text cmd/build-constants.go 'MinioReleaseBaseURL = ""'
|
||||
require_text cmd/globals.go "globalInplaceUpdateDisabled = true"
|
||||
reject_text cmd/globals.go "subnetAdminPublicKey"
|
||||
reject_text cmd/admin-handlers.go "getSubnetAdminPublicKey"
|
||||
|
||||
echo "Silo delivery and runtime rebrand checks passed"
|
||||
@@ -1,280 +0,0 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
xhttp "github.com/minio/minio/internal/http"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
"github.com/pgsty/silo-pkg/v3/policy"
|
||||
)
|
||||
|
||||
// Data types used for returning dummy access control
|
||||
// policy XML, these variables shouldn't be used elsewhere
|
||||
// they are only defined to be used in this file alone.
|
||||
type grantee struct {
|
||||
XMLNS string `xml:"xmlns:xsi,attr"`
|
||||
XMLXSI string `xml:"xsi:type,attr"`
|
||||
Type string `xml:"Type"`
|
||||
ID string `xml:"ID,omitempty"`
|
||||
DisplayName string `xml:"DisplayName,omitempty"`
|
||||
URI string `xml:"URI,omitempty"`
|
||||
}
|
||||
|
||||
type grant struct {
|
||||
Grantee grantee `xml:"Grantee"`
|
||||
Permission string `xml:"Permission"`
|
||||
}
|
||||
|
||||
type accessControlPolicy struct {
|
||||
XMLName xml.Name `xml:"AccessControlPolicy"`
|
||||
Owner Owner `xml:"Owner"`
|
||||
AccessControlList struct {
|
||||
Grants []grant `xml:"Grant"`
|
||||
} `xml:"AccessControlList"`
|
||||
}
|
||||
|
||||
// PutBucketACLHandler - PUT Bucket ACL
|
||||
// -----------------
|
||||
// This operation uses the ACL subresource
|
||||
// to set ACL for a bucket, this is a dummy call
|
||||
// only responds success if the ACL is private.
|
||||
func (api objectAPIHandlers) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "PutBucketACL")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
objAPI := api.ObjectAPI()
|
||||
if objAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Allow putBucketACL if policy action is set, since this is a dummy call
|
||||
// we are simply re-purposing the bucketPolicyAction.
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketPolicyAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Before proceeding validate if bucket exists.
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
aclHeader := r.Header.Get(xhttp.AmzACL)
|
||||
if aclHeader == "" {
|
||||
acl := &accessControlPolicy{}
|
||||
if err = xmlDecoder(r.Body, acl, r.ContentLength); err != nil {
|
||||
if terr, ok := err.(*xml.SyntaxError); ok && terr.Msg == io.EOF.Error() {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrMalformedXML),
|
||||
r.URL)
|
||||
return
|
||||
}
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if len(acl.AccessControlList.Grants) == 0 {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if acl.AccessControlList.Grants[0].Permission != "FULL_CONTROL" {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if aclHeader != "" && aclHeader != "private" {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// GetBucketACLHandler - GET Bucket ACL
|
||||
// -----------------
|
||||
// This operation uses the ACL
|
||||
// subresource to return the ACL of a specified bucket.
|
||||
func (api objectAPIHandlers) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetBucketACL")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
|
||||
objAPI := api.ObjectAPI()
|
||||
if objAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Allow getBucketACL if policy action is set, since this is a dummy call
|
||||
// we are simply re-purposing the bucketPolicyAction.
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Before proceeding validate if bucket exists.
|
||||
_, err := objAPI.GetBucketInfo(ctx, bucket, BucketOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
acl := &accessControlPolicy{}
|
||||
acl.AccessControlList.Grants = append(acl.AccessControlList.Grants, grant{
|
||||
Grantee: grantee{
|
||||
XMLNS: "http://www.w3.org/2001/XMLSchema-instance",
|
||||
XMLXSI: "CanonicalUser",
|
||||
Type: "CanonicalUser",
|
||||
},
|
||||
Permission: "FULL_CONTROL",
|
||||
})
|
||||
|
||||
if err := xml.NewEncoder(w).Encode(acl); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// PutObjectACLHandler - PUT Object ACL
|
||||
// -----------------
|
||||
// This operation uses the ACL subresource
|
||||
// to set ACL for a bucket, this is a dummy call
|
||||
// only responds success if the ACL is private.
|
||||
func (api objectAPIHandlers) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "PutObjectACL")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
object, err := unescapePath(vars["object"])
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
objAPI := api.ObjectAPI()
|
||||
if objAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Allow putObjectACL if policy action is set, since this is a dummy call
|
||||
// we are simply re-purposing the bucketPolicyAction.
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.PutBucketPolicyAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Before proceeding validate if object exists.
|
||||
_, err = objAPI.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
aclHeader := r.Header.Get(xhttp.AmzACL)
|
||||
if aclHeader == "" {
|
||||
acl := &accessControlPolicy{}
|
||||
if err = xmlDecoder(r.Body, acl, r.ContentLength); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if len(acl.AccessControlList.Grants) == 0 {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if acl.AccessControlList.Grants[0].Permission != "FULL_CONTROL" {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if aclHeader != "" && aclHeader != "private" {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, NotImplemented{}), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// GetObjectACLHandler - GET Object ACL
|
||||
// -----------------
|
||||
// This operation uses the ACL
|
||||
// subresource to return the ACL of a specified object.
|
||||
func (api objectAPIHandlers) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(r, w, "GetObjectACL")
|
||||
|
||||
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
object, err := unescapePath(vars["object"])
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
objAPI := api.ObjectAPI()
|
||||
if objAPI == nil {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Allow getObjectACL if policy action is set, since this is a dummy call
|
||||
// we are simply re-purposing the bucketPolicyAction.
|
||||
if s3Error := checkRequestAuthType(ctx, r, policy.GetBucketPolicyAction, bucket, ""); s3Error != ErrNone {
|
||||
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Error), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Before proceeding validate if object exists.
|
||||
_, err = objAPI.GetObjectInfo(ctx, bucket, object, ObjectOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
acl := &accessControlPolicy{}
|
||||
acl.AccessControlList.Grants = append(acl.AccessControlList.Grants, grant{
|
||||
Grantee: grantee{
|
||||
XMLNS: "http://www.w3.org/2001/XMLSchema-instance",
|
||||
XMLXSI: "CanonicalUser",
|
||||
Type: "CanonicalUser",
|
||||
},
|
||||
Permission: "FULL_CONTROL",
|
||||
})
|
||||
if err := xml.NewEncoder(w).Encode(acl); err != nil {
|
||||
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1,348 +0,0 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/mux"
|
||||
)
|
||||
|
||||
func corsAdminRequest(t *testing.T, cred auth.Credentials, method, path string, body []byte) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
router := mux.NewRouter()
|
||||
registerAdminRouter(router, true)
|
||||
req, err := newTestSignedRequestV4(method, adminPathPrefix+adminAPIVersionPrefix+path,
|
||||
int64(len(body)), bytes.NewReader(body), cred.AccessKey, cred.SecretKey, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("admin %s: %d: %s", path, rec.Code, rec.Body.String())
|
||||
}
|
||||
return rec
|
||||
}
|
||||
|
||||
func corsImportReport(t *testing.T, rec *httptest.ResponseRecorder) madmin.BucketMetaImportErrs {
|
||||
t.Helper()
|
||||
var rpt madmin.BucketMetaImportErrs
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &rpt); err != nil {
|
||||
t.Fatalf("import report %q: %v", rec.Body.String(), err)
|
||||
}
|
||||
return rpt
|
||||
}
|
||||
|
||||
func corsZip(t *testing.T, entries map[string][]byte) []byte {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
zw := zip.NewWriter(&buf)
|
||||
for name, data := range entries {
|
||||
w, err := zw.Create(name)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = w.Write(data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := zw.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// corsCorruptedZip builds an archive holding a stored (uncompressed) cors.xml
|
||||
// whose payload is altered after the checksum is computed, plus the given
|
||||
// companion entries. The altered document stays well formed, so only the zip
|
||||
// checksum tells the two apart.
|
||||
func corsCorruptedZip(t *testing.T, name string, doc []byte, others map[string][]byte) []byte {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
zw := zip.NewWriter(&buf)
|
||||
w, err := zw.CreateHeader(&zip.FileHeader{Name: name, Method: zip.Store})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = w.Write(doc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for other, data := range others {
|
||||
ow, err := zw.Create(other)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err = ow.Write(data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err = zw.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw := buf.Bytes()
|
||||
at := bytes.Index(raw, []byte("app.example.com"))
|
||||
if at < 0 {
|
||||
t.Fatalf("stored CORS payload not found in archive")
|
||||
}
|
||||
raw[at] = 'A'
|
||||
return raw
|
||||
}
|
||||
|
||||
// TestAdminBucketMetadataCORSRoundTrip covers the export/import round trip for
|
||||
// per-bucket CORS, per-file error reporting for an invalid document, and that
|
||||
// an archive without cors.xml leaves an existing configuration alone.
|
||||
func TestAdminBucketMetadataCORSRoundTrip(t *testing.T) {
|
||||
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: func(obj ObjectLayer, instanceType, bucket string, _ http.Handler, cred auth.Credentials, t *testing.T) {
|
||||
corsXML := []byte(testSiteReplicationCORSDoc)
|
||||
if _, err := updateLocalBucketCORSMetadata(t.Context(), obj, bucket, corsXML); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Export must carry the stored document verbatim.
|
||||
rec := corsAdminRequest(t, cred, http.MethodGet, "/export-bucket-metadata?bucket="+bucket, nil)
|
||||
archive := rec.Body.Bytes()
|
||||
zr, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var exported []byte
|
||||
for _, f := range zr.File {
|
||||
if f.Name != bucket+"/"+bucketCorsConfig {
|
||||
continue
|
||||
}
|
||||
r, err := f.Open()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
exported, err = io.ReadAll(r)
|
||||
r.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if !bytes.Equal(exported, corsXML) {
|
||||
t.Fatalf("%s: exported CORS = %q, want %q", instanceType, exported, corsXML)
|
||||
}
|
||||
|
||||
// Drop the configuration: the archive must then omit the entry.
|
||||
if _, err = updateLocalBucketCORSMetadata(t.Context(), obj, bucket, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err = globalBucketMetadataSys.GetCorsConfigXML(bucket); err == nil {
|
||||
t.Fatalf("%s: CORS still present before restore", instanceType)
|
||||
}
|
||||
rec = corsAdminRequest(t, cred, http.MethodGet, "/export-bucket-metadata?bucket="+bucket, nil)
|
||||
empty := rec.Body.Bytes()
|
||||
zr, err = zip.NewReader(bytes.NewReader(empty), int64(len(empty)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, f := range zr.File {
|
||||
if f.Name == bucket+"/"+bucketCorsConfig {
|
||||
t.Fatalf("%s: export emitted %s for a bucket without CORS", instanceType, f.Name)
|
||||
}
|
||||
}
|
||||
|
||||
rec = corsAdminRequest(t, cred, http.MethodPut, "/import-bucket-metadata", archive)
|
||||
if st := corsImportReport(t, rec).Buckets[bucket]; !st.Cors.IsSet || st.Cors.Err != "" {
|
||||
t.Fatalf("%s: import report cors = %+v", instanceType, st.Cors)
|
||||
}
|
||||
stored, storedAt, err := globalBucketMetadataSys.GetCorsConfigXML(bucket)
|
||||
if err != nil || !bytes.Equal(stored, corsXML) {
|
||||
t.Fatalf("%s: restored CORS = %q, err = %v", instanceType, stored, err)
|
||||
}
|
||||
created, err := globalBucketMetadataSys.CreatedAt(bucket)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !storedAt.After(created) {
|
||||
t.Fatalf("%s: restored CORS timestamp %v is not after bucket creation %v", instanceType, storedAt, created)
|
||||
}
|
||||
|
||||
// An archive without cors.xml must not remove the configuration.
|
||||
corsAdminRequest(t, cred, http.MethodPut, "/import-bucket-metadata",
|
||||
corsZip(t, map[string][]byte{bucket + "/quota.json": []byte(`{"quota":0}`)}))
|
||||
if stored, _, err = globalBucketMetadataSys.GetCorsConfigXML(bucket); err != nil || !bytes.Equal(stored, corsXML) {
|
||||
t.Fatalf("%s: import without cors.xml changed CORS: %q, err = %v", instanceType, stored, err)
|
||||
}
|
||||
|
||||
// A bucket the import itself creates must still land above its own
|
||||
// creation time, otherwise CORS replication would drop the restore.
|
||||
fresh := "cors-import-created-bucket"
|
||||
rec = corsAdminRequest(t, cred, http.MethodPut, "/import-bucket-metadata",
|
||||
corsZip(t, map[string][]byte{fresh + "/" + bucketCorsConfig: corsXML}))
|
||||
if st := corsImportReport(t, rec).Buckets[fresh]; !st.Cors.IsSet || st.Cors.Err != "" {
|
||||
t.Fatalf("%s: fresh bucket import report cors = %+v", instanceType, st.Cors)
|
||||
}
|
||||
freshStored, freshAt, err := globalBucketMetadataSys.GetCorsConfigXML(fresh)
|
||||
if err != nil || !bytes.Equal(freshStored, corsXML) {
|
||||
t.Fatalf("%s: fresh bucket CORS = %q, err = %v", instanceType, freshStored, err)
|
||||
}
|
||||
freshCreated, err := globalBucketMetadataSys.CreatedAt(fresh)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !freshAt.After(freshCreated) {
|
||||
t.Fatalf("%s: fresh bucket CORS timestamp %v is not after creation %v", instanceType, freshAt, freshCreated)
|
||||
}
|
||||
|
||||
// An invalid document must fail loudly for that bucket and change nothing.
|
||||
rec = corsAdminRequest(t, cred, http.MethodPut, "/import-bucket-metadata",
|
||||
corsZip(t, map[string][]byte{bucket + "/" + bucketCorsConfig: []byte("<CORSConfiguration><CORSRule>")}))
|
||||
if st := corsImportReport(t, rec).Buckets[bucket]; st.Cors.Err == "" {
|
||||
t.Fatalf("%s: invalid CORS import reported no error: %+v", instanceType, st)
|
||||
}
|
||||
if stored, _, err = globalBucketMetadataSys.GetCorsConfigXML(bucket); err != nil || !bytes.Equal(stored, corsXML) {
|
||||
t.Fatalf("%s: invalid CORS import changed stored config: %q, err = %v", instanceType, stored, err)
|
||||
}
|
||||
|
||||
// A well formed document carried by a corrupt zip entry must be
|
||||
// rejected too, leaving the stored document and its timestamp alone
|
||||
// while the other configs in the same archive still apply.
|
||||
_, corsAt, err := globalBucketMetadataSys.GetCorsConfigXML(bucket)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec = corsAdminRequest(t, cred, http.MethodPut, "/import-bucket-metadata",
|
||||
corsCorruptedZip(t, bucket+"/"+bucketCorsConfig, corsXML,
|
||||
map[string][]byte{bucket + "/quota.json": []byte(`{"quota":4096,"quotatype":"hard"}`)}))
|
||||
st := corsImportReport(t, rec).Buckets[bucket]
|
||||
if st.Cors.Err == "" {
|
||||
t.Fatalf("%s: corrupt CORS entry reported no error: %+v", instanceType, st)
|
||||
}
|
||||
if !st.Quota.IsSet || st.Quota.Err != "" {
|
||||
t.Fatalf("%s: corrupt CORS entry blocked the neighboring quota: %+v", instanceType, st.Quota)
|
||||
}
|
||||
stored, storedAt, err = globalBucketMetadataSys.GetCorsConfigXML(bucket)
|
||||
if err != nil || !bytes.Equal(stored, corsXML) || !storedAt.Equal(corsAt) {
|
||||
t.Fatalf("%s: corrupt CORS entry changed stored config: %q at %v (was %v), err = %v", instanceType, stored, storedAt, corsAt, err)
|
||||
}
|
||||
quota, _, err := globalBucketMetadataSys.GetQuotaConfig(t.Context(), bucket)
|
||||
if err != nil || quota == nil || quota.Quota != 4096 {
|
||||
t.Fatalf("%s: neighboring quota not applied: %+v, err = %v", instanceType, quota, err)
|
||||
}
|
||||
}})
|
||||
}
|
||||
|
||||
// corsPeerStub is a stand-in site-replication peer. It records every
|
||||
// SRBucketMeta it is asked to apply and answers with status.
|
||||
func corsPeerStub(t *testing.T, applied chan<- madmin.SRBucketMeta, status int) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPut && applied != nil {
|
||||
var item madmin.SRBucketMeta
|
||||
if err := json.NewDecoder(r.Body).Decode(&item); err != nil {
|
||||
t.Errorf("decode peer apply: %v", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
applied <- item
|
||||
}
|
||||
w.WriteHeader(status)
|
||||
}))
|
||||
}
|
||||
|
||||
// TestAdminBucketMetadataCORSImportReplicatesPastPeerFailure pins that an
|
||||
// imported CORS document reaches the reachable peers even when the shared
|
||||
// bucket metadata hook failed against an unreachable one, and that both
|
||||
// failures are still reported for the bucket.
|
||||
func TestAdminBucketMetadataCORSImportReplicatesPastPeerFailure(t *testing.T) {
|
||||
ExecObjectLayerAPITest(ExecObjectLayerAPITestArgs{t: t, objAPITest: func(obj ObjectLayer, instanceType, bucket string, _ http.Handler, cred auth.Credentials, t *testing.T) {
|
||||
ctx := t.Context()
|
||||
corsXML := []byte(testSiteReplicationCORSDoc)
|
||||
|
||||
healthyApplies := make(chan madmin.SRBucketMeta, 4)
|
||||
healthy := corsPeerStub(t, healthyApplies, http.StatusOK)
|
||||
defer healthy.Close()
|
||||
broken := corsPeerStub(t, nil, http.StatusBadRequest)
|
||||
defer broken.Close()
|
||||
|
||||
// With site replication on, admin requests resolve their token signing
|
||||
// key through the site replicator account, so it has to exist.
|
||||
serviceCred, err := auth.CreateCredentials(siteReplicatorSvcAcc, "cors-import-service-secret")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
serviceCred.ParentUser = cred.AccessKey
|
||||
if _, err = globalIAMSys.store.AddServiceAccount(ctx, serviceCred); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer globalIAMSys.DeleteServiceAccount(ctx, serviceCred.AccessKey, false)
|
||||
globalSiteReplicatorCred.Set(serviceCred.SecretKey)
|
||||
defer globalSiteReplicatorCred.Set("")
|
||||
|
||||
globalSiteReplicationSys.Lock()
|
||||
oldEnabled, oldState := globalSiteReplicationSys.enabled, globalSiteReplicationSys.state
|
||||
globalSiteReplicationSys.enabled = true
|
||||
globalSiteReplicationSys.state = srState{
|
||||
Name: "cors-import-test",
|
||||
ServiceAccountAccessKey: serviceCred.AccessKey,
|
||||
Peers: map[string]madmin.PeerInfo{
|
||||
globalDeploymentID(): {Name: "local", DeploymentID: globalDeploymentID()},
|
||||
"peer-healthy": {Name: "healthy", DeploymentID: "peer-healthy", Endpoint: healthy.URL},
|
||||
"peer-broken": {Name: "broken", DeploymentID: "peer-broken", Endpoint: broken.URL},
|
||||
},
|
||||
}
|
||||
globalSiteReplicationSys.Unlock()
|
||||
defer func() {
|
||||
globalSiteReplicationSys.Lock()
|
||||
globalSiteReplicationSys.enabled, globalSiteReplicationSys.state = oldEnabled, oldState
|
||||
globalSiteReplicationSys.Unlock()
|
||||
}()
|
||||
|
||||
rec := corsAdminRequest(t, cred, http.MethodPut, "/import-bucket-metadata",
|
||||
corsZip(t, map[string][]byte{
|
||||
bucket + "/" + bucketCorsConfig: corsXML,
|
||||
bucket + "/quota.json": []byte(`{"quota":8192,"quotatype":"hard"}`),
|
||||
}))
|
||||
st := corsImportReport(t, rec).Buckets[bucket]
|
||||
if !st.Cors.IsSet || st.Cors.Err != "" {
|
||||
t.Fatalf("%s: import report cors = %+v", instanceType, st.Cors)
|
||||
}
|
||||
stored, storedAt, err := globalBucketMetadataSys.GetCorsConfigXML(bucket)
|
||||
if err != nil || !bytes.Equal(stored, corsXML) {
|
||||
t.Fatalf("%s: stored CORS = %q, err = %v", instanceType, stored, err)
|
||||
}
|
||||
|
||||
// The reachable peer must have been told about the CORS document,
|
||||
// carrying exactly the timestamp that was saved locally.
|
||||
var corsSeen, sharedSeen bool
|
||||
for range 2 {
|
||||
select {
|
||||
case item := <-healthyApplies:
|
||||
if item.Type != madmin.SRBucketMetaTypeCorsConfig {
|
||||
sharedSeen = item.Bucket == bucket && item.Quota != nil
|
||||
continue
|
||||
}
|
||||
if item.Bucket != bucket || item.Cors == nil || !item.UpdatedAt.Equal(storedAt) {
|
||||
t.Fatalf("%s: peer CORS event = %#v, want %s at %v", instanceType, item, bucket, storedAt)
|
||||
}
|
||||
payload, decErr := base64.StdEncoding.Strict().DecodeString(*item.Cors)
|
||||
if decErr != nil || !bytes.Equal(payload, corsXML) {
|
||||
t.Fatalf("%s: peer CORS payload = %q, err = %v", instanceType, payload, decErr)
|
||||
}
|
||||
corsSeen = true
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatalf("%s: healthy peer received no further events (shared=%v cors=%v)", instanceType, sharedSeen, corsSeen)
|
||||
}
|
||||
}
|
||||
if !sharedSeen || !corsSeen {
|
||||
t.Fatalf("%s: healthy peer events shared=%v cors=%v, want both", instanceType, sharedSeen, corsSeen)
|
||||
}
|
||||
|
||||
// Both hook failures against the unreachable peer stay reported.
|
||||
if got := strings.Count(st.Err, "->broken:"); got != 2 {
|
||||
t.Fatalf("%s: bucket error mentions the broken peer %d times, want 2: %q", instanceType, got, st.Err)
|
||||
}
|
||||
}})
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,263 +0,0 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/minio/kms-go/kes"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/pgsty/silo-pkg/v3/policy"
|
||||
)
|
||||
|
||||
// validateAdminReq will validate request against and return whether it is allowed.
|
||||
// If any of the supplied actions are allowed it will be successful.
|
||||
// If nil ObjectLayer is returned, the operation is not permitted.
|
||||
// When nil ObjectLayer has been returned an error has always been sent to w.
|
||||
func validateAdminReq(ctx context.Context, w http.ResponseWriter, r *http.Request, actions ...policy.AdminAction) (ObjectLayer, auth.Credentials) {
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return nil, auth.Credentials{}
|
||||
}
|
||||
|
||||
for _, action := range actions {
|
||||
// Validate request signature.
|
||||
cred, adminAPIErr := checkAdminRequestAuth(ctx, r, action, "")
|
||||
switch adminAPIErr {
|
||||
case ErrNone:
|
||||
return objectAPI, cred
|
||||
case ErrAccessDenied:
|
||||
// Try another
|
||||
continue
|
||||
default:
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(adminAPIErr), r.URL)
|
||||
return nil, cred
|
||||
}
|
||||
}
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||
return nil, auth.Credentials{}
|
||||
}
|
||||
|
||||
// AdminError - is a generic error for all admin APIs.
|
||||
type AdminError struct {
|
||||
Code string
|
||||
Message string
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
func (ae AdminError) Error() string {
|
||||
return ae.Message
|
||||
}
|
||||
|
||||
func toAdminAPIErr(ctx context.Context, err error) APIError {
|
||||
if err == nil {
|
||||
return noError
|
||||
}
|
||||
|
||||
var apiErr APIError
|
||||
switch e := err.(type) {
|
||||
case policy.Error:
|
||||
apiErr = APIError{
|
||||
Code: "XMinioMalformedIAMPolicy",
|
||||
Description: e.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case config.ErrConfigNotFound:
|
||||
apiErr = APIError{
|
||||
Code: "XMinioConfigNotFoundError",
|
||||
Description: e.Error(),
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
}
|
||||
case config.ErrConfigGeneric:
|
||||
apiErr = APIError{
|
||||
Code: "XMinioConfigError",
|
||||
Description: e.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case AdminError:
|
||||
apiErr = APIError{
|
||||
Code: e.Code,
|
||||
Description: e.Message,
|
||||
HTTPStatusCode: e.StatusCode,
|
||||
}
|
||||
case SRError:
|
||||
apiErr = errorCodes.ToAPIErrWithErr(e.Code, e.Cause)
|
||||
case decomError:
|
||||
apiErr = APIError{
|
||||
Code: "XMinioDecommissionNotAllowed",
|
||||
Description: e.Err,
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
default:
|
||||
switch {
|
||||
case errors.Is(err, errTooManyPolicies):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioAdminInvalidRequest",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errors.Is(err, errDecommissionAlreadyRunning):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioDecommissionNotAllowed",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errors.Is(err, errDecommissionComplete):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioDecommissionNotAllowed",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errors.Is(err, errDecommissionRebalanceAlreadyRunning):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioDecommissionNotAllowed",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errors.Is(err, errRebalanceDecommissionAlreadyRunning):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioRebalanceNotAllowed",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errors.Is(err, errConfigNotFound):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioConfigError",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusNotFound,
|
||||
}
|
||||
case errors.Is(err, errIAMActionNotAllowed):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioIAMActionNotAllowed",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusForbidden,
|
||||
}
|
||||
case errors.Is(err, errIAMServiceAccountNotAllowed):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioIAMServiceAccountNotAllowed",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errors.Is(err, errIAMNotInitialized):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioIAMNotInitialized",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusServiceUnavailable,
|
||||
}
|
||||
case errors.Is(err, errPolicyInUse):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioIAMPolicyInUse",
|
||||
Description: "The policy cannot be removed, as it is in use",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errors.Is(err, errSessionPolicyTooLarge):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioIAMServiceAccountSessionPolicyTooLarge",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errors.Is(err, kes.ErrKeyExists):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioKMSKeyExists",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
}
|
||||
|
||||
// Tier admin API errors
|
||||
case errors.Is(err, madmin.ErrTierNameEmpty):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioAdminTierNameEmpty",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errors.Is(err, madmin.ErrTierInvalidConfig):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioAdminTierInvalidConfig",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errors.Is(err, madmin.ErrTierInvalidConfigVersion):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioAdminTierInvalidConfigVersion",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errors.Is(err, madmin.ErrTierTypeUnsupported):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioAdminTierTypeUnsupported",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errIsTierPermError(err):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioAdminTierInsufficientPermissions",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
case errors.Is(err, errTierInvalidConfig):
|
||||
apiErr = APIError{
|
||||
Code: "XMinioAdminTierInvalidConfig",
|
||||
Description: err.Error(),
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
}
|
||||
default:
|
||||
apiErr = errorCodes.ToAPIErrWithErr(toAdminAPIErrCode(ctx, err), err)
|
||||
}
|
||||
}
|
||||
return apiErr
|
||||
}
|
||||
|
||||
// toAdminAPIErrCode - converts errErasureWriteQuorum error to admin API
|
||||
// specific error.
|
||||
func toAdminAPIErrCode(ctx context.Context, err error) APIErrorCode {
|
||||
if errors.Is(err, errErasureWriteQuorum) {
|
||||
return ErrAdminConfigNoQuorum
|
||||
}
|
||||
return toAPIErrorCode(ctx, err)
|
||||
}
|
||||
|
||||
// wraps export error for more context
|
||||
func exportError(ctx context.Context, err error, fname, entity string) APIError {
|
||||
if entity == "" {
|
||||
return toAPIError(ctx, fmt.Errorf("error exporting %s with: %w", fname, err))
|
||||
}
|
||||
return toAPIError(ctx, fmt.Errorf("error exporting %s from %s with: %w", entity, fname, err))
|
||||
}
|
||||
|
||||
// wraps import error for more context
|
||||
func importError(ctx context.Context, err error, fname, entity string) APIError {
|
||||
if entity == "" {
|
||||
return toAPIError(ctx, fmt.Errorf("error importing %s with: %w", fname, err))
|
||||
}
|
||||
return toAPIError(ctx, fmt.Errorf("error importing %s from %s with: %w", entity, fname, err))
|
||||
}
|
||||
|
||||
// wraps import error for more context
|
||||
func importErrorWithAPIErr(ctx context.Context, apiErr APIErrorCode, err error, fname, entity string) APIError {
|
||||
if entity == "" {
|
||||
return errorCodes.ToAPIErrWithErr(apiErr, fmt.Errorf("error importing %s with: %w", fname, err))
|
||||
}
|
||||
return errorCodes.ToAPIErrWithErr(apiErr, fmt.Errorf("error importing %s from %s with: %w", entity, fname, err))
|
||||
}
|
||||
@@ -1,542 +0,0 @@
|
||||
// Copyright (c) 2015-2023 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/minio/internal/config/etcd"
|
||||
xldap "github.com/minio/minio/internal/config/identity/ldap"
|
||||
"github.com/minio/minio/internal/config/identity/openid"
|
||||
idplugin "github.com/minio/minio/internal/config/identity/plugin"
|
||||
polplugin "github.com/minio/minio/internal/config/policy/plugin"
|
||||
"github.com/minio/minio/internal/config/storageclass"
|
||||
"github.com/minio/minio/internal/config/subnet"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
"github.com/pgsty/silo-pkg/v3/policy"
|
||||
)
|
||||
|
||||
// DelConfigKVHandler - DELETE /minio/admin/v3/del-config-kv
|
||||
func (a adminAPIHandlers) DelConfigKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
||||
// More than maxConfigSize bytes were available
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
password := cred.SecretKey
|
||||
kvBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
adminLogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
subSys, _, _, err := config.GetSubSys(string(kvBytes))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := readServerConfig(ctx, objectAPI, nil)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = cfg.DelFrom(bytes.NewReader(kvBytes)); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = validateConfig(ctx, cfg, subSys); err != nil {
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if subnet proxy being deleted and if so the value of proxy of subnet
|
||||
// target of logger webhook configuration also should be deleted
|
||||
loggerWebhookProxyDeleted := setLoggerWebhookSubnetProxy(subSys, cfg)
|
||||
|
||||
if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// freshly retrieve the config so that default values are loaded for reset config
|
||||
if cfg, err = getValidConfig(objectAPI); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
dynamic := config.SubSystemsDynamic.Contains(subSys)
|
||||
if dynamic {
|
||||
applyDynamic(ctx, objectAPI, cfg, subSys, r, w)
|
||||
if subSys == config.SubnetSubSys && loggerWebhookProxyDeleted {
|
||||
// Logger webhook proxy deleted, apply the dynamic changes
|
||||
applyDynamic(ctx, objectAPI, cfg, config.LoggerWebhookSubSys, r, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func applyDynamic(ctx context.Context, objectAPI ObjectLayer, cfg config.Config, subSys string,
|
||||
r *http.Request, w http.ResponseWriter,
|
||||
) {
|
||||
// Apply dynamic values.
|
||||
if err := applyDynamicConfigForSubSys(GlobalContext, objectAPI, cfg, subSys); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
globalNotificationSys.SignalConfigReload(subSys)
|
||||
// Tell the client that dynamic config was applied.
|
||||
w.Header().Set(madmin.ConfigAppliedHeader, madmin.ConfigAppliedTrue)
|
||||
}
|
||||
|
||||
type badConfigErr struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
// Error - return the error message
|
||||
func (bce badConfigErr) Error() string {
|
||||
return bce.Err.Error()
|
||||
}
|
||||
|
||||
// Unwrap the error to its underlying error.
|
||||
func (bce badConfigErr) Unwrap() error {
|
||||
return bce.Err
|
||||
}
|
||||
|
||||
type setConfigResult struct {
|
||||
Cfg config.Config
|
||||
SubSys string
|
||||
Dynamic bool
|
||||
LoggerWebhookCfgUpdated bool
|
||||
}
|
||||
|
||||
// SetConfigKVHandler - PUT /minio/admin/v3/set-config-kv
|
||||
func (a adminAPIHandlers) SetConfigKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
||||
// More than maxConfigSize bytes were available
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
password := cred.SecretKey
|
||||
kvBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
adminLogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
result, err := setConfigKV(ctx, objectAPI, kvBytes)
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case badConfigErr:
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
||||
default:
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if result.Dynamic {
|
||||
applyDynamic(ctx, objectAPI, result.Cfg, result.SubSys, r, w)
|
||||
// If logger webhook config updated (proxy due to callhome), explicitly dynamically
|
||||
// apply the config
|
||||
if result.LoggerWebhookCfgUpdated {
|
||||
applyDynamic(ctx, objectAPI, result.Cfg, config.LoggerWebhookSubSys, r, w)
|
||||
}
|
||||
}
|
||||
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
func setConfigKV(ctx context.Context, objectAPI ObjectLayer, kvBytes []byte) (result setConfigResult, err error) {
|
||||
result.Cfg, err = readServerConfig(ctx, objectAPI, nil)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
result.Dynamic, err = result.Cfg.ReadConfig(bytes.NewReader(kvBytes))
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
result.SubSys, _, _, err = config.GetSubSys(string(kvBytes))
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
tgts, err := config.ParseConfigTargetID(bytes.NewReader(kvBytes))
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
ctx = context.WithValue(ctx, config.ContextKeyForTargetFromConfig, tgts)
|
||||
if verr := validateConfig(ctx, result.Cfg, result.SubSys); verr != nil {
|
||||
err = badConfigErr{Err: verr}
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Check if subnet proxy being set and if so set the same value to proxy of subnet
|
||||
// target of logger webhook configuration
|
||||
result.LoggerWebhookCfgUpdated = setLoggerWebhookSubnetProxy(result.SubSys, result.Cfg)
|
||||
|
||||
// Update the actual server config on disk.
|
||||
if err = saveServerConfig(ctx, objectAPI, result.Cfg); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Write the config input KV to history.
|
||||
err = saveServerConfigHistory(ctx, objectAPI, kvBytes)
|
||||
return result, err
|
||||
}
|
||||
|
||||
// GetConfigKVHandler - GET /minio/admin/v3/get-config-kv?key={key}
|
||||
//
|
||||
// `key` can be one of three forms:
|
||||
// 1. `subsys:target` -> request for config of a single subsystem and target pair.
|
||||
// 2. `subsys:` -> request for config of a single subsystem and the default target.
|
||||
// 3. `subsys` -> request for config of all targets for the given subsystem.
|
||||
//
|
||||
// This is a reporting API and config secrets are redacted in the response.
|
||||
func (a adminAPIHandlers) GetConfigKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cfg := globalServerConfig.Clone()
|
||||
vars := mux.Vars(r)
|
||||
key := vars["key"]
|
||||
|
||||
var subSys, target string
|
||||
{
|
||||
ws := strings.SplitN(key, madmin.SubSystemSeparator, 2)
|
||||
subSys = ws[0]
|
||||
if len(ws) == 2 {
|
||||
if ws[1] == "" {
|
||||
target = madmin.Default
|
||||
} else {
|
||||
target = ws[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subSysConfigs, err := cfg.GetSubsysInfo(subSys, target, true)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var s strings.Builder
|
||||
for _, subSysConfig := range subSysConfigs {
|
||||
subSysConfig.WriteTo(&s, false)
|
||||
}
|
||||
|
||||
password := cred.SecretKey
|
||||
econfigData, err := madmin.EncryptData(password, []byte(s.String()))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, econfigData)
|
||||
}
|
||||
|
||||
func (a adminAPIHandlers) ClearConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
restoreID := vars["restoreId"]
|
||||
if restoreID == "" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
return
|
||||
}
|
||||
if restoreID == "all" {
|
||||
chEntries, err := listServerConfigHistory(ctx, objectAPI, false, -1)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
for _, chEntry := range chEntries {
|
||||
if err = delServerConfigHistory(ctx, objectAPI, chEntry.RestoreID); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if err := delServerConfigHistory(ctx, objectAPI, restoreID); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// RestoreConfigHistoryKVHandler - restores a config with KV settings for the given KV id.
|
||||
func (a adminAPIHandlers) RestoreConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
restoreID := vars["restoreId"]
|
||||
if restoreID == "" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
kvBytes, err := readServerConfigHistory(ctx, objectAPI, restoreID)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := readServerConfig(ctx, objectAPI, nil)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = cfg.ReadConfig(bytes.NewReader(kvBytes)); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = validateConfig(ctx, cfg, ""); err != nil {
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
delServerConfigHistory(ctx, objectAPI, restoreID)
|
||||
}
|
||||
|
||||
// ListConfigHistoryKVHandler - lists all the KV ids.
|
||||
func (a adminAPIHandlers) ListConfigHistoryKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
count, err := strconv.Atoi(vars["count"])
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
chEntries, err := listServerConfigHistory(ctx, objectAPI, true, count)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := json.Marshal(chEntries)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
password := cred.SecretKey
|
||||
econfigData, err := madmin.EncryptData(password, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, econfigData)
|
||||
}
|
||||
|
||||
// HelpConfigKVHandler - GET /minio/admin/v3/help-config-kv?subSys={subSys}&key={key}
|
||||
func (a adminAPIHandlers) HelpConfigKVHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
|
||||
subSys := vars["subSys"]
|
||||
key := vars["key"]
|
||||
|
||||
_, envOnly := r.Form["env"]
|
||||
|
||||
rd, err := GetHelp(subSys, key, envOnly)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(rd)
|
||||
}
|
||||
|
||||
// SetConfigHandler - PUT /minio/admin/v3/config
|
||||
func (a adminAPIHandlers) SetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
||||
// More than maxConfigSize bytes were available
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
password := cred.SecretKey
|
||||
kvBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
adminLogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
cfg := newServerConfig()
|
||||
if _, err = cfg.ReadConfig(bytes.NewReader(kvBytes)); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = validateConfig(ctx, cfg, ""); err != nil {
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Update the actual server config on disk.
|
||||
if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Write to the config input KV to history.
|
||||
if err = saveServerConfigHistory(ctx, objectAPI, kvBytes); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
// GetConfigHandler - GET /minio/admin/v3/config
|
||||
//
|
||||
// This endpoint is mainly for exporting and backing up the configuration.
|
||||
// Secrets are not redacted.
|
||||
func (a adminAPIHandlers) GetConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cfg := globalServerConfig.Clone()
|
||||
|
||||
var s strings.Builder
|
||||
hkvs := config.HelpSubSysMap[""]
|
||||
for _, hkv := range hkvs {
|
||||
// We ignore the error below, as we cannot get one.
|
||||
cfgSubsysItems, _ := cfg.GetSubsysInfo(hkv.Key, "", false)
|
||||
|
||||
for _, item := range cfgSubsysItems {
|
||||
off := item.Config.Get(config.Enable) == config.EnableOff
|
||||
switch hkv.Key {
|
||||
case config.EtcdSubSys:
|
||||
off = !etcd.Enabled(item.Config)
|
||||
case config.StorageClassSubSys:
|
||||
off = !storageclass.Enabled(item.Config)
|
||||
case config.PolicyPluginSubSys:
|
||||
off = !polplugin.Enabled(item.Config)
|
||||
case config.IdentityOpenIDSubSys:
|
||||
off = !openid.Enabled(item.Config)
|
||||
case config.IdentityLDAPSubSys:
|
||||
off = !xldap.Enabled(item.Config)
|
||||
case config.IdentityTLSSubSys:
|
||||
off = !globalIAMSys.STSTLSConfig.Enabled
|
||||
case config.IdentityPluginSubSys:
|
||||
off = !idplugin.Enabled(item.Config)
|
||||
}
|
||||
item.WriteTo(&s, off)
|
||||
}
|
||||
}
|
||||
|
||||
password := cred.SecretKey
|
||||
econfigData, err := madmin.EncryptData(password, []byte(s.String()))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, econfigData)
|
||||
}
|
||||
|
||||
// setLoggerWebhookSubnetProxy - Sets the logger webhook's subnet proxy value to
|
||||
// one being set for subnet proxy
|
||||
func setLoggerWebhookSubnetProxy(subSys string, cfg config.Config) bool {
|
||||
if subSys == config.SubnetSubSys || subSys == config.LoggerWebhookSubSys {
|
||||
subnetWebhookCfg := cfg[config.LoggerWebhookSubSys][subnet.LoggerWebhookName]
|
||||
loggerWebhookSubnetProxy := subnetWebhookCfg.Get(logger.Proxy)
|
||||
subnetProxy := cfg[config.SubnetSubSys][config.Default].Get(logger.Proxy)
|
||||
if loggerWebhookSubnetProxy != subnetProxy {
|
||||
subnetWebhookCfg.Set(logger.Proxy, subnetProxy)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,439 +0,0 @@
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/minio/minio/internal/config"
|
||||
cfgldap "github.com/minio/minio/internal/config/identity/ldap"
|
||||
"github.com/minio/minio/internal/config/identity/openid"
|
||||
"github.com/minio/mux"
|
||||
"github.com/pgsty/silo-pkg/v3/ldap"
|
||||
"github.com/pgsty/silo-pkg/v3/policy"
|
||||
)
|
||||
|
||||
func addOrUpdateIDPHandler(ctx context.Context, w http.ResponseWriter, r *http.Request, isUpdate bool) {
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
||||
// More than maxConfigSize bytes were available
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure body content type is opaque to ensure that request body has not
|
||||
// been interpreted as form data.
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if contentType != "application/octet-stream" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
password := cred.SecretKey
|
||||
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
adminLogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
idpCfgType := mux.Vars(r)["type"]
|
||||
if !madmin.ValidIDPConfigTypes.Contains(idpCfgType) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigInvalidIDPType), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var subSys string
|
||||
switch idpCfgType {
|
||||
case madmin.OpenidIDPCfg:
|
||||
subSys = madmin.IdentityOpenIDSubSys
|
||||
case madmin.LDAPIDPCfg:
|
||||
subSys = madmin.IdentityLDAPSubSys
|
||||
}
|
||||
|
||||
cfgName := mux.Vars(r)["name"]
|
||||
cfgTarget := madmin.Default
|
||||
if cfgName != "" {
|
||||
cfgTarget = cfgName
|
||||
if idpCfgType == madmin.LDAPIDPCfg && cfgName != madmin.Default {
|
||||
// LDAP does not support multiple configurations. So cfgName must be
|
||||
// empty or `madmin.Default`.
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigLDAPNonDefaultConfigName), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check that this is a valid Create vs Update API call.
|
||||
s := globalServerConfig.Clone()
|
||||
if apiErrCode := handleCreateUpdateValidation(s, subSys, cfgTarget, isUpdate); apiErrCode != ErrNone {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(apiErrCode), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
cfgData := ""
|
||||
{
|
||||
tgtSuffix := ""
|
||||
if cfgTarget != madmin.Default {
|
||||
tgtSuffix = config.SubSystemSeparator + cfgTarget
|
||||
}
|
||||
cfgData = subSys + tgtSuffix + config.KvSpaceSeparator + string(reqBytes)
|
||||
}
|
||||
|
||||
cfg, err := readServerConfig(ctx, objectAPI, nil)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
dynamic, err := cfg.ReadConfig(strings.NewReader(cfgData))
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// IDP config is not dynamic. Sanity check.
|
||||
if dynamic {
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInternalError), "", r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = validateConfig(ctx, cfg, subSys); err != nil {
|
||||
var validationErr ldap.Validation
|
||||
if errors.As(err, &validationErr) {
|
||||
// If we got an LDAP validation error, we need to send appropriate
|
||||
// error message back to client (likely mc).
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigLDAPValidation),
|
||||
validationErr.FormatError(), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Update the actual server config on disk.
|
||||
if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Write to the config input KV to history.
|
||||
if err = saveServerConfigHistory(ctx, objectAPI, []byte(cfgData)); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
}
|
||||
|
||||
func handleCreateUpdateValidation(s config.Config, subSys, cfgTarget string, isUpdate bool) APIErrorCode {
|
||||
if cfgTarget != madmin.Default {
|
||||
// This cannot give an error at this point.
|
||||
subSysTargets, _ := s.GetAvailableTargets(subSys)
|
||||
subSysTargetsSet := set.CreateStringSet(subSysTargets...)
|
||||
if isUpdate && !subSysTargetsSet.Contains(cfgTarget) {
|
||||
return ErrAdminConfigIDPCfgNameDoesNotExist
|
||||
}
|
||||
if !isUpdate && subSysTargetsSet.Contains(cfgTarget) {
|
||||
return ErrAdminConfigIDPCfgNameAlreadyExists
|
||||
}
|
||||
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// For the default configuration name, since it will always be an available
|
||||
// target, we need to check if a configuration value has been set previously
|
||||
// to figure out if this is a valid create or update API call.
|
||||
|
||||
// This cannot really error (FIXME: improve the type for GetConfigInfo)
|
||||
var cfgInfos []madmin.IDPCfgInfo
|
||||
switch subSys {
|
||||
case madmin.IdentityOpenIDSubSys:
|
||||
cfgInfos, _ = globalIAMSys.OpenIDConfig.GetConfigInfo(s, cfgTarget)
|
||||
case madmin.IdentityLDAPSubSys:
|
||||
cfgInfos, _ = globalIAMSys.LDAPConfig.GetConfigInfo(s, cfgTarget)
|
||||
}
|
||||
|
||||
if len(cfgInfos) > 0 && !isUpdate {
|
||||
return ErrAdminConfigIDPCfgNameAlreadyExists
|
||||
}
|
||||
if len(cfgInfos) == 0 && isUpdate {
|
||||
return ErrAdminConfigIDPCfgNameDoesNotExist
|
||||
}
|
||||
return ErrNone
|
||||
}
|
||||
|
||||
// AddIdentityProviderCfg: adds a new IDP config for openid/ldap.
|
||||
//
|
||||
// PUT <admin-prefix>/idp-cfg/openid/dex1 -> create named config `dex1`
|
||||
//
|
||||
// PUT <admin-prefix>/idp-cfg/openid/_ -> create (default) named config `_`
|
||||
func (a adminAPIHandlers) AddIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
addOrUpdateIDPHandler(ctx, w, r, false)
|
||||
}
|
||||
|
||||
// UpdateIdentityProviderCfg: updates an existing IDP config for openid/ldap.
|
||||
//
|
||||
// POST <admin-prefix>/idp-cfg/openid/dex1 -> update named config `dex1`
|
||||
//
|
||||
// POST <admin-prefix>/idp-cfg/openid/_ -> update (default) named config `_`
|
||||
func (a adminAPIHandlers) UpdateIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
addOrUpdateIDPHandler(ctx, w, r, true)
|
||||
}
|
||||
|
||||
// ListIdentityProviderCfg:
|
||||
//
|
||||
// GET <admin-prefix>/idp-cfg/openid -> lists openid provider configs.
|
||||
func (a adminAPIHandlers) ListIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
password := cred.SecretKey
|
||||
|
||||
idpCfgType := mux.Vars(r)["type"]
|
||||
if !madmin.ValidIDPConfigTypes.Contains(idpCfgType) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigInvalidIDPType), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var cfgList []madmin.IDPListItem
|
||||
var err error
|
||||
switch idpCfgType {
|
||||
case madmin.OpenidIDPCfg:
|
||||
cfg := globalServerConfig.Clone()
|
||||
cfgList, err = globalIAMSys.OpenIDConfig.GetConfigList(cfg)
|
||||
case madmin.LDAPIDPCfg:
|
||||
cfg := globalServerConfig.Clone()
|
||||
cfgList, err = globalIAMSys.LDAPConfig.GetConfigList(cfg)
|
||||
|
||||
default:
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := json.Marshal(cfgList)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
econfigData, err := madmin.EncryptData(password, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, econfigData)
|
||||
}
|
||||
|
||||
// GetIdentityProviderCfg:
|
||||
//
|
||||
// GET <admin-prefix>/idp-cfg/openid/dex_test
|
||||
func (a adminAPIHandlers) GetIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
idpCfgType := mux.Vars(r)["type"]
|
||||
cfgName := mux.Vars(r)["name"]
|
||||
password := cred.SecretKey
|
||||
|
||||
if !madmin.ValidIDPConfigTypes.Contains(idpCfgType) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigInvalidIDPType), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
cfg := globalServerConfig.Clone()
|
||||
var cfgInfos []madmin.IDPCfgInfo
|
||||
var err error
|
||||
switch idpCfgType {
|
||||
case madmin.OpenidIDPCfg:
|
||||
cfgInfos, err = globalIAMSys.OpenIDConfig.GetConfigInfo(cfg, cfgName)
|
||||
case madmin.LDAPIDPCfg:
|
||||
cfgInfos, err = globalIAMSys.LDAPConfig.GetConfigInfo(cfg, cfgName)
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, openid.ErrProviderConfigNotFound) || errors.Is(err, cfgldap.ErrProviderConfigNotFound) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
res := madmin.IDPConfig{
|
||||
Type: idpCfgType,
|
||||
Name: cfgName,
|
||||
Info: cfgInfos,
|
||||
}
|
||||
data, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
econfigData, err := madmin.EncryptData(password, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, econfigData)
|
||||
}
|
||||
|
||||
// DeleteIdentityProviderCfg:
|
||||
//
|
||||
// DELETE <admin-prefix>/idp-cfg/openid/dex_test
|
||||
func (a adminAPIHandlers) DeleteIdentityProviderCfg(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.ConfigUpdateAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
idpCfgType := mux.Vars(r)["type"]
|
||||
cfgName := mux.Vars(r)["name"]
|
||||
if !madmin.ValidIDPConfigTypes.Contains(idpCfgType) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigInvalidIDPType), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
cfgCopy := globalServerConfig.Clone()
|
||||
var subSys string
|
||||
switch idpCfgType {
|
||||
case madmin.OpenidIDPCfg:
|
||||
subSys = config.IdentityOpenIDSubSys
|
||||
cfgInfos, err := globalIAMSys.OpenIDConfig.GetConfigInfo(cfgCopy, cfgName)
|
||||
if err != nil {
|
||||
if errors.Is(err, openid.ErrProviderConfigNotFound) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
hasEnv := false
|
||||
for _, ci := range cfgInfos {
|
||||
if ci.IsCfg && ci.IsEnv {
|
||||
hasEnv = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasEnv {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigEnvOverridden), r.URL)
|
||||
return
|
||||
}
|
||||
case madmin.LDAPIDPCfg:
|
||||
subSys = config.IdentityLDAPSubSys
|
||||
cfgInfos, err := globalIAMSys.LDAPConfig.GetConfigInfo(cfgCopy, cfgName)
|
||||
if err != nil {
|
||||
if errors.Is(err, openid.ErrProviderConfigNotFound) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
hasEnv := false
|
||||
for _, ci := range cfgInfos {
|
||||
if ci.IsCfg && ci.IsEnv {
|
||||
hasEnv = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasEnv {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigEnvOverridden), r.URL)
|
||||
return
|
||||
}
|
||||
default:
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := readServerConfig(ctx, objectAPI, nil)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
cfgKey := fmt.Sprintf("%s:%s", subSys, cfgName)
|
||||
if cfgName == madmin.Default {
|
||||
cfgKey = subSys
|
||||
}
|
||||
if err = cfg.DelKVS(cfgKey); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if err = validateConfig(ctx, cfg, subSys); err != nil {
|
||||
var validationErr ldap.Validation
|
||||
if errors.As(err, &validationErr) {
|
||||
// If we got an LDAP validation error, we need to send appropriate
|
||||
// error message back to client (likely mc).
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigLDAPValidation),
|
||||
validationErr.FormatError(), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeCustomErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), err.Error(), r.URL)
|
||||
return
|
||||
}
|
||||
if err = saveServerConfig(ctx, objectAPI, cfg); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
dynamic := config.SubSystemsDynamic.Contains(subSys)
|
||||
if dynamic {
|
||||
applyDynamic(ctx, objectAPI, cfg, subSys, r, w)
|
||||
}
|
||||
}
|
||||
@@ -1,657 +0,0 @@
|
||||
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/mux"
|
||||
xldap "github.com/pgsty/silo-pkg/v3/ldap"
|
||||
"github.com/pgsty/silo-pkg/v3/policy"
|
||||
)
|
||||
|
||||
// ListLDAPPolicyMappingEntities lists users/groups mapped to given/all policies.
|
||||
//
|
||||
// GET <admin-prefix>/idp/ldap/policy-entities?[query-params]
|
||||
//
|
||||
// Query params:
|
||||
//
|
||||
// user=... -> repeatable query parameter, specifying users to query for
|
||||
// policy mapping
|
||||
//
|
||||
// group=... -> repeatable query parameter, specifying groups to query for
|
||||
// policy mapping
|
||||
//
|
||||
// policy=... -> repeatable query parameter, specifying policy to query for
|
||||
// user/group mapping
|
||||
//
|
||||
// When all query parameters are omitted, returns mappings for all policies.
|
||||
func (a adminAPIHandlers) ListLDAPPolicyMappingEntities(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Check authorization.
|
||||
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r,
|
||||
policy.ListGroupsAdminAction, policy.ListUsersAdminAction, policy.ListUserPoliciesAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate API arguments.
|
||||
|
||||
q := madmin.PolicyEntitiesQuery{
|
||||
Users: r.Form["user"],
|
||||
Groups: r.Form["group"],
|
||||
Policy: r.Form["policy"],
|
||||
}
|
||||
|
||||
// Query IAM
|
||||
|
||||
res, err := globalIAMSys.QueryLDAPPolicyEntities(r.Context(), q)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Encode result and send response.
|
||||
|
||||
data, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
password := cred.SecretKey
|
||||
econfigData, err := madmin.EncryptData(password, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
writeSuccessResponseJSON(w, econfigData)
|
||||
}
|
||||
|
||||
// AttachDetachPolicyLDAP attaches or detaches policies from an LDAP entity
|
||||
// (user or group).
|
||||
//
|
||||
// POST <admin-prefix>/idp/ldap/policy/{operation}
|
||||
func (a adminAPIHandlers) AttachDetachPolicyLDAP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Check authorization.
|
||||
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, policy.UpdatePolicyAssociationAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// fail if ldap is not enabled
|
||||
if !globalIAMSys.LDAPConfig.Enabled() {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminLDAPNotEnabled), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
|
||||
// More than maxConfigSize bytes were available
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure body content type is opaque to ensure that request body has not
|
||||
// been interpreted as form data.
|
||||
contentType := r.Header.Get("Content-Type")
|
||||
if contentType != "application/octet-stream" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrBadRequest), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate operation
|
||||
operation := mux.Vars(r)["operation"]
|
||||
if operation != "attach" && operation != "detach" {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
isAttach := operation == "attach"
|
||||
|
||||
// Validate API arguments in body.
|
||||
password := cred.SecretKey
|
||||
reqBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
|
||||
if err != nil {
|
||||
adminLogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var par madmin.PolicyAssociationReq
|
||||
err = json.Unmarshal(reqBytes, &par)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err := par.IsValid(); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Call IAM subsystem
|
||||
updatedAt, addedOrRemoved, _, err := globalIAMSys.PolicyDBUpdateLDAP(ctx, isAttach, par)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
respBody := madmin.PolicyAssociationResp{
|
||||
UpdatedAt: updatedAt,
|
||||
}
|
||||
if isAttach {
|
||||
respBody.PoliciesAttached = addedOrRemoved
|
||||
} else {
|
||||
respBody.PoliciesDetached = addedOrRemoved
|
||||
}
|
||||
|
||||
data, err := json.Marshal(respBody)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
encryptedData, err := madmin.EncryptData(password, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, encryptedData)
|
||||
}
|
||||
|
||||
// AddServiceAccountLDAP adds a new service account for provided LDAP username or DN
|
||||
//
|
||||
// PUT /minio/admin/v3/idp/ldap/add-service-account
|
||||
func (a adminAPIHandlers) AddServiceAccountLDAP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cred, opts, createReq, targetUser, APIError := commonAddServiceAccount(r, true)
|
||||
if APIError.Code != "" {
|
||||
writeErrorResponseJSON(ctx, w, APIError, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// fail if ldap is not enabled
|
||||
if !globalIAMSys.LDAPConfig.Enabled() {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminLDAPNotEnabled), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Find the user for the request sender (as it may be sent via a service
|
||||
// account or STS account):
|
||||
requestorUser := cred.AccessKey
|
||||
requestorParentUser := cred.AccessKey
|
||||
requestorGroups := cred.Groups
|
||||
requestorIsDerivedCredential := false
|
||||
if cred.IsServiceAccount() || cred.IsTemp() {
|
||||
requestorParentUser = cred.ParentUser
|
||||
requestorIsDerivedCredential = true
|
||||
}
|
||||
|
||||
// Check if we are creating svc account for request sender.
|
||||
isSvcAccForRequestor := targetUser == requestorUser || targetUser == requestorParentUser
|
||||
|
||||
var (
|
||||
targetGroups []string
|
||||
err error
|
||||
)
|
||||
|
||||
// If we are creating svc account for request sender, ensure that targetUser
|
||||
// is a real user (i.e. not derived credentials).
|
||||
if isSvcAccForRequestor {
|
||||
if requestorIsDerivedCredential {
|
||||
if requestorParentUser == "" {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx,
|
||||
errors.New("service accounts cannot be generated for temporary credentials without parent")), r.URL)
|
||||
return
|
||||
}
|
||||
targetUser = requestorParentUser
|
||||
}
|
||||
targetGroups = requestorGroups
|
||||
|
||||
// Deny if the target user is not LDAP
|
||||
foundResult, err := globalIAMSys.LDAPConfig.GetValidatedDNForUsername(targetUser)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if foundResult == nil {
|
||||
err := errors.New("Specified user does not exist on LDAP server")
|
||||
APIErr := errorCodes.ToAPIErrWithErr(ErrAdminNoSuchUser, err)
|
||||
writeErrorResponseJSON(ctx, w, APIErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// In case of LDAP/OIDC we need to set `opts.claims` to ensure
|
||||
// it is associated with the LDAP/OIDC user properly.
|
||||
for k, v := range cred.Claims {
|
||||
if k == expClaim {
|
||||
continue
|
||||
}
|
||||
opts.claims[k] = v
|
||||
}
|
||||
} else {
|
||||
// We still need to ensure that the target user is a valid LDAP user.
|
||||
//
|
||||
// The target user may be supplied as a (short) username or a DN.
|
||||
// However, for now, we only support using the short username.
|
||||
|
||||
isDN := globalIAMSys.LDAPConfig.ParsesAsDN(targetUser)
|
||||
opts.claims[ldapUserN] = targetUser // simple username
|
||||
var lookupResult *xldap.DNSearchResult
|
||||
lookupResult, targetGroups, err = globalIAMSys.LDAPConfig.LookupUserDN(targetUser)
|
||||
if err != nil {
|
||||
// if not found, check if DN
|
||||
if strings.Contains(strings.ToLower(err.Error()), "user dn not found for:") {
|
||||
if isDN {
|
||||
// warn user that DNs are not allowed
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminLDAPExpectedLoginName, err), r.URL)
|
||||
} else {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminNoSuchUser, err), r.URL)
|
||||
}
|
||||
}
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
targetUser = lookupResult.NormDN
|
||||
opts.claims[ldapUser] = targetUser // DN
|
||||
opts.claims[ldapActualUser] = lookupResult.ActualDN
|
||||
|
||||
// Check if this user or their groups have a policy applied.
|
||||
ldapPolicies, err := globalIAMSys.PolicyDBGet(targetUser, targetGroups...)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if len(ldapPolicies) == 0 {
|
||||
err = fmt.Errorf("No policy set for user `%s` or any of their groups: `%s`", opts.claims[ldapActualUser], strings.Join(targetGroups, "`,`"))
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrAdminNoSuchUser, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Add LDAP attributes that were looked up into the claims.
|
||||
for attribKey, attribValue := range lookupResult.Attributes {
|
||||
opts.claims[ldapAttribPrefix+attribKey] = attribValue
|
||||
}
|
||||
}
|
||||
|
||||
newCred, updatedAt, err := globalIAMSys.NewServiceAccount(ctx, targetUser, targetGroups, opts)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
createResp := madmin.AddServiceAccountResp{
|
||||
Credentials: madmin.Credentials{
|
||||
AccessKey: newCred.AccessKey,
|
||||
SecretKey: newCred.SecretKey,
|
||||
Expiration: newCred.Expiration,
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(createResp)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, encryptedData)
|
||||
|
||||
// Call hook for cluster-replication if the service account is not for a
|
||||
// root user.
|
||||
if newCred.ParentUser != globalActiveCred.AccessKey {
|
||||
replLogIf(ctx, globalSiteReplicationSys.IAMChangeHook(ctx, madmin.SRIAMItem{
|
||||
Type: madmin.SRIAMItemSvcAcc,
|
||||
SvcAccChange: &madmin.SRSvcAccChange{
|
||||
Create: &madmin.SRSvcAccCreate{
|
||||
Parent: newCred.ParentUser,
|
||||
AccessKey: newCred.AccessKey,
|
||||
SecretKey: newCred.SecretKey,
|
||||
Groups: newCred.Groups,
|
||||
Name: newCred.Name,
|
||||
Description: newCred.Description,
|
||||
Claims: opts.claims,
|
||||
SessionPolicy: madmin.SRSessionPolicy(createReq.Policy),
|
||||
Status: auth.AccountOn,
|
||||
Expiration: createReq.Expiration,
|
||||
},
|
||||
},
|
||||
UpdatedAt: updatedAt,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// ListAccessKeysLDAP - GET /minio/admin/v3/idp/ldap/list-access-keys
|
||||
func (a adminAPIHandlers) ListAccessKeysLDAP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||
if s3Err != ErrNone {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
userDN := r.Form.Get("userDN")
|
||||
|
||||
// If listing is requested for a specific user (who is not the request
|
||||
// sender), check that the user has permissions.
|
||||
if userDN != "" && userDN != cred.ParentUser {
|
||||
if !globalIAMSys.IsAllowed(policy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
Groups: cred.Groups,
|
||||
Action: policy.ListServiceAccountsAdminAction,
|
||||
ConditionValues: getConditionValues(r, "", cred),
|
||||
IsOwner: owner,
|
||||
Claims: cred.Claims,
|
||||
}) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !globalIAMSys.IsAllowed(policy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
Groups: cred.Groups,
|
||||
Action: policy.ListServiceAccountsAdminAction,
|
||||
ConditionValues: getConditionValues(r, "", cred),
|
||||
IsOwner: owner,
|
||||
Claims: cred.Claims,
|
||||
DenyOnly: true,
|
||||
}) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||
return
|
||||
}
|
||||
userDN = cred.AccessKey
|
||||
if cred.ParentUser != "" {
|
||||
userDN = cred.ParentUser
|
||||
}
|
||||
}
|
||||
|
||||
dnResult, err := globalIAMSys.LDAPConfig.GetValidatedDNForUsername(userDN)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if dnResult == nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errNoSuchUser), r.URL)
|
||||
return
|
||||
}
|
||||
targetAccount := dnResult.NormDN
|
||||
|
||||
listType := r.Form.Get("listType")
|
||||
if listType != "sts-only" && listType != "svcacc-only" && listType != "" {
|
||||
// default to both
|
||||
listType = ""
|
||||
}
|
||||
|
||||
var serviceAccounts []auth.Credentials
|
||||
var stsKeys []auth.Credentials
|
||||
|
||||
if listType == "" || listType == "sts-only" {
|
||||
stsKeys, err = globalIAMSys.ListSTSAccounts(ctx, targetAccount)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
if listType == "" || listType == "svcacc-only" {
|
||||
serviceAccounts, err = globalIAMSys.ListServiceAccounts(ctx, targetAccount)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var serviceAccountList []madmin.ServiceAccountInfo
|
||||
var stsKeyList []madmin.ServiceAccountInfo
|
||||
|
||||
for _, svc := range serviceAccounts {
|
||||
expiryTime := svc.Expiration
|
||||
serviceAccountList = append(serviceAccountList, madmin.ServiceAccountInfo{
|
||||
AccessKey: svc.AccessKey,
|
||||
Expiration: &expiryTime,
|
||||
Name: svc.Name,
|
||||
Description: svc.Description,
|
||||
})
|
||||
}
|
||||
for _, sts := range stsKeys {
|
||||
expiryTime := sts.Expiration
|
||||
stsKeyList = append(stsKeyList, madmin.ServiceAccountInfo{
|
||||
AccessKey: sts.AccessKey,
|
||||
Expiration: &expiryTime,
|
||||
})
|
||||
}
|
||||
|
||||
listResp := madmin.ListAccessKeysLDAPResp{
|
||||
ServiceAccounts: serviceAccountList,
|
||||
STSKeys: stsKeyList,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(listResp)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, encryptedData)
|
||||
}
|
||||
|
||||
// ListAccessKeysLDAPBulk - GET /minio/admin/v3/idp/ldap/list-access-keys-bulk
|
||||
func (a adminAPIHandlers) ListAccessKeysLDAPBulk(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||
if s3Err != ErrNone {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
dnList := r.Form["userDNs"]
|
||||
isAll := r.Form.Get("all") == "true"
|
||||
selfOnly := !isAll && len(dnList) == 0
|
||||
|
||||
if isAll && len(dnList) > 0 {
|
||||
// This should be checked on client side, so return generic error
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Empty DN list and not self, list access keys for all users
|
||||
if isAll {
|
||||
if !globalIAMSys.IsAllowed(policy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
Groups: cred.Groups,
|
||||
Action: policy.ListUsersAdminAction,
|
||||
ConditionValues: getConditionValues(r, "", cred),
|
||||
IsOwner: owner,
|
||||
Claims: cred.Claims,
|
||||
}) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||
return
|
||||
}
|
||||
} else if len(dnList) == 1 {
|
||||
var dn string
|
||||
foundResult, err := globalIAMSys.LDAPConfig.GetValidatedDNForUsername(dnList[0])
|
||||
if err == nil {
|
||||
dn = foundResult.NormDN
|
||||
}
|
||||
if dn == cred.ParentUser || dnList[0] == cred.ParentUser {
|
||||
selfOnly = true
|
||||
}
|
||||
}
|
||||
|
||||
if !globalIAMSys.IsAllowed(policy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
Groups: cred.Groups,
|
||||
Action: policy.ListServiceAccountsAdminAction,
|
||||
ConditionValues: getConditionValues(r, "", cred),
|
||||
IsOwner: owner,
|
||||
Claims: cred.Claims,
|
||||
DenyOnly: selfOnly,
|
||||
}) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if selfOnly && len(dnList) == 0 {
|
||||
selfDN := cred.AccessKey
|
||||
if cred.ParentUser != "" {
|
||||
selfDN = cred.ParentUser
|
||||
}
|
||||
dnList = append(dnList, selfDN)
|
||||
}
|
||||
|
||||
var ldapUserList []string
|
||||
if isAll {
|
||||
ldapUsers, err := globalIAMSys.ListLDAPUsers(ctx)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
for user := range ldapUsers {
|
||||
ldapUserList = append(ldapUserList, user)
|
||||
}
|
||||
} else {
|
||||
for _, userDN := range dnList {
|
||||
// Validate the userDN
|
||||
foundResult, err := globalIAMSys.LDAPConfig.GetValidatedDNForUsername(userDN)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if foundResult == nil {
|
||||
continue
|
||||
}
|
||||
ldapUserList = append(ldapUserList, foundResult.NormDN)
|
||||
}
|
||||
}
|
||||
|
||||
listType := r.Form.Get("listType")
|
||||
var listSTSKeys, listServiceAccounts bool
|
||||
switch listType {
|
||||
case madmin.AccessKeyListUsersOnly:
|
||||
listSTSKeys = false
|
||||
listServiceAccounts = false
|
||||
case madmin.AccessKeyListSTSOnly:
|
||||
listSTSKeys = true
|
||||
listServiceAccounts = false
|
||||
case madmin.AccessKeyListSvcaccOnly:
|
||||
listSTSKeys = false
|
||||
listServiceAccounts = true
|
||||
case madmin.AccessKeyListAll:
|
||||
listSTSKeys = true
|
||||
listServiceAccounts = true
|
||||
default:
|
||||
err := errors.New("invalid list type")
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrInvalidRequest, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
accessKeyMap := make(map[string]madmin.ListAccessKeysLDAPResp)
|
||||
for _, internalDN := range ldapUserList {
|
||||
externalDN := globalIAMSys.LDAPConfig.DecodeDN(internalDN)
|
||||
accessKeys := madmin.ListAccessKeysLDAPResp{}
|
||||
if listSTSKeys {
|
||||
stsKeys, err := globalIAMSys.ListSTSAccounts(ctx, internalDN)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
for _, sts := range stsKeys {
|
||||
accessKeys.STSKeys = append(accessKeys.STSKeys, madmin.ServiceAccountInfo{
|
||||
AccessKey: sts.AccessKey,
|
||||
Expiration: &sts.Expiration,
|
||||
})
|
||||
}
|
||||
// if only STS keys, skip if user has no STS keys
|
||||
if !listServiceAccounts && len(stsKeys) == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if listServiceAccounts {
|
||||
serviceAccounts, err := globalIAMSys.ListServiceAccounts(ctx, internalDN)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
for _, svc := range serviceAccounts {
|
||||
accessKeys.ServiceAccounts = append(accessKeys.ServiceAccounts, madmin.ServiceAccountInfo{
|
||||
AccessKey: svc.AccessKey,
|
||||
Expiration: &svc.Expiration,
|
||||
Name: svc.Name,
|
||||
Description: svc.Description,
|
||||
})
|
||||
}
|
||||
// if only service accounts, skip if user has no service accounts
|
||||
if !listSTSKeys && len(serviceAccounts) == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
accessKeyMap[externalDN] = accessKeys
|
||||
}
|
||||
|
||||
data, err := json.Marshal(accessKeyMap)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, encryptedData)
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
// Copyright (c) 2015-2025 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"sort"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio-go/v7/pkg/set"
|
||||
"github.com/pgsty/silo-pkg/v3/policy"
|
||||
)
|
||||
|
||||
const dummyRoleARN = "dummy-internal"
|
||||
|
||||
// ListAccessKeysOpenIDBulk - GET /minio/admin/v3/idp/openid/list-access-keys-bulk
|
||||
func (a adminAPIHandlers) ListAccessKeysOpenIDBulk(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Get current object layer instance.
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
cred, owner, s3Err := validateAdminSignature(ctx, r, "")
|
||||
if s3Err != ErrNone {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if !globalIAMSys.OpenIDConfig.Enabled {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminOpenIDNotEnabled), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
userList := r.Form["users"]
|
||||
isAll := r.Form.Get("all") == "true"
|
||||
selfOnly := !isAll && len(userList) == 0
|
||||
cfgName := r.Form.Get("configName")
|
||||
allConfigs := r.Form.Get("allConfigs") == "true"
|
||||
if cfgName == "" && !allConfigs {
|
||||
cfgName = madmin.Default
|
||||
}
|
||||
|
||||
if isAll && len(userList) > 0 {
|
||||
// This should be checked on client side, so return generic error
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Empty DN list and not self, list access keys for all users
|
||||
if isAll {
|
||||
if !globalIAMSys.IsAllowed(policy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
Groups: cred.Groups,
|
||||
Action: policy.ListUsersAdminAction,
|
||||
ConditionValues: getConditionValues(r, "", cred),
|
||||
IsOwner: owner,
|
||||
Claims: cred.Claims,
|
||||
}) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||
return
|
||||
}
|
||||
} else if len(userList) == 1 && userList[0] == cred.ParentUser {
|
||||
selfOnly = true
|
||||
}
|
||||
|
||||
if !globalIAMSys.IsAllowed(policy.Args{
|
||||
AccountName: cred.AccessKey,
|
||||
Groups: cred.Groups,
|
||||
Action: policy.ListServiceAccountsAdminAction,
|
||||
ConditionValues: getConditionValues(r, "", cred),
|
||||
IsOwner: owner,
|
||||
Claims: cred.Claims,
|
||||
DenyOnly: selfOnly,
|
||||
}) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if selfOnly && len(userList) == 0 {
|
||||
selfDN := cred.AccessKey
|
||||
if cred.ParentUser != "" {
|
||||
selfDN = cred.ParentUser
|
||||
}
|
||||
userList = append(userList, selfDN)
|
||||
}
|
||||
|
||||
listType := r.Form.Get("listType")
|
||||
var listSTSKeys, listServiceAccounts bool
|
||||
switch listType {
|
||||
case madmin.AccessKeyListUsersOnly:
|
||||
listSTSKeys = false
|
||||
listServiceAccounts = false
|
||||
case madmin.AccessKeyListSTSOnly:
|
||||
listSTSKeys = true
|
||||
listServiceAccounts = false
|
||||
case madmin.AccessKeyListSvcaccOnly:
|
||||
listSTSKeys = false
|
||||
listServiceAccounts = true
|
||||
case madmin.AccessKeyListAll:
|
||||
listSTSKeys = true
|
||||
listServiceAccounts = true
|
||||
default:
|
||||
err := errors.New("invalid list type")
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErrWithErr(ErrInvalidRequest, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
s := globalServerConfig.Clone()
|
||||
roleArnMap := make(map[string]string)
|
||||
// Map of configs to a map of users to their access keys
|
||||
cfgToUsersMap := make(map[string]map[string]madmin.OpenIDUserAccessKeys)
|
||||
configs, err := globalIAMSys.OpenIDConfig.GetConfigList(s)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
for _, config := range configs {
|
||||
if !allConfigs && cfgName != config.Name {
|
||||
continue
|
||||
}
|
||||
arn := dummyRoleARN
|
||||
if config.RoleARN != "" {
|
||||
arn = config.RoleARN
|
||||
}
|
||||
roleArnMap[arn] = config.Name
|
||||
newResp := make(map[string]madmin.OpenIDUserAccessKeys)
|
||||
cfgToUsersMap[config.Name] = newResp
|
||||
}
|
||||
if len(roleArnMap) == 0 {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminNoSuchConfigTarget), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
userSet := set.CreateStringSet(userList...)
|
||||
accessKeys, err := globalIAMSys.ListAllAccessKeys(ctx)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
for _, accessKey := range accessKeys {
|
||||
// Filter out any disqualifying access keys
|
||||
_, ok := accessKey.Claims[subClaim]
|
||||
if !ok {
|
||||
continue // OpenID access keys must have a sub claim
|
||||
}
|
||||
if (!listSTSKeys && !accessKey.IsServiceAccount()) || (!listServiceAccounts && accessKey.IsServiceAccount()) {
|
||||
continue // skip if not the type we want
|
||||
}
|
||||
arn, ok := accessKey.Claims[roleArnClaim].(string)
|
||||
if !ok {
|
||||
if _, ok := accessKey.Claims[iamPolicyClaimNameOpenID()]; !ok {
|
||||
continue // skip if no roleArn and no policy claim
|
||||
}
|
||||
// claim-based provider is in the roleArnMap under dummy ARN
|
||||
arn = dummyRoleARN
|
||||
}
|
||||
matchingCfgName, ok := roleArnMap[arn]
|
||||
if !ok {
|
||||
continue // skip if not part of the target config
|
||||
}
|
||||
var id string
|
||||
if idClaim := globalIAMSys.OpenIDConfig.GetUserIDClaim(matchingCfgName); idClaim != "" {
|
||||
id, _ = accessKey.Claims[idClaim].(string)
|
||||
}
|
||||
if !userSet.IsEmpty() && !userSet.Contains(accessKey.ParentUser) && !userSet.Contains(id) {
|
||||
continue // skip if not in the user list
|
||||
}
|
||||
openIDUserAccessKeys, ok := cfgToUsersMap[matchingCfgName][accessKey.ParentUser]
|
||||
|
||||
// Add new user to map if not already present
|
||||
if !ok {
|
||||
var readableClaim string
|
||||
if rc := globalIAMSys.OpenIDConfig.GetUserReadableClaim(matchingCfgName); rc != "" {
|
||||
readableClaim, _ = accessKey.Claims[rc].(string)
|
||||
}
|
||||
openIDUserAccessKeys = madmin.OpenIDUserAccessKeys{
|
||||
MinioAccessKey: accessKey.ParentUser,
|
||||
ID: id,
|
||||
ReadableName: readableClaim,
|
||||
}
|
||||
}
|
||||
svcAccInfo := madmin.ServiceAccountInfo{
|
||||
AccessKey: accessKey.AccessKey,
|
||||
Expiration: &accessKey.Expiration,
|
||||
}
|
||||
if accessKey.IsServiceAccount() {
|
||||
openIDUserAccessKeys.ServiceAccounts = append(openIDUserAccessKeys.ServiceAccounts, svcAccInfo)
|
||||
} else {
|
||||
openIDUserAccessKeys.STSKeys = append(openIDUserAccessKeys.STSKeys, svcAccInfo)
|
||||
}
|
||||
cfgToUsersMap[matchingCfgName][accessKey.ParentUser] = openIDUserAccessKeys
|
||||
}
|
||||
|
||||
// Convert map to slice and sort
|
||||
resp := make([]madmin.ListAccessKeysOpenIDResp, 0, len(cfgToUsersMap))
|
||||
for cfgName, usersMap := range cfgToUsersMap {
|
||||
users := make([]madmin.OpenIDUserAccessKeys, 0, len(usersMap))
|
||||
for _, user := range usersMap {
|
||||
users = append(users, user)
|
||||
}
|
||||
sort.Slice(users, func(i, j int) bool {
|
||||
return users[i].MinioAccessKey < users[j].MinioAccessKey
|
||||
})
|
||||
resp = append(resp, madmin.ListAccessKeysOpenIDResp{
|
||||
ConfigName: cfgName,
|
||||
Users: users,
|
||||
})
|
||||
}
|
||||
sort.Slice(resp, func(i, j int) bool {
|
||||
return resp[i].ConfigName < resp[j].ConfigName
|
||||
})
|
||||
|
||||
data, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
encryptedData, err := madmin.EncryptData(cred.SecretKey, data)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, encryptedData)
|
||||
}
|
||||
@@ -1,393 +0,0 @@
|
||||
// Copyright (c) 2015-2024 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/minio/mux"
|
||||
"github.com/pgsty/silo-pkg/v3/env"
|
||||
"github.com/pgsty/silo-pkg/v3/policy"
|
||||
)
|
||||
|
||||
var (
|
||||
errRebalanceDecommissionAlreadyRunning = errors.New("Rebalance cannot be started, decommission is already in progress")
|
||||
errDecommissionRebalanceAlreadyRunning = errors.New("Decommission cannot be started, rebalance is already in progress")
|
||||
)
|
||||
|
||||
func (a adminAPIHandlers) StartDecommission(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.DecommissionAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Legacy args style such as non-ellipses style is not supported with this API.
|
||||
if globalEndpoints.Legacy() {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
z, ok := objectAPI.(*erasureServerPools)
|
||||
if !ok || len(z.serverPools) == 1 {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if z.IsDecommissionRunning() {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errDecommissionAlreadyRunning), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if z.IsRebalanceStarted(ctx) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminRebalanceAlreadyStarted), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
v := vars["pool"]
|
||||
byID := vars["by-id"] == "true"
|
||||
|
||||
pools := strings.Split(v, ",")
|
||||
poolIndices := make([]int, 0, len(pools))
|
||||
|
||||
for _, pool := range pools {
|
||||
var idx int
|
||||
if byID {
|
||||
var err error
|
||||
idx, err = strconv.Atoi(pool)
|
||||
if err != nil {
|
||||
// We didn't find any matching pools, invalid input
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
idx = globalEndpoints.GetPoolIdx(pool)
|
||||
if idx == -1 {
|
||||
// We didn't find any matching pools, invalid input
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
var pool *erasureSets
|
||||
for pidx := range z.serverPools {
|
||||
if pidx == idx {
|
||||
pool = z.serverPools[idx]
|
||||
break
|
||||
}
|
||||
}
|
||||
if pool == nil {
|
||||
// We didn't find any matching pools, invalid input
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
poolIndices = append(poolIndices, idx)
|
||||
}
|
||||
|
||||
if len(poolIndices) == 0 || !proxyDecommissionRequest(ctx, globalEndpoints[poolIndices[0]].Endpoints[0], w, r) {
|
||||
if err := z.Decommission(r.Context(), poolIndices...); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a adminAPIHandlers) CancelDecommission(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.DecommissionAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Legacy args style such as non-ellipses style is not supported with this API.
|
||||
if globalEndpoints.Legacy() {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
pools, ok := objectAPI.(*erasureServerPools)
|
||||
if !ok {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
v := vars["pool"]
|
||||
byID := vars["by-id"] == "true"
|
||||
idx := -1
|
||||
|
||||
if byID {
|
||||
if i, err := strconv.Atoi(v); err == nil && i >= 0 && i < len(globalEndpoints) {
|
||||
idx = i
|
||||
}
|
||||
} else {
|
||||
idx = globalEndpoints.GetPoolIdx(v)
|
||||
}
|
||||
|
||||
if idx == -1 {
|
||||
// We didn't find any matching pools, invalid input
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if !proxyDecommissionRequest(ctx, globalEndpoints[idx].Endpoints[0], w, r) {
|
||||
if err := pools.DecommissionCancel(ctx, idx); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a adminAPIHandlers) StatusPool(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.ServerInfoAdminAction, policy.DecommissionAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Legacy args style such as non-ellipses style is not supported with this API.
|
||||
if globalEndpoints.Legacy() {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
pools, ok := objectAPI.(*erasureServerPools)
|
||||
if !ok {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
v := vars["pool"]
|
||||
byID := vars["by-id"] == "true"
|
||||
idx := -1
|
||||
|
||||
if byID {
|
||||
if i, err := strconv.Atoi(v); err == nil && i >= 0 && i < len(globalEndpoints) {
|
||||
idx = i
|
||||
}
|
||||
} else {
|
||||
idx = globalEndpoints.GetPoolIdx(v)
|
||||
}
|
||||
|
||||
if idx == -1 {
|
||||
apiErr := toAdminAPIErr(ctx, errInvalidArgument)
|
||||
apiErr.Description = fmt.Sprintf("specified pool '%s' not found, please specify a valid pool", v)
|
||||
// We didn't find any matching pools, invalid input
|
||||
writeErrorResponseJSON(ctx, w, apiErr, r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
status, err := pools.Status(r.Context(), idx)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
adminLogIf(r.Context(), json.NewEncoder(w).Encode(&status))
|
||||
}
|
||||
|
||||
func (a adminAPIHandlers) ListPools(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.ServerInfoAdminAction, policy.DecommissionAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Legacy args style such as non-ellipses style is not supported with this API.
|
||||
if globalEndpoints.Legacy() {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
pools, ok := objectAPI.(*erasureServerPools)
|
||||
if !ok {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
poolsStatus := make([]PoolStatus, len(globalEndpoints))
|
||||
for idx := range globalEndpoints {
|
||||
status, err := pools.Status(r.Context(), idx)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
poolsStatus[idx] = status
|
||||
}
|
||||
|
||||
adminLogIf(r.Context(), json.NewEncoder(w).Encode(poolsStatus))
|
||||
}
|
||||
|
||||
func (a adminAPIHandlers) RebalanceStart(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.RebalanceAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// NB rebalance-start admin API is always coordinated from first pool's
|
||||
// first node. The following is required to serialize (the effects of)
|
||||
// concurrent rebalance-start commands.
|
||||
if ep := globalEndpoints[0].Endpoints[0]; !ep.IsLocal {
|
||||
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
||||
if proxyEp.Host == ep.Host {
|
||||
if proxied, success := proxyRequestByNodeIndex(ctx, w, r, nodeIdx, false); proxied && success {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pools, ok := objectAPI.(*erasureServerPools)
|
||||
if !ok || len(pools.serverPools) == 1 {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if pools.IsDecommissionRunning() {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errRebalanceDecommissionAlreadyRunning), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if pools.IsRebalanceStarted(ctx) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminRebalanceAlreadyStarted), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
bucketInfos, err := objectAPI.ListBuckets(ctx, BucketOptions{})
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
buckets := make([]string, 0, len(bucketInfos))
|
||||
for _, bInfo := range bucketInfos {
|
||||
buckets = append(buckets, bInfo.Name)
|
||||
}
|
||||
|
||||
var id string
|
||||
if id, err = pools.initRebalanceMeta(ctx, buckets); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Rebalance routine is run on the first node of any pool participating in rebalance.
|
||||
pools.StartRebalance()
|
||||
|
||||
b, err := json.Marshal(struct {
|
||||
ID string `json:"id"`
|
||||
}{ID: id})
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAPIError(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, b)
|
||||
// Notify peers to load rebalance.bin and start rebalance routine if they happen to be
|
||||
// participating pool's leader node
|
||||
globalNotificationSys.LoadRebalanceMeta(ctx, true)
|
||||
}
|
||||
|
||||
func (a adminAPIHandlers) RebalanceStatus(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.RebalanceAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Proxy rebalance-status to first pool first node, so that users see a
|
||||
// consistent view of rebalance progress even though different rebalancing
|
||||
// pools may temporarily have out of date info on the others.
|
||||
if ep := globalEndpoints[0].Endpoints[0]; !ep.IsLocal {
|
||||
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
||||
if proxyEp.Host == ep.Host {
|
||||
if proxied, success := proxyRequestByNodeIndex(ctx, w, r, nodeIdx, false); proxied && success {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pools, ok := objectAPI.(*erasureServerPools)
|
||||
if !ok {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
rs, err := rebalanceStatus(ctx, pools)
|
||||
if err != nil {
|
||||
if errors.Is(err, errRebalanceNotStarted) || errors.Is(err, errConfigNotFound) {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminRebalanceNotStarted), r.URL)
|
||||
return
|
||||
}
|
||||
adminLogIf(ctx, fmt.Errorf("failed to fetch rebalance status: %w", err))
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
adminLogIf(r.Context(), json.NewEncoder(w).Encode(rs))
|
||||
}
|
||||
|
||||
func (a adminAPIHandlers) RebalanceStop(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.RebalanceAdminAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
pools, ok := objectAPI.(*erasureServerPools)
|
||||
if !ok {
|
||||
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrNotImplemented), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
// Cancel any ongoing rebalance operation
|
||||
globalNotificationSys.StopRebalance(r.Context())
|
||||
writeSuccessResponseHeadersOnly(w)
|
||||
adminLogIf(ctx, pools.saveRebalanceStats(GlobalContext, 0, rebalSaveStoppedAt))
|
||||
globalNotificationSys.LoadRebalanceMeta(ctx, false)
|
||||
}
|
||||
|
||||
func proxyDecommissionRequest(ctx context.Context, defaultEndPoint Endpoint, w http.ResponseWriter, r *http.Request) (proxy bool) {
|
||||
host := env.Get("_MINIO_DECOM_ENDPOINT_HOST", defaultEndPoint.Host)
|
||||
if host == "" {
|
||||
return proxy
|
||||
}
|
||||
for nodeIdx, proxyEp := range globalProxyEndpoints {
|
||||
if proxyEp.Host == host && !proxyEp.IsLocal {
|
||||
if proxied, success := proxyRequestByNodeIndex(ctx, w, r, nodeIdx, false); proxied && success {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return proxy
|
||||
}
|
||||
@@ -1,625 +0,0 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
xioutil "github.com/minio/minio/internal/ioutil"
|
||||
"github.com/minio/mux"
|
||||
"github.com/pgsty/silo-pkg/v3/policy"
|
||||
)
|
||||
|
||||
// SiteReplicationAdd - PUT /minio/admin/v3/site-replication/add
|
||||
func (a adminAPIHandlers) SiteReplicationAdd(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, policy.SiteReplicationAddAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var sites []madmin.PeerSite
|
||||
if err := parseJSONBody(ctx, r.Body, &sites, cred.SecretKey); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
opts := getSRAddOptions(r)
|
||||
status, err := globalSiteReplicationSys.AddPeerClusters(ctx, sites, opts)
|
||||
if err != nil {
|
||||
adminLogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := json.Marshal(status)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, body)
|
||||
}
|
||||
|
||||
func getSRAddOptions(r *http.Request) (opts madmin.SRAddOptions) {
|
||||
opts.ReplicateILMExpiry = r.Form.Get("replicateILMExpiry") == "true"
|
||||
return opts
|
||||
}
|
||||
|
||||
// SRPeerJoin - PUT /minio/admin/v3/site-replication/join
|
||||
//
|
||||
// used internally to tell current cluster to enable SR with
|
||||
// the provided peer clusters and service account.
|
||||
func (a adminAPIHandlers) SRPeerJoin(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, policy.SiteReplicationAddAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var joinArg madmin.SRPeerJoinReq
|
||||
if err := parseJSONBody(ctx, r.Body, &joinArg, cred.SecretKey); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err := globalSiteReplicationSys.PeerJoinReq(ctx, joinArg); err != nil {
|
||||
adminLogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SRPeerBucketOps - PUT /minio/admin/v3/site-replication/bucket-ops?bucket=x&operation=y
|
||||
func (a adminAPIHandlers) SRPeerBucketOps(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationOperationAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
bucket := vars["bucket"]
|
||||
operation := madmin.BktOp(vars["operation"])
|
||||
|
||||
var err error
|
||||
switch operation {
|
||||
default:
|
||||
err = errSRInvalidRequest(errInvalidArgument)
|
||||
case madmin.MakeWithVersioningBktOp:
|
||||
createdAt, cerr := time.Parse(time.RFC3339Nano, strings.TrimSpace(r.Form.Get("createdAt")))
|
||||
if cerr != nil {
|
||||
createdAt = timeSentinel
|
||||
}
|
||||
|
||||
opts := MakeBucketOptions{
|
||||
LockEnabled: r.Form.Get("lockEnabled") == "true",
|
||||
VersioningEnabled: r.Form.Get("versioningEnabled") == "true",
|
||||
ForceCreate: r.Form.Get("forceCreate") == "true",
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
err = globalSiteReplicationSys.PeerBucketMakeWithVersioningHandler(ctx, bucket, opts)
|
||||
case madmin.ConfigureReplBktOp:
|
||||
err = globalSiteReplicationSys.PeerBucketConfigureReplHandler(ctx, bucket)
|
||||
case madmin.DeleteBucketBktOp, madmin.ForceDeleteBucketBktOp:
|
||||
err = globalSiteReplicationSys.PeerBucketDeleteHandler(ctx, bucket, DeleteBucketOptions{
|
||||
Force: operation == madmin.ForceDeleteBucketBktOp,
|
||||
SRDeleteOp: getSRBucketDeleteOp(true),
|
||||
})
|
||||
case madmin.PurgeDeletedBucketOp:
|
||||
globalSiteReplicationSys.purgeDeletedBucket(ctx, objectAPI, bucket)
|
||||
}
|
||||
if err != nil {
|
||||
adminLogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SRPeerReplicateIAMItem - PUT /minio/admin/v3/site-replication/iam-item
|
||||
func (a adminAPIHandlers) SRPeerReplicateIAMItem(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationOperationAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var item madmin.SRIAMItem
|
||||
if err := parseJSONBody(ctx, r.Body, &item, ""); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
switch item.Type {
|
||||
default:
|
||||
err = errSRInvalidRequest(errInvalidArgument)
|
||||
case madmin.SRIAMItemPolicy:
|
||||
if item.Policy == nil {
|
||||
err = globalSiteReplicationSys.PeerAddPolicyHandler(ctx, item.Name, nil, item.UpdatedAt)
|
||||
} else {
|
||||
policy, perr := policy.ParseConfig(bytes.NewReader(item.Policy))
|
||||
if perr != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, perr), r.URL)
|
||||
return
|
||||
}
|
||||
if policy.IsEmpty() {
|
||||
err = globalSiteReplicationSys.PeerAddPolicyHandler(ctx, item.Name, nil, item.UpdatedAt)
|
||||
} else {
|
||||
err = globalSiteReplicationSys.PeerAddPolicyHandler(ctx, item.Name, policy, item.UpdatedAt)
|
||||
}
|
||||
}
|
||||
case madmin.SRIAMItemSvcAcc:
|
||||
err = globalSiteReplicationSys.PeerSvcAccChangeHandler(ctx, item.SvcAccChange, item.UpdatedAt)
|
||||
case madmin.SRIAMItemPolicyMapping:
|
||||
err = globalSiteReplicationSys.PeerPolicyMappingHandler(ctx, item.PolicyMapping, item.UpdatedAt)
|
||||
case madmin.SRIAMItemSTSAcc:
|
||||
err = globalSiteReplicationSys.PeerSTSAccHandler(ctx, item.STSCredential, item.UpdatedAt)
|
||||
case madmin.SRIAMItemIAMUser:
|
||||
err = globalSiteReplicationSys.PeerIAMUserChangeHandler(ctx, item.IAMUser, item.UpdatedAt)
|
||||
case madmin.SRIAMItemGroupInfo:
|
||||
err = globalSiteReplicationSys.PeerGroupInfoChangeHandler(ctx, item.GroupInfo, item.UpdatedAt)
|
||||
}
|
||||
if err != nil {
|
||||
adminLogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SRPeerReplicateBucketItem - PUT /minio/admin/v3/site-replication/peer/bucket-meta
|
||||
func (a adminAPIHandlers) SRPeerReplicateBucketItem(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationOperationAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var item madmin.SRBucketMeta
|
||||
if err := parseJSONBody(ctx, r.Body, &item, ""); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if item.Bucket == "" {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errSRInvalidRequest(errInvalidArgument)), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
switch item.Type {
|
||||
default:
|
||||
err = globalSiteReplicationSys.PeerBucketMetadataUpdateHandler(ctx, item)
|
||||
case madmin.SRBucketMetaTypePolicy:
|
||||
if item.Policy == nil {
|
||||
err = globalSiteReplicationSys.PeerBucketPolicyHandler(ctx, item.Bucket, nil, item.UpdatedAt)
|
||||
} else {
|
||||
bktPolicy, berr := policy.ParseBucketPolicyConfig(bytes.NewReader(item.Policy), item.Bucket)
|
||||
if berr != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, berr), r.URL)
|
||||
return
|
||||
}
|
||||
if bktPolicy.IsEmpty() {
|
||||
err = globalSiteReplicationSys.PeerBucketPolicyHandler(ctx, item.Bucket, nil, item.UpdatedAt)
|
||||
} else {
|
||||
err = globalSiteReplicationSys.PeerBucketPolicyHandler(ctx, item.Bucket, bktPolicy, item.UpdatedAt)
|
||||
}
|
||||
}
|
||||
case madmin.SRBucketMetaTypeQuotaConfig:
|
||||
if item.Quota == nil {
|
||||
err = globalSiteReplicationSys.PeerBucketQuotaConfigHandler(ctx, item.Bucket, nil, item.UpdatedAt)
|
||||
} else {
|
||||
quotaConfig, err := parseBucketQuota(item.Bucket, item.Quota)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if err = globalSiteReplicationSys.PeerBucketQuotaConfigHandler(ctx, item.Bucket, quotaConfig, item.UpdatedAt); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
case madmin.SRBucketMetaTypeVersionConfig:
|
||||
err = globalSiteReplicationSys.PeerBucketVersioningHandler(ctx, item.Bucket, item.Versioning, item.UpdatedAt)
|
||||
case madmin.SRBucketMetaTypeTags:
|
||||
err = globalSiteReplicationSys.PeerBucketTaggingHandler(ctx, item.Bucket, item.Tags, item.UpdatedAt)
|
||||
case madmin.SRBucketMetaTypeObjectLockConfig:
|
||||
err = globalSiteReplicationSys.peerBucketObjectLockConfigItem(ctx, item)
|
||||
case madmin.SRBucketMetaTypeSSEConfig:
|
||||
err = globalSiteReplicationSys.PeerBucketSSEConfigHandler(ctx, item.Bucket, item.SSEConfig, item.UpdatedAt)
|
||||
case madmin.SRBucketMetaTypeCorsConfig:
|
||||
err = globalSiteReplicationSys.PeerBucketCorsConfigHandler(ctx, item.Bucket, item.Cors, item.UpdatedAt)
|
||||
case madmin.SRBucketMetaLCConfig:
|
||||
err = globalSiteReplicationSys.PeerBucketLCConfigHandler(ctx, item.Bucket, item.ExpiryLCConfig, item.UpdatedAt)
|
||||
}
|
||||
if err != nil {
|
||||
adminLogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SiteReplicationInfo - GET /minio/admin/v3/site-replication/info
|
||||
func (a adminAPIHandlers) SiteReplicationInfo(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationInfoAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
info, err := globalSiteReplicationSys.GetClusterInfo(ctx)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = json.NewEncoder(w).Encode(info); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (a adminAPIHandlers) SRPeerGetIDPSettings(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationAddAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
idpSettings := globalSiteReplicationSys.GetIDPSettings(ctx)
|
||||
if err := json.NewEncoder(w).Encode(idpSettings); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func parseJSONBody(ctx context.Context, body io.Reader, v any, encryptionKey string) error {
|
||||
data, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return SRError{
|
||||
Cause: err,
|
||||
Code: ErrSiteReplicationInvalidRequest,
|
||||
}
|
||||
}
|
||||
if encryptionKey != "" {
|
||||
data, err = madmin.DecryptData(encryptionKey, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return SRError{
|
||||
Cause: err,
|
||||
Code: ErrSiteReplicationInvalidRequest,
|
||||
}
|
||||
}
|
||||
}
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
// SiteReplicationStatus - GET /minio/admin/v3/site-replication/status
|
||||
func (a adminAPIHandlers) SiteReplicationStatus(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationInfoAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
opts := getSRStatusOptions(r)
|
||||
// default options to all if status options are unset for backward compatibility
|
||||
var dfltOpts madmin.SRStatusOptions
|
||||
if opts == dfltOpts {
|
||||
opts.Buckets = true
|
||||
opts.Users = true
|
||||
opts.Policies = true
|
||||
opts.Groups = true
|
||||
opts.ILMExpiryRules = true
|
||||
}
|
||||
info, err := globalSiteReplicationSys.SiteReplicationStatus(ctx, objectAPI, opts)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
// Report the ILMExpiryStats only if at least one site has replication of ILM expiry enabled
|
||||
var replicateILMExpiry bool
|
||||
for _, site := range info.Sites {
|
||||
if site.ReplicateILMExpiry {
|
||||
replicateILMExpiry = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !replicateILMExpiry {
|
||||
// explicitly send nil for ILMExpiryStats
|
||||
info.ILMExpiryStats = nil
|
||||
}
|
||||
|
||||
if err = json.NewEncoder(w).Encode(info); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SiteReplicationMetaInfo - GET /minio/admin/v3/site-replication/metainfo
|
||||
func (a adminAPIHandlers) SiteReplicationMetaInfo(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationInfoAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
opts := getSRStatusOptions(r)
|
||||
info, err := globalSiteReplicationSys.SiteReplicationMetaInfo(ctx, objectAPI, opts)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err = json.NewEncoder(w).Encode(info); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SiteReplicationEdit - PUT /minio/admin/v3/site-replication/edit
|
||||
func (a adminAPIHandlers) SiteReplicationEdit(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, cred := validateAdminReq(ctx, w, r, policy.SiteReplicationAddAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
var site madmin.PeerInfo
|
||||
err := parseJSONBody(ctx, r.Body, &site, cred.SecretKey)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
opts := getSREditOptions(r)
|
||||
status, err := globalSiteReplicationSys.EditPeerCluster(ctx, site, opts)
|
||||
if err != nil {
|
||||
adminLogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
body, err := json.Marshal(status)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccessResponseJSON(w, body)
|
||||
}
|
||||
|
||||
func getSREditOptions(r *http.Request) (opts madmin.SREditOptions) {
|
||||
opts.DisableILMExpiryReplication = r.Form.Get("disableILMExpiryReplication") == "true"
|
||||
opts.EnableILMExpiryReplication = r.Form.Get("enableILMExpiryReplication") == "true"
|
||||
return opts
|
||||
}
|
||||
|
||||
// SRPeerEdit - PUT /minio/admin/v3/site-replication/peer/edit
|
||||
//
|
||||
// used internally to tell current cluster to update endpoint for peer
|
||||
func (a adminAPIHandlers) SRPeerEdit(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationAddAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var pi madmin.PeerInfo
|
||||
if err := parseJSONBody(ctx, r.Body, &pi, ""); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err := globalSiteReplicationSys.PeerEditReq(ctx, pi); err != nil {
|
||||
adminLogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SRStateEdit - PUT /minio/admin/v3/site-replication/state/edit
|
||||
//
|
||||
// used internally to tell current cluster to update site replication state
|
||||
func (a adminAPIHandlers) SRStateEdit(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationOperationAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var state madmin.SRStateEditReq
|
||||
if err := parseJSONBody(ctx, r.Body, &state, ""); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
if err := globalSiteReplicationSys.PeerStateEditReq(ctx, state); err != nil {
|
||||
adminLogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func getSRStatusOptions(r *http.Request) (opts madmin.SRStatusOptions) {
|
||||
q := r.Form
|
||||
opts.Buckets = q.Get("buckets") == "true"
|
||||
opts.Policies = q.Get("policies") == "true"
|
||||
opts.Groups = q.Get("groups") == "true"
|
||||
opts.Users = q.Get("users") == "true"
|
||||
opts.ILMExpiryRules = q.Get("ilm-expiry-rules") == "true"
|
||||
opts.PeerState = q.Get("peer-state") == "true"
|
||||
opts.Entity = madmin.GetSREntityType(q.Get("entity"))
|
||||
opts.EntityValue = q.Get("entityvalue")
|
||||
opts.ShowDeleted = q.Get("showDeleted") == "true"
|
||||
opts.Metrics = q.Get("metrics") == "true"
|
||||
return opts
|
||||
}
|
||||
|
||||
// SiteReplicationRemove - PUT /minio/admin/v3/site-replication/remove
|
||||
func (a adminAPIHandlers) SiteReplicationRemove(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationRemoveAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
var rreq madmin.SRRemoveReq
|
||||
err := parseJSONBody(ctx, r.Body, &rreq, "")
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
status, err := globalSiteReplicationSys.RemovePeerCluster(ctx, objectAPI, rreq)
|
||||
if err != nil {
|
||||
adminLogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := json.Marshal(status)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
writeSuccessResponseJSON(w, body)
|
||||
}
|
||||
|
||||
// SRPeerRemove - PUT /minio/admin/v3/site-replication/peer/remove
|
||||
//
|
||||
// used internally to tell current cluster to update endpoint for peer
|
||||
func (a adminAPIHandlers) SRPeerRemove(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationRemoveAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var req madmin.SRRemoveReq
|
||||
if err := parseJSONBody(ctx, r.Body, &req, ""); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
|
||||
if err := globalSiteReplicationSys.InternalRemoveReq(ctx, objectAPI, req); err != nil {
|
||||
adminLogIf(ctx, err)
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SiteReplicationResyncOp - PUT /minio/admin/v3/site-replication/resync/op
|
||||
func (a adminAPIHandlers) SiteReplicationResyncOp(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
objectAPI, _ := validateAdminReq(ctx, w, r, policy.SiteReplicationResyncAction)
|
||||
if objectAPI == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var peerSite madmin.PeerInfo
|
||||
if err := parseJSONBody(ctx, r.Body, &peerSite, ""); err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
vars := mux.Vars(r)
|
||||
op := madmin.SiteResyncOp(vars["operation"])
|
||||
var (
|
||||
status madmin.SRResyncOpStatus
|
||||
err error
|
||||
)
|
||||
switch op {
|
||||
case madmin.SiteResyncStart:
|
||||
status, err = globalSiteReplicationSys.startResync(ctx, objectAPI, peerSite)
|
||||
case madmin.SiteResyncCancel:
|
||||
status, err = globalSiteReplicationSys.cancelResync(ctx, objectAPI, peerSite)
|
||||
default:
|
||||
err = errSRInvalidRequest(errInvalidArgument)
|
||||
}
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
body, err := json.Marshal(status)
|
||||
if err != nil {
|
||||
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
|
||||
return
|
||||
}
|
||||
writeSuccessResponseJSON(w, body)
|
||||
}
|
||||
|
||||
// SiteReplicationDevNull - everything goes to io.Discard
|
||||
// [POST] /minio/admin/v3/site-replication/devnull
|
||||
func (a adminAPIHandlers) SiteReplicationDevNull(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
globalSiteNetPerfRX.Connect()
|
||||
defer globalSiteNetPerfRX.Disconnect()
|
||||
|
||||
connectTime := time.Now()
|
||||
for {
|
||||
n, err := io.CopyN(xioutil.Discard, r.Body, 128*humanize.KiByte)
|
||||
atomic.AddUint64(&globalSiteNetPerfRX.RX, uint64(n))
|
||||
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||
// If there is a disconnection before globalNetPerfMinDuration (we give a margin of error of 1 sec)
|
||||
// would mean the network is not stable. Logging here will help in debugging network issues.
|
||||
if time.Since(connectTime) < (globalNetPerfMinDuration - time.Second) {
|
||||
adminLogIf(ctx, err)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
} else {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SiteReplicationNetPerf - everything goes to io.Discard
|
||||
// [POST] /minio/admin/v3/site-replication/netperf
|
||||
func (a adminAPIHandlers) SiteReplicationNetPerf(w http.ResponseWriter, r *http.Request) {
|
||||
durationStr := r.Form.Get(peerRESTDuration)
|
||||
duration, _ := time.ParseDuration(durationStr)
|
||||
if duration < globalNetPerfMinDuration {
|
||||
duration = globalNetPerfMinDuration
|
||||
}
|
||||
result := siteNetperf(r.Context(), duration)
|
||||
adminLogIf(r.Context(), gob.NewEncoder(w).Encode(result))
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//go:build !race
|
||||
// +build !race
|
||||
|
||||
// Tests in this file are not run under the `-race` flag as they are too slow
|
||||
// and cause context deadline errors.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
minio "github.com/minio/minio-go/v7"
|
||||
"github.com/pgsty/silo-pkg/v3/sync/errgroup"
|
||||
)
|
||||
|
||||
func runAllIAMConcurrencyTests(suite *TestSuiteIAM, c *check) {
|
||||
suite.SetUpSuite(c)
|
||||
suite.TestDeleteUserRace(c)
|
||||
suite.TearDownSuite(c)
|
||||
}
|
||||
|
||||
func TestIAMInternalIDPConcurrencyServerSuite(t *testing.T) {
|
||||
if runtime.GOOS == globalWindowsOSName {
|
||||
t.Skip("windows is clunky")
|
||||
}
|
||||
|
||||
baseTestCases := []TestSuiteCommon{
|
||||
// Init and run test on ErasureSD backend with signature v4.
|
||||
{serverType: "ErasureSD", signer: signerV4},
|
||||
// Init and run test on ErasureSD backend, with tls enabled.
|
||||
{serverType: "ErasureSD", signer: signerV4, secure: true},
|
||||
// Init and run test on Erasure backend.
|
||||
{serverType: "Erasure", signer: signerV4},
|
||||
// Init and run test on ErasureSet backend.
|
||||
{serverType: "ErasureSet", signer: signerV4},
|
||||
}
|
||||
testCases := []*TestSuiteIAM{}
|
||||
for _, bt := range baseTestCases {
|
||||
testCases = append(testCases,
|
||||
newTestSuiteIAM(bt, false),
|
||||
newTestSuiteIAM(bt, true),
|
||||
)
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
etcdStr := ""
|
||||
if testCase.withEtcdBackend {
|
||||
etcdStr = " (with etcd backend)"
|
||||
}
|
||||
t.Run(
|
||||
fmt.Sprintf("Test: %d, ServerType: %s%s", i+1, testCase.serverType, etcdStr),
|
||||
func(t *testing.T) {
|
||||
runAllIAMConcurrencyTests(testCase, &check{t, testCase.serverType})
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TestSuiteIAM) TestDeleteUserRace(c *check) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
defer cancel()
|
||||
|
||||
bucket := getRandomBucketName()
|
||||
err := s.client.MakeBucket(ctx, bucket, minio.MakeBucketOptions{})
|
||||
if err != nil {
|
||||
c.Fatalf("bucket creat error: %v", err)
|
||||
}
|
||||
|
||||
// Create a policy policy
|
||||
policy := "mypolicy"
|
||||
policyBytes := fmt.Appendf(nil, `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"s3:PutObject",
|
||||
"s3:GetObject",
|
||||
"s3:ListBucket"
|
||||
],
|
||||
"Resource": [
|
||||
"arn:aws:s3:::%s/*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}`, bucket)
|
||||
err = s.adm.AddCannedPolicy(ctx, policy, policyBytes)
|
||||
if err != nil {
|
||||
c.Fatalf("policy add error: %v", err)
|
||||
}
|
||||
|
||||
userCount := 50
|
||||
accessKeys := make([]string, userCount)
|
||||
secretKeys := make([]string, userCount)
|
||||
for i := range userCount {
|
||||
accessKey, secretKey := mustGenerateCredentials(c)
|
||||
err = s.adm.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled)
|
||||
if err != nil {
|
||||
c.Fatalf("Unable to set user: %v", err)
|
||||
}
|
||||
|
||||
userReq := madmin.PolicyAssociationReq{
|
||||
Policies: []string{policy},
|
||||
User: accessKey,
|
||||
}
|
||||
if _, err := s.adm.AttachPolicy(ctx, userReq); err != nil {
|
||||
c.Fatalf("Unable to attach policy: %v", err)
|
||||
}
|
||||
|
||||
accessKeys[i] = accessKey
|
||||
secretKeys[i] = secretKey
|
||||
}
|
||||
|
||||
g := errgroup.Group{}
|
||||
for i := range userCount {
|
||||
g.Go(func(i int) func() error {
|
||||
return func() error {
|
||||
uClient := s.getUserClient(c, accessKeys[i], secretKeys[i], "")
|
||||
err := s.adm.RemoveUser(ctx, accessKeys[i])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.mustNotListObjects(ctx, uClient, bucket)
|
||||
return nil
|
||||
}
|
||||
}(i), i)
|
||||
}
|
||||
if errs := g.Wait(); len(errs) > 0 {
|
||||
c.Fatalf("unable to remove users: %v", errs)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,535 +0,0 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"sort"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/auth"
|
||||
"github.com/minio/mux"
|
||||
)
|
||||
|
||||
// adminErasureTestBed - encapsulates subsystems that need to be setup for
|
||||
// admin-handler unit tests.
|
||||
type adminErasureTestBed struct {
|
||||
erasureDirs []string
|
||||
objLayer ObjectLayer
|
||||
router *mux.Router
|
||||
done context.CancelFunc
|
||||
}
|
||||
|
||||
// prepareAdminErasureTestBed - helper function that setups a single-node
|
||||
// Erasure backend for admin-handler tests.
|
||||
func prepareAdminErasureTestBed(ctx context.Context) (*adminErasureTestBed, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
// reset global variables to start afresh.
|
||||
resetTestGlobals()
|
||||
|
||||
// Set globalIsErasure to indicate that the setup uses an erasure
|
||||
// code backend.
|
||||
globalIsErasure = true
|
||||
|
||||
// Initializing objectLayer for HealFormatHandler.
|
||||
objLayer, erasureDirs, xlErr := initTestErasureObjLayer(ctx)
|
||||
if xlErr != nil {
|
||||
cancel()
|
||||
return nil, xlErr
|
||||
}
|
||||
|
||||
// Initialize Silo server config.
|
||||
if err := newTestConfig(globalMinioDefaultRegion, objLayer); err != nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Initialize boot time
|
||||
globalBootTime = UTCNow()
|
||||
|
||||
globalEndpoints = mustGetPoolEndpoints(0, erasureDirs...)
|
||||
|
||||
initAllSubsystems(ctx)
|
||||
|
||||
initConfigSubsystem(ctx, objLayer)
|
||||
|
||||
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
|
||||
|
||||
// Setup admin mgmt REST API handlers.
|
||||
adminRouter := mux.NewRouter()
|
||||
registerAdminRouter(adminRouter, true)
|
||||
|
||||
return &adminErasureTestBed{
|
||||
erasureDirs: erasureDirs,
|
||||
objLayer: objLayer,
|
||||
router: adminRouter,
|
||||
done: cancel,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TearDown - method that resets the test bed for subsequent unit
|
||||
// tests to start afresh.
|
||||
func (atb *adminErasureTestBed) TearDown() {
|
||||
atb.done()
|
||||
removeRoots(atb.erasureDirs)
|
||||
resetTestGlobals()
|
||||
}
|
||||
|
||||
// initTestObjLayer - Helper function to initialize an Erasure-based object
|
||||
// layer and set globalObjectAPI.
|
||||
func initTestErasureObjLayer(ctx context.Context) (ObjectLayer, []string, error) {
|
||||
erasureDirs, err := getRandomDisks(16)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
endpoints := mustGetPoolEndpoints(0, erasureDirs...)
|
||||
globalPolicySys = NewPolicySys()
|
||||
objLayer, err := newErasureServerPools(ctx, endpoints)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Make objLayer available to all internal services via globalObjectAPI.
|
||||
globalObjLayerMutex.Lock()
|
||||
globalObjectAPI = objLayer
|
||||
globalObjLayerMutex.Unlock()
|
||||
return objLayer, erasureDirs, nil
|
||||
}
|
||||
|
||||
// cmdType - Represents different service subcomands like status, stop
|
||||
// and restart.
|
||||
type cmdType int
|
||||
|
||||
const (
|
||||
restartCmd cmdType = iota
|
||||
stopCmd
|
||||
)
|
||||
|
||||
// toServiceSignal - Helper function that translates a given cmdType
|
||||
// value to its corresponding serviceSignal value.
|
||||
func (c cmdType) toServiceSignal() serviceSignal {
|
||||
switch c {
|
||||
case restartCmd:
|
||||
return serviceRestart
|
||||
case stopCmd:
|
||||
return serviceStop
|
||||
}
|
||||
return serviceRestart
|
||||
}
|
||||
|
||||
func (c cmdType) toServiceAction() madmin.ServiceAction {
|
||||
switch c {
|
||||
case restartCmd:
|
||||
return madmin.ServiceActionRestart
|
||||
case stopCmd:
|
||||
return madmin.ServiceActionStop
|
||||
}
|
||||
return madmin.ServiceActionRestart
|
||||
}
|
||||
|
||||
// testServiceSignalReceiver - Helper function that simulates a
|
||||
// go-routine waiting on service signal.
|
||||
func testServiceSignalReceiver(cmd cmdType, t *testing.T) {
|
||||
expectedCmd := cmd.toServiceSignal()
|
||||
serviceCmd := <-globalServiceSignalCh
|
||||
if serviceCmd != expectedCmd {
|
||||
t.Errorf("Expected service command %v but received %v", expectedCmd, serviceCmd)
|
||||
}
|
||||
}
|
||||
|
||||
// getServiceCmdRequest - Constructs a management REST API request for service
|
||||
// subcommands for a given cmdType value.
|
||||
func getServiceCmdRequest(cmd cmdType, cred auth.Credentials) (*http.Request, error) {
|
||||
queryVal := url.Values{}
|
||||
queryVal.Set("action", string(cmd.toServiceAction()))
|
||||
queryVal.Set("type", "2")
|
||||
resource := adminPathPrefix + adminAPIVersionPrefix + "/service?" + queryVal.Encode()
|
||||
req, err := newTestRequest(http.MethodPost, resource, 0, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// management REST API uses signature V4 for authentication.
|
||||
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// testServicesCmdHandler - parametrizes service subcommand tests on
|
||||
// cmdType value.
|
||||
func testServicesCmdHandler(cmd cmdType, t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
adminTestBed, err := prepareAdminErasureTestBed(ctx)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to initialize a single node Erasure backend for admin handler tests.", err)
|
||||
}
|
||||
defer adminTestBed.TearDown()
|
||||
|
||||
// Initialize admin peers to make admin RPC calls. Note: In a
|
||||
// single node setup, this degenerates to a simple function
|
||||
// call under the hood.
|
||||
globalMinioAddr = "127.0.0.1:9000"
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Setting up a go routine to simulate ServerRouter's
|
||||
// handleServiceSignals for stop and restart commands.
|
||||
if cmd == restartCmd {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
testServiceSignalReceiver(cmd, t)
|
||||
}()
|
||||
}
|
||||
credentials := globalActiveCred
|
||||
|
||||
req, err := getServiceCmdRequest(cmd, credentials)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to build service status request %v", err)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
adminTestBed.router.ServeHTTP(rec, req)
|
||||
|
||||
resp, _ := io.ReadAll(rec.Body)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("Expected to receive %d status code but received %d. Body (%s)",
|
||||
http.StatusOK, rec.Code, string(resp))
|
||||
}
|
||||
|
||||
result := &serviceResult{}
|
||||
if err := json.Unmarshal(resp, result); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
_ = result
|
||||
|
||||
// Wait until testServiceSignalReceiver() called in a goroutine quits.
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Test for service restart management REST API.
|
||||
func TestServiceRestartHandler(t *testing.T) {
|
||||
testServicesCmdHandler(restartCmd, t)
|
||||
}
|
||||
|
||||
// buildAdminRequest - helper function to build an admin API request.
|
||||
func buildAdminRequest(queryVal url.Values, method, path string,
|
||||
contentLength int64, bodySeeker io.ReadSeeker) (*http.Request, error,
|
||||
) {
|
||||
req, err := newTestRequest(method,
|
||||
adminPathPrefix+adminAPIVersionPrefix+path+"?"+queryVal.Encode(),
|
||||
contentLength, bodySeeker)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cred := globalActiveCred
|
||||
err = signRequestV4(req, cred.AccessKey, cred.SecretKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func TestAdminServerInfo(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
adminTestBed, err := prepareAdminErasureTestBed(ctx)
|
||||
if err != nil {
|
||||
t.Fatal("Failed to initialize a single node Erasure backend for admin handler tests.", err)
|
||||
}
|
||||
|
||||
defer adminTestBed.TearDown()
|
||||
|
||||
// Initialize admin peers to make admin RPC calls.
|
||||
globalMinioAddr = "127.0.0.1:9000"
|
||||
|
||||
// Prepare query params for set-config mgmt REST API.
|
||||
queryVal := url.Values{}
|
||||
queryVal.Set("info", "")
|
||||
|
||||
req, err := buildAdminRequest(queryVal, http.MethodGet, "/info", 0, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to construct get-config object request - %v", err)
|
||||
}
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
adminTestBed.router.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("Expected to succeed but failed with %d", rec.Code)
|
||||
}
|
||||
|
||||
results := madmin.InfoMessage{}
|
||||
err = json.NewDecoder(rec.Body).Decode(&results)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to decode set config result json %v", err)
|
||||
}
|
||||
|
||||
if results.Region != globalMinioDefaultRegion {
|
||||
t.Errorf("Expected %s, got %s", globalMinioDefaultRegion, results.Region)
|
||||
}
|
||||
}
|
||||
|
||||
// TestToAdminAPIErrCode - test for toAdminAPIErrCode helper function.
|
||||
func TestToAdminAPIErrCode(t *testing.T) {
|
||||
testCases := []struct {
|
||||
err error
|
||||
expectedAPIErr APIErrorCode
|
||||
}{
|
||||
// 1. Server not in quorum.
|
||||
{
|
||||
err: errErasureWriteQuorum,
|
||||
expectedAPIErr: ErrAdminConfigNoQuorum,
|
||||
},
|
||||
// 2. No error.
|
||||
{
|
||||
err: nil,
|
||||
expectedAPIErr: ErrNone,
|
||||
},
|
||||
// 3. Non-admin API specific error.
|
||||
{
|
||||
err: errDiskNotFound,
|
||||
expectedAPIErr: toAPIErrorCode(GlobalContext, errDiskNotFound),
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range testCases {
|
||||
actualErr := toAdminAPIErrCode(GlobalContext, test.err)
|
||||
if actualErr != test.expectedAPIErr {
|
||||
t.Errorf("Test %d: Expected %v but received %v",
|
||||
i+1, test.expectedAPIErr, actualErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractHealInitParams(t *testing.T) {
|
||||
mkParams := func(clientToken string, forceStart, forceStop bool) url.Values {
|
||||
v := url.Values{}
|
||||
if clientToken != "" {
|
||||
v.Add(mgmtClientToken, clientToken)
|
||||
}
|
||||
if forceStart {
|
||||
v.Add(mgmtForceStart, "")
|
||||
}
|
||||
if forceStop {
|
||||
v.Add(mgmtForceStop, "")
|
||||
}
|
||||
return v
|
||||
}
|
||||
qParamsArr := []url.Values{
|
||||
// Invalid cases
|
||||
mkParams("", true, true),
|
||||
mkParams("111", true, true),
|
||||
mkParams("111", true, false),
|
||||
mkParams("111", false, true),
|
||||
// Valid cases follow
|
||||
mkParams("", true, false),
|
||||
mkParams("", false, true),
|
||||
mkParams("", false, false),
|
||||
mkParams("111", false, false),
|
||||
}
|
||||
varsArr := []map[string]string{
|
||||
// Invalid cases
|
||||
{mgmtPrefix: "objprefix"},
|
||||
// Valid cases
|
||||
{},
|
||||
{mgmtBucket: "bucket"},
|
||||
{mgmtBucket: "bucket", mgmtPrefix: "objprefix"},
|
||||
}
|
||||
|
||||
// Body is always valid - we do not test JSON decoding.
|
||||
body := `{"recursive": false, "dryRun": true, "remove": false, "scanMode": 0}`
|
||||
|
||||
// Test all combinations!
|
||||
for pIdx, params := range qParamsArr {
|
||||
for vIdx, vars := range varsArr {
|
||||
_, err := extractHealInitParams(vars, params, bytes.NewReader([]byte(body)))
|
||||
isErrCase := false
|
||||
if pIdx < 4 || vIdx < 1 {
|
||||
isErrCase = true
|
||||
}
|
||||
|
||||
if err != ErrNone && !isErrCase {
|
||||
t.Errorf("Got unexpected error: %v %v %v", pIdx, vIdx, err)
|
||||
} else if err == ErrNone && isErrCase {
|
||||
t.Errorf("Got no error but expected one: %v %v", pIdx, vIdx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type byResourceUID struct{ madmin.LockEntries }
|
||||
|
||||
func (b byResourceUID) Less(i, j int) bool {
|
||||
toUniqLock := func(entry madmin.LockEntry) string {
|
||||
return fmt.Sprintf("%s/%s", entry.Resource, entry.ID)
|
||||
}
|
||||
return toUniqLock(b.LockEntries[i]) < toUniqLock(b.LockEntries[j])
|
||||
}
|
||||
|
||||
func TestTopLockEntries(t *testing.T) {
|
||||
locksHeld := make(map[string][]lockRequesterInfo)
|
||||
var owners []string
|
||||
for i := range 4 {
|
||||
owners = append(owners, fmt.Sprintf("node-%d", i))
|
||||
}
|
||||
|
||||
// Simulate DeleteObjects of 10 objects in a single request. i.e same lock
|
||||
// request UID, but 10 different resource names associated with it.
|
||||
var lris []lockRequesterInfo
|
||||
uuid := mustGetUUID()
|
||||
for i := range 10 {
|
||||
resource := fmt.Sprintf("bucket/delete-object-%d", i)
|
||||
lri := lockRequesterInfo{
|
||||
Name: resource,
|
||||
Writer: true,
|
||||
UID: uuid,
|
||||
Owner: owners[i%len(owners)],
|
||||
Group: true,
|
||||
Quorum: 3,
|
||||
}
|
||||
lris = append(lris, lri)
|
||||
locksHeld[resource] = []lockRequesterInfo{lri}
|
||||
}
|
||||
|
||||
// Add a few concurrent read locks to the mix
|
||||
for i := range 50 {
|
||||
resource := fmt.Sprintf("bucket/get-object-%d", i)
|
||||
lri := lockRequesterInfo{
|
||||
Name: resource,
|
||||
UID: mustGetUUID(),
|
||||
Owner: owners[i%len(owners)],
|
||||
Quorum: 2,
|
||||
}
|
||||
lris = append(lris, lri)
|
||||
locksHeld[resource] = append(locksHeld[resource], lri)
|
||||
// concurrent read lock, same resource different uid
|
||||
lri.UID = mustGetUUID()
|
||||
lris = append(lris, lri)
|
||||
locksHeld[resource] = append(locksHeld[resource], lri)
|
||||
}
|
||||
|
||||
var peerLocks []*PeerLocks
|
||||
for _, owner := range owners {
|
||||
peerLocks = append(peerLocks, &PeerLocks{
|
||||
Addr: owner,
|
||||
Locks: locksHeld,
|
||||
})
|
||||
}
|
||||
var exp madmin.LockEntries
|
||||
for _, lri := range lris {
|
||||
lockType := func(lri lockRequesterInfo) string {
|
||||
if lri.Writer {
|
||||
return "WRITE"
|
||||
}
|
||||
return "READ"
|
||||
}
|
||||
exp = append(exp, madmin.LockEntry{
|
||||
Resource: lri.Name,
|
||||
Type: lockType(lri),
|
||||
ServerList: owners,
|
||||
Owner: lri.Owner,
|
||||
ID: lri.UID,
|
||||
Quorum: lri.Quorum,
|
||||
Timestamp: time.Unix(0, lri.Timestamp),
|
||||
})
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
peerLocks []*PeerLocks
|
||||
expected madmin.LockEntries
|
||||
}{
|
||||
{
|
||||
peerLocks: peerLocks,
|
||||
expected: exp,
|
||||
},
|
||||
}
|
||||
|
||||
// printEntries := func(entries madmin.LockEntries) {
|
||||
// for i, entry := range entries {
|
||||
// fmt.Printf("%d: %s %s %s %s %v %d\n", i, entry.Resource, entry.ID, entry.Owner, entry.Type, entry.ServerList, entry.Elapsed)
|
||||
// }
|
||||
// }
|
||||
|
||||
check := func(exp, got madmin.LockEntries) (int, bool) {
|
||||
if len(exp) != len(got) {
|
||||
return 0, false
|
||||
}
|
||||
sort.Slice(exp, byResourceUID{exp}.Less)
|
||||
sort.Slice(got, byResourceUID{got}.Less)
|
||||
// printEntries(exp)
|
||||
// printEntries(got)
|
||||
for i, e := range exp {
|
||||
if !e.Timestamp.Equal(got[i].Timestamp) {
|
||||
return i, false
|
||||
}
|
||||
// Skip checking elapsed since it's time sensitive.
|
||||
// if e.Elapsed != got[i].Elapsed {
|
||||
// return false
|
||||
// }
|
||||
if e.Resource != got[i].Resource {
|
||||
return i, false
|
||||
}
|
||||
if e.Type != got[i].Type {
|
||||
return i, false
|
||||
}
|
||||
if e.Source != got[i].Source {
|
||||
return i, false
|
||||
}
|
||||
if e.Owner != got[i].Owner {
|
||||
return i, false
|
||||
}
|
||||
if e.ID != got[i].ID {
|
||||
return i, false
|
||||
}
|
||||
if len(e.ServerList) != len(got[i].ServerList) {
|
||||
return i, false
|
||||
}
|
||||
for j := range e.ServerList {
|
||||
if e.ServerList[j] != got[i].ServerList[j] {
|
||||
return i, false
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, true
|
||||
}
|
||||
|
||||
for i, tc := range testCases {
|
||||
got := topLockEntries(tc.peerLocks, false)
|
||||
if idx, ok := check(tc.expected, got); !ok {
|
||||
t.Fatalf("%d: mismatch at %d \n expected %#v but got %#v", i, idx, tc.expected[idx], got[idx])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,918 +0,0 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
xioutil "github.com/minio/minio/internal/ioutil"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
)
|
||||
|
||||
// healStatusSummary - overall short summary of a healing sequence
|
||||
type healStatusSummary string
|
||||
|
||||
// healStatusSummary constants
|
||||
const (
|
||||
healNotStartedStatus healStatusSummary = "not started"
|
||||
healRunningStatus = "running"
|
||||
healStoppedStatus = "stopped"
|
||||
healFinishedStatus = "finished"
|
||||
)
|
||||
|
||||
const (
|
||||
// a heal sequence with this many un-consumed heal result
|
||||
// items blocks until heal-status consumption resumes or is
|
||||
// aborted due to timeout.
|
||||
maxUnconsumedHealResultItems = 1000
|
||||
|
||||
// if no heal-results are consumed (via the heal-status API)
|
||||
// for this timeout duration, the heal sequence is aborted.
|
||||
healUnconsumedTimeout = 24 * time.Hour
|
||||
|
||||
// time-duration to keep heal sequence state after it
|
||||
// completes.
|
||||
keepHealSeqStateDuration = time.Minute * 10
|
||||
|
||||
// nopHeal is a no operating healing action to
|
||||
// wait for the current healing operation to finish
|
||||
nopHeal = ""
|
||||
)
|
||||
|
||||
var (
|
||||
errHealIdleTimeout = errors.New("healing results were not consumed for too long")
|
||||
errHealStopSignalled = errors.New("heal stop signaled")
|
||||
|
||||
errFnHealFromAPIErr = func(ctx context.Context, err error) error {
|
||||
apiErr := toAdminAPIErr(ctx, err)
|
||||
return fmt.Errorf("Heal internal error: %s: %s",
|
||||
apiErr.Code, apiErr.Description)
|
||||
}
|
||||
)
|
||||
|
||||
// healSequenceStatus - accumulated status of the heal sequence
|
||||
type healSequenceStatus struct {
|
||||
// summary and detail for failures
|
||||
Summary healStatusSummary `json:"Summary"`
|
||||
FailureDetail string `json:"Detail,omitempty"`
|
||||
StartTime time.Time `json:"StartTime"`
|
||||
|
||||
// settings for the heal sequence
|
||||
HealSettings madmin.HealOpts `json:"Settings"`
|
||||
|
||||
// slice of available heal result records
|
||||
Items []madmin.HealResultItem `json:"Items"`
|
||||
}
|
||||
|
||||
// structure to hold state of all heal sequences in server memory
|
||||
type allHealState struct {
|
||||
sync.RWMutex
|
||||
|
||||
// map of heal path to heal sequence
|
||||
healSeqMap map[string]*healSequence // Indexed by endpoint
|
||||
// keep track of the healing status of disks in the memory
|
||||
// false: the disk needs to be healed but no healing routine is started
|
||||
// true: the disk is currently healing
|
||||
healLocalDisks map[Endpoint]bool
|
||||
healStatus map[string]healingTracker // Indexed by disk ID
|
||||
}
|
||||
|
||||
// newHealState - initialize global heal state management
|
||||
func newHealState(ctx context.Context, cleanup bool) *allHealState {
|
||||
hstate := &allHealState{
|
||||
healSeqMap: make(map[string]*healSequence),
|
||||
healLocalDisks: make(map[Endpoint]bool),
|
||||
healStatus: make(map[string]healingTracker),
|
||||
}
|
||||
if cleanup {
|
||||
go hstate.periodicHealSeqsClean(ctx)
|
||||
}
|
||||
return hstate
|
||||
}
|
||||
|
||||
func (ahs *allHealState) popHealLocalDisks(healLocalDisks ...Endpoint) {
|
||||
ahs.Lock()
|
||||
defer ahs.Unlock()
|
||||
|
||||
for _, ep := range healLocalDisks {
|
||||
delete(ahs.healLocalDisks, ep)
|
||||
}
|
||||
for id, disk := range ahs.healStatus {
|
||||
for _, ep := range healLocalDisks {
|
||||
if disk.Endpoint == ep.String() {
|
||||
delete(ahs.healStatus, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateHealStatus will update the heal status.
|
||||
func (ahs *allHealState) updateHealStatus(tracker *healingTracker) {
|
||||
ahs.Lock()
|
||||
defer ahs.Unlock()
|
||||
|
||||
tracker.mu.RLock()
|
||||
t := *tracker
|
||||
t.QueuedBuckets = append(make([]string, 0, len(tracker.QueuedBuckets)), tracker.QueuedBuckets...)
|
||||
t.HealedBuckets = append(make([]string, 0, len(tracker.HealedBuckets)), tracker.HealedBuckets...)
|
||||
ahs.healStatus[tracker.ID] = t
|
||||
tracker.mu.RUnlock()
|
||||
}
|
||||
|
||||
// Sort by zone, set and disk index
|
||||
func sortDisks(disks []madmin.Disk) {
|
||||
sort.Slice(disks, func(i, j int) bool {
|
||||
a, b := &disks[i], &disks[j]
|
||||
if a.PoolIndex != b.PoolIndex {
|
||||
return a.PoolIndex < b.PoolIndex
|
||||
}
|
||||
if a.SetIndex != b.SetIndex {
|
||||
return a.SetIndex < b.SetIndex
|
||||
}
|
||||
return a.DiskIndex < b.DiskIndex
|
||||
})
|
||||
}
|
||||
|
||||
// getLocalHealingDisks returns local healing disks indexed by endpoint.
|
||||
func (ahs *allHealState) getLocalHealingDisks() map[string]madmin.HealingDisk {
|
||||
ahs.RLock()
|
||||
defer ahs.RUnlock()
|
||||
dst := make(map[string]madmin.HealingDisk, len(ahs.healStatus))
|
||||
for _, v := range ahs.healStatus {
|
||||
dst[v.Endpoint] = v.toHealingDisk()
|
||||
}
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
// getHealLocalDiskEndpoints() returns the list of disks that need
|
||||
// to be healed but there is no healing routine in progress on them.
|
||||
func (ahs *allHealState) getHealLocalDiskEndpoints() Endpoints {
|
||||
ahs.RLock()
|
||||
defer ahs.RUnlock()
|
||||
|
||||
var endpoints Endpoints
|
||||
for ep, healing := range ahs.healLocalDisks {
|
||||
if !healing {
|
||||
endpoints = append(endpoints, ep)
|
||||
}
|
||||
}
|
||||
return endpoints
|
||||
}
|
||||
|
||||
// Set, in the memory, the state of the disk as currently healing or not
|
||||
func (ahs *allHealState) setDiskHealingStatus(ep Endpoint, healing bool) {
|
||||
ahs.Lock()
|
||||
defer ahs.Unlock()
|
||||
|
||||
ahs.healLocalDisks[ep] = healing
|
||||
}
|
||||
|
||||
func (ahs *allHealState) pushHealLocalDisks(healLocalDisks ...Endpoint) {
|
||||
ahs.Lock()
|
||||
defer ahs.Unlock()
|
||||
|
||||
for _, ep := range healLocalDisks {
|
||||
ahs.healLocalDisks[ep] = false
|
||||
}
|
||||
}
|
||||
|
||||
func (ahs *allHealState) periodicHealSeqsClean(ctx context.Context) {
|
||||
// Launch clean-up routine to remove this heal sequence (after
|
||||
// it ends) from the global state after timeout has elapsed.
|
||||
periodicTimer := time.NewTimer(time.Minute * 5)
|
||||
defer periodicTimer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-periodicTimer.C:
|
||||
now := UTCNow()
|
||||
ahs.Lock()
|
||||
for path, h := range ahs.healSeqMap {
|
||||
if h.hasEnded() && h.endTime.Add(keepHealSeqStateDuration).Before(now) {
|
||||
delete(ahs.healSeqMap, path)
|
||||
}
|
||||
}
|
||||
ahs.Unlock()
|
||||
|
||||
periodicTimer.Reset(time.Minute * 5)
|
||||
case <-ctx.Done():
|
||||
// server could be restarting - need
|
||||
// to exit immediately
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getHealSequenceByToken - Retrieve a heal sequence by token. The second
|
||||
// argument returns if a heal sequence actually exists.
|
||||
func (ahs *allHealState) getHealSequenceByToken(token string) (h *healSequence, exists bool) {
|
||||
ahs.RLock()
|
||||
defer ahs.RUnlock()
|
||||
for _, healSeq := range ahs.healSeqMap {
|
||||
if healSeq.clientToken == token {
|
||||
return healSeq, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// getHealSequence - Retrieve a heal sequence by path. The second
|
||||
// argument returns if a heal sequence actually exists.
|
||||
func (ahs *allHealState) getHealSequence(path string) (h *healSequence, exists bool) {
|
||||
ahs.RLock()
|
||||
defer ahs.RUnlock()
|
||||
h, exists = ahs.healSeqMap[path]
|
||||
return h, exists
|
||||
}
|
||||
|
||||
func (ahs *allHealState) stopHealSequence(path string) ([]byte, APIError) {
|
||||
var hsp madmin.HealStopSuccess
|
||||
he, exists := ahs.getHealSequence(path)
|
||||
if !exists {
|
||||
hsp = madmin.HealStopSuccess{
|
||||
ClientToken: "unknown",
|
||||
StartTime: UTCNow(),
|
||||
}
|
||||
} else {
|
||||
clientToken := he.clientToken
|
||||
if globalIsDistErasure {
|
||||
clientToken = fmt.Sprintf("%s%s%d", he.clientToken, getKeySeparator(), GetProxyEndpointLocalIndex(globalProxyEndpoints))
|
||||
}
|
||||
|
||||
hsp = madmin.HealStopSuccess{
|
||||
ClientToken: clientToken,
|
||||
ClientAddress: he.clientAddress,
|
||||
StartTime: he.startTime,
|
||||
}
|
||||
|
||||
he.stop()
|
||||
for !he.hasEnded() {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
ahs.Lock()
|
||||
defer ahs.Unlock()
|
||||
// Heal sequence explicitly stopped, remove it.
|
||||
delete(ahs.healSeqMap, path)
|
||||
}
|
||||
|
||||
b, err := json.Marshal(&hsp)
|
||||
return b, toAdminAPIErr(GlobalContext, err)
|
||||
}
|
||||
|
||||
// LaunchNewHealSequence - launches a background routine that performs
|
||||
// healing according to the healSequence argument. For each heal
|
||||
// sequence, state is stored in the `globalAllHealState`, which is a
|
||||
// map of the heal path to `healSequence` which holds state about the
|
||||
// heal sequence.
|
||||
//
|
||||
// Heal results are persisted in server memory for
|
||||
// `keepHealSeqStateDuration`. This function also launches a
|
||||
// background routine to clean up heal results after the
|
||||
// aforementioned duration.
|
||||
func (ahs *allHealState) LaunchNewHealSequence(h *healSequence, objAPI ObjectLayer) (
|
||||
respBytes []byte, apiErr APIError, errMsg string,
|
||||
) {
|
||||
if h.forceStarted {
|
||||
_, apiErr = ahs.stopHealSequence(pathJoin(h.bucket, h.object))
|
||||
if apiErr.Code != "" {
|
||||
return respBytes, apiErr, ""
|
||||
}
|
||||
} else {
|
||||
oh, exists := ahs.getHealSequence(pathJoin(h.bucket, h.object))
|
||||
if exists && !oh.hasEnded() {
|
||||
errMsg = "Heal is already running on the given path " +
|
||||
"(use force-start option to stop and start afresh). " +
|
||||
fmt.Sprintf("The heal was started by IP %s at %s, token is %s",
|
||||
h.clientAddress, h.startTime.Format(http.TimeFormat), h.clientToken)
|
||||
return nil, errorCodes.ToAPIErr(ErrHealAlreadyRunning), errMsg
|
||||
}
|
||||
}
|
||||
|
||||
ahs.Lock()
|
||||
defer ahs.Unlock()
|
||||
|
||||
// Check if new heal sequence to be started overlaps with any
|
||||
// existing, running sequence
|
||||
hpath := pathJoin(h.bucket, h.object)
|
||||
for k, hSeq := range ahs.healSeqMap {
|
||||
if !hSeq.hasEnded() && (HasPrefix(k, hpath) || HasPrefix(hpath, k)) {
|
||||
errMsg = "The provided heal sequence path overlaps with an existing " +
|
||||
fmt.Sprintf("heal path: %s", k)
|
||||
return nil, errorCodes.ToAPIErr(ErrHealOverlappingPaths), errMsg
|
||||
}
|
||||
}
|
||||
|
||||
// Add heal state and start sequence
|
||||
ahs.healSeqMap[hpath] = h
|
||||
|
||||
clientToken := h.clientToken
|
||||
if globalIsDistErasure {
|
||||
clientToken = fmt.Sprintf("%s%s%d", h.clientToken, getKeySeparator(), GetProxyEndpointLocalIndex(globalProxyEndpoints))
|
||||
}
|
||||
|
||||
if h.clientToken == bgHealingUUID {
|
||||
// For background heal do nothing, do not spawn an unnecessary goroutine.
|
||||
} else {
|
||||
// Launch top-level background heal go-routine
|
||||
go h.healSequenceStart(objAPI)
|
||||
}
|
||||
|
||||
b, err := json.Marshal(madmin.HealStartSuccess{
|
||||
ClientToken: clientToken,
|
||||
ClientAddress: h.clientAddress,
|
||||
StartTime: h.startTime,
|
||||
})
|
||||
if err != nil {
|
||||
bugLogIf(h.ctx, err)
|
||||
return nil, toAdminAPIErr(h.ctx, err), ""
|
||||
}
|
||||
return b, noError, ""
|
||||
}
|
||||
|
||||
// PopHealStatusJSON - Called by heal-status API. It fetches the heal
|
||||
// status results from global state and returns its JSON
|
||||
// representation. The clientToken helps ensure there aren't
|
||||
// conflicting clients fetching status.
|
||||
func (ahs *allHealState) PopHealStatusJSON(hpath string,
|
||||
clientToken string) ([]byte, APIErrorCode,
|
||||
) {
|
||||
// fetch heal state for given path
|
||||
h, exists := ahs.getHealSequence(hpath)
|
||||
if !exists {
|
||||
// heal sequence doesn't exist, must have finished.
|
||||
jbytes, err := json.Marshal(healSequenceStatus{
|
||||
Summary: healFinishedStatus,
|
||||
})
|
||||
return jbytes, toAdminAPIErrCode(GlobalContext, err)
|
||||
}
|
||||
|
||||
// Check if client-token is valid
|
||||
if clientToken != h.clientToken {
|
||||
return nil, ErrHealInvalidClientToken
|
||||
}
|
||||
|
||||
// Take lock to access and update the heal-sequence
|
||||
h.mutex.Lock()
|
||||
defer h.mutex.Unlock()
|
||||
|
||||
numItems := len(h.currentStatus.Items)
|
||||
|
||||
// calculate index of most recently available heal result
|
||||
// record.
|
||||
lastResultIndex := h.lastSentResultIndex
|
||||
if numItems > 0 {
|
||||
lastResultIndex = h.currentStatus.Items[numItems-1].ResultIndex
|
||||
}
|
||||
|
||||
h.lastSentResultIndex = lastResultIndex
|
||||
|
||||
jbytes, err := json.Marshal(h.currentStatus)
|
||||
if err != nil {
|
||||
h.currentStatus.Items = nil
|
||||
|
||||
bugLogIf(h.ctx, err)
|
||||
return nil, ErrInternalError
|
||||
}
|
||||
|
||||
h.currentStatus.Items = nil
|
||||
|
||||
return jbytes, ErrNone
|
||||
}
|
||||
|
||||
// healSource denotes single entity and heal option.
|
||||
type healSource struct {
|
||||
bucket string
|
||||
object string
|
||||
versionID string
|
||||
noWait bool // a non blocking call, if task queue is full return right away.
|
||||
opts *madmin.HealOpts // optional heal option overrides default setting
|
||||
}
|
||||
|
||||
// healSequence - state for each heal sequence initiated on the
|
||||
// server.
|
||||
type healSequence struct {
|
||||
// bucket, and object on which heal seq. was initiated
|
||||
bucket, object string
|
||||
|
||||
// Report healing progress
|
||||
reportProgress bool
|
||||
|
||||
// time at which heal sequence was started
|
||||
startTime time.Time
|
||||
|
||||
// time at which heal sequence has ended
|
||||
endTime time.Time
|
||||
|
||||
// Heal client info
|
||||
clientToken, clientAddress string
|
||||
|
||||
// was this heal sequence force started?
|
||||
forceStarted bool
|
||||
|
||||
// heal settings applied to this heal sequence
|
||||
settings madmin.HealOpts
|
||||
|
||||
// current accumulated status of the heal sequence
|
||||
currentStatus healSequenceStatus
|
||||
|
||||
// channel signaled by background routine when traversal has
|
||||
// completed
|
||||
traverseAndHealDoneCh chan error
|
||||
|
||||
// canceler to cancel heal sequence.
|
||||
cancelCtx context.CancelFunc
|
||||
|
||||
// the last result index sent to client
|
||||
lastSentResultIndex int64
|
||||
|
||||
// Number of total items scanned against item type
|
||||
scannedItemsMap map[madmin.HealItemType]int64
|
||||
|
||||
// Number of total items healed against item type
|
||||
healedItemsMap map[madmin.HealItemType]int64
|
||||
|
||||
// Number of total items where healing failed against item type
|
||||
healFailedItemsMap map[madmin.HealItemType]int64
|
||||
|
||||
// The time of the last scan/heal activity
|
||||
lastHealActivity time.Time
|
||||
|
||||
// Holds the request-info for logging
|
||||
ctx context.Context
|
||||
|
||||
// used to lock this structure as it is concurrently accessed
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
// NewHealSequence - creates healSettings, assumes bucket and
|
||||
// objPrefix are already validated.
|
||||
func newHealSequence(ctx context.Context, bucket, objPrefix, clientAddr string,
|
||||
hs madmin.HealOpts, forceStart bool,
|
||||
) *healSequence {
|
||||
reqInfo := &logger.ReqInfo{RemoteHost: clientAddr, API: "Heal", BucketName: bucket}
|
||||
reqInfo.AppendTags("prefix", objPrefix)
|
||||
ctx, cancel := context.WithCancel(logger.SetReqInfo(ctx, reqInfo))
|
||||
|
||||
clientToken := mustGetUUID()
|
||||
|
||||
return &healSequence{
|
||||
bucket: bucket,
|
||||
object: objPrefix,
|
||||
reportProgress: true,
|
||||
startTime: UTCNow(),
|
||||
clientToken: clientToken,
|
||||
clientAddress: clientAddr,
|
||||
forceStarted: forceStart,
|
||||
settings: hs,
|
||||
currentStatus: healSequenceStatus{
|
||||
Summary: healNotStartedStatus,
|
||||
HealSettings: hs,
|
||||
},
|
||||
traverseAndHealDoneCh: make(chan error),
|
||||
cancelCtx: cancel,
|
||||
ctx: ctx,
|
||||
scannedItemsMap: make(map[madmin.HealItemType]int64),
|
||||
healedItemsMap: make(map[madmin.HealItemType]int64),
|
||||
healFailedItemsMap: make(map[madmin.HealItemType]int64),
|
||||
}
|
||||
}
|
||||
|
||||
// getScannedItemsCount - returns a count of all scanned items
|
||||
func (h *healSequence) getScannedItemsCount() int64 {
|
||||
var count int64
|
||||
h.mutex.RLock()
|
||||
defer h.mutex.RUnlock()
|
||||
|
||||
for _, v := range h.scannedItemsMap {
|
||||
count += v
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// getScannedItemsMap - returns map of all scanned items against type
|
||||
func (h *healSequence) getScannedItemsMap() map[madmin.HealItemType]int64 {
|
||||
h.mutex.RLock()
|
||||
defer h.mutex.RUnlock()
|
||||
|
||||
// Make a copy before returning the value
|
||||
retMap := make(map[madmin.HealItemType]int64, len(h.scannedItemsMap))
|
||||
maps.Copy(retMap, h.scannedItemsMap)
|
||||
|
||||
return retMap
|
||||
}
|
||||
|
||||
// getHealedItemsMap - returns the map of all healed items against type
|
||||
func (h *healSequence) getHealedItemsMap() map[madmin.HealItemType]int64 {
|
||||
h.mutex.RLock()
|
||||
defer h.mutex.RUnlock()
|
||||
|
||||
// Make a copy before returning the value
|
||||
retMap := make(map[madmin.HealItemType]int64, len(h.healedItemsMap))
|
||||
maps.Copy(retMap, h.healedItemsMap)
|
||||
|
||||
return retMap
|
||||
}
|
||||
|
||||
// getHealFailedItemsMap - returns map of all items where heal failed against
|
||||
// drive endpoint and status
|
||||
func (h *healSequence) getHealFailedItemsMap() map[madmin.HealItemType]int64 {
|
||||
h.mutex.RLock()
|
||||
defer h.mutex.RUnlock()
|
||||
|
||||
// Make a copy before returning the value
|
||||
retMap := make(map[madmin.HealItemType]int64, len(h.healFailedItemsMap))
|
||||
maps.Copy(retMap, h.healFailedItemsMap)
|
||||
|
||||
return retMap
|
||||
}
|
||||
|
||||
func (h *healSequence) countFailed(healType madmin.HealItemType) {
|
||||
h.mutex.Lock()
|
||||
defer h.mutex.Unlock()
|
||||
|
||||
h.healFailedItemsMap[healType]++
|
||||
h.lastHealActivity = UTCNow()
|
||||
}
|
||||
|
||||
func (h *healSequence) countScanned(healType madmin.HealItemType) {
|
||||
h.mutex.Lock()
|
||||
defer h.mutex.Unlock()
|
||||
|
||||
h.scannedItemsMap[healType]++
|
||||
h.lastHealActivity = UTCNow()
|
||||
}
|
||||
|
||||
func (h *healSequence) countHealed(healType madmin.HealItemType) {
|
||||
h.mutex.Lock()
|
||||
defer h.mutex.Unlock()
|
||||
|
||||
h.healedItemsMap[healType]++
|
||||
h.lastHealActivity = UTCNow()
|
||||
}
|
||||
|
||||
// isQuitting - determines if the heal sequence is quitting (due to an
|
||||
// external signal)
|
||||
func (h *healSequence) isQuitting() bool {
|
||||
select {
|
||||
case <-h.ctx.Done():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// check if the heal sequence has ended
|
||||
func (h *healSequence) hasEnded() bool {
|
||||
h.mutex.RLock()
|
||||
defer h.mutex.RUnlock()
|
||||
// background heal never ends
|
||||
if h.clientToken == bgHealingUUID {
|
||||
return false
|
||||
}
|
||||
return !h.endTime.IsZero()
|
||||
}
|
||||
|
||||
// stops the heal sequence - safe to call multiple times.
|
||||
func (h *healSequence) stop() {
|
||||
h.cancelCtx()
|
||||
}
|
||||
|
||||
// pushHealResultItem - pushes a heal result item for consumption in
|
||||
// the heal-status API. It blocks if there are
|
||||
// maxUnconsumedHealResultItems. When it blocks, the heal sequence
|
||||
// routine is effectively paused - this happens when the server has
|
||||
// accumulated the maximum number of heal records per heal
|
||||
// sequence. When the client consumes further records, the heal
|
||||
// sequence automatically resumes. The return value indicates if the
|
||||
// operation succeeded.
|
||||
func (h *healSequence) pushHealResultItem(r madmin.HealResultItem) error {
|
||||
// start a timer to keep an upper time limit to find an empty
|
||||
// slot to add the given heal result - if no slot is found it
|
||||
// means that the server is holding the maximum amount of
|
||||
// heal-results in memory and the client has not consumed it
|
||||
// for too long.
|
||||
unconsumedTimer := time.NewTimer(healUnconsumedTimeout)
|
||||
defer unconsumedTimer.Stop()
|
||||
|
||||
var itemsLen int
|
||||
for {
|
||||
h.mutex.Lock()
|
||||
itemsLen = len(h.currentStatus.Items)
|
||||
if itemsLen == maxUnconsumedHealResultItems {
|
||||
// wait for a second, or quit if an external
|
||||
// stop signal is received or the
|
||||
// unconsumedTimer fires.
|
||||
select {
|
||||
// Check after a second
|
||||
case <-time.After(time.Second):
|
||||
h.mutex.Unlock()
|
||||
continue
|
||||
|
||||
case <-h.ctx.Done():
|
||||
h.mutex.Unlock()
|
||||
// discard result and return.
|
||||
return errHealStopSignalled
|
||||
|
||||
// Timeout if no results consumed for too long.
|
||||
case <-unconsumedTimer.C:
|
||||
h.mutex.Unlock()
|
||||
return errHealIdleTimeout
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Set the correct result index for the new result item
|
||||
if itemsLen > 0 {
|
||||
r.ResultIndex = 1 + h.currentStatus.Items[itemsLen-1].ResultIndex
|
||||
} else {
|
||||
r.ResultIndex = 1 + h.lastSentResultIndex
|
||||
}
|
||||
|
||||
// append to results
|
||||
h.currentStatus.Items = append(h.currentStatus.Items, r)
|
||||
|
||||
// release lock
|
||||
h.mutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// healSequenceStart - this is the top-level background heal
|
||||
// routine. It launches another go-routine that actually traverses
|
||||
// on-disk data, checks and heals according to the selected
|
||||
// settings. This go-routine itself, (1) monitors the traversal
|
||||
// routine for completion, and (2) listens for external stop
|
||||
// signals. When either event happens, it sets the finish status for
|
||||
// the heal-sequence.
|
||||
func (h *healSequence) healSequenceStart(objAPI ObjectLayer) {
|
||||
// Set status as running
|
||||
h.mutex.Lock()
|
||||
h.currentStatus.Summary = healRunningStatus
|
||||
h.currentStatus.StartTime = UTCNow()
|
||||
h.mutex.Unlock()
|
||||
|
||||
go h.traverseAndHeal(objAPI)
|
||||
|
||||
select {
|
||||
case err, ok := <-h.traverseAndHealDoneCh:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
h.mutex.Lock()
|
||||
h.endTime = UTCNow()
|
||||
// Heal traversal is complete.
|
||||
if err == nil {
|
||||
// heal traversal succeeded.
|
||||
h.currentStatus.Summary = healFinishedStatus
|
||||
} else {
|
||||
// heal traversal had an error.
|
||||
h.currentStatus.Summary = healStoppedStatus
|
||||
h.currentStatus.FailureDetail = err.Error()
|
||||
}
|
||||
h.mutex.Unlock()
|
||||
case <-h.ctx.Done():
|
||||
h.mutex.Lock()
|
||||
h.endTime = UTCNow()
|
||||
h.currentStatus.Summary = healFinishedStatus
|
||||
h.mutex.Unlock()
|
||||
|
||||
// drain traverse channel so the traversal
|
||||
// go-routine does not leak.
|
||||
go func() {
|
||||
// Eventually the traversal go-routine closes
|
||||
// the channel and returns, so this go-routine
|
||||
// itself will not leak.
|
||||
<-h.traverseAndHealDoneCh
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *healSequence) queueHealTask(source healSource, healType madmin.HealItemType) error {
|
||||
// Send heal request
|
||||
task := healTask{
|
||||
bucket: source.bucket,
|
||||
object: source.object,
|
||||
versionID: source.versionID,
|
||||
opts: h.settings,
|
||||
}
|
||||
if source.opts != nil {
|
||||
task.opts = *source.opts
|
||||
} else {
|
||||
task.opts.ScanMode = madmin.HealNormalScan
|
||||
}
|
||||
|
||||
h.countScanned(healType)
|
||||
|
||||
if source.noWait {
|
||||
select {
|
||||
case globalBackgroundHealRoutine.tasks <- task:
|
||||
if serverDebugLog {
|
||||
fmt.Printf("Task in the queue: %#v\n", task)
|
||||
}
|
||||
default:
|
||||
// task queue is full, no more workers, we shall move on and heal later.
|
||||
return nil
|
||||
}
|
||||
// Don't wait for result
|
||||
return nil
|
||||
}
|
||||
|
||||
// respCh must be set to wait for result.
|
||||
// We make it size 1, so a result can always be written
|
||||
// even if we aren't listening.
|
||||
task.respCh = make(chan healResult, 1)
|
||||
select {
|
||||
case globalBackgroundHealRoutine.tasks <- task:
|
||||
if serverDebugLog {
|
||||
fmt.Printf("Task in the queue: %#v\n", task)
|
||||
}
|
||||
case <-h.ctx.Done():
|
||||
return nil
|
||||
}
|
||||
|
||||
countOKDrives := func(drives []madmin.HealDriveInfo) (count int) {
|
||||
for _, drive := range drives {
|
||||
if drive.State == madmin.DriveStateOk {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// task queued, now wait for the response.
|
||||
select {
|
||||
case res := <-task.respCh:
|
||||
if res.err == nil {
|
||||
h.countHealed(healType)
|
||||
} else {
|
||||
h.countFailed(healType)
|
||||
}
|
||||
if !h.reportProgress {
|
||||
if errors.Is(res.err, errSkipFile) { // this is only sent usually by nopHeal
|
||||
return nil
|
||||
}
|
||||
|
||||
// Report caller of any failure
|
||||
return res.err
|
||||
}
|
||||
res.result.Type = healType
|
||||
if res.err != nil {
|
||||
res.result.Detail = res.err.Error()
|
||||
}
|
||||
if res.result.ParityBlocks > 0 && res.result.DataBlocks > 0 && res.result.DataBlocks > res.result.ParityBlocks {
|
||||
if got := countOKDrives(res.result.After.Drives); got < res.result.ParityBlocks {
|
||||
res.result.Detail = fmt.Sprintf("quorum loss - expected %d minimum, got drive states in OK %d", res.result.ParityBlocks, got)
|
||||
}
|
||||
}
|
||||
return h.pushHealResultItem(res.result)
|
||||
case <-h.ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (h *healSequence) healDiskMeta(objAPI ObjectLayer) error {
|
||||
// Start healing the config prefix.
|
||||
return h.healMinioSysMeta(objAPI, minioConfigPrefix)()
|
||||
}
|
||||
|
||||
func (h *healSequence) healItems(objAPI ObjectLayer) error {
|
||||
if h.clientToken == bgHealingUUID {
|
||||
// For background heal do nothing.
|
||||
return nil
|
||||
}
|
||||
|
||||
if h.bucket == "" { // heal internal meta only during a site-wide heal
|
||||
if err := h.healDiskMeta(objAPI); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Heal buckets and objects
|
||||
return h.healBuckets(objAPI)
|
||||
}
|
||||
|
||||
// traverseAndHeal - traverses on-disk data and performs healing
|
||||
// according to settings. At each "safe" point it also checks if an
|
||||
// external quit signal has been received and quits if so. Since the
|
||||
// healing traversal may be mutating on-disk data when an external
|
||||
// quit signal is received, this routine cannot quit immediately and
|
||||
// has to wait until a safe point is reached, such as between scanning
|
||||
// two objects.
|
||||
func (h *healSequence) traverseAndHeal(objAPI ObjectLayer) {
|
||||
h.traverseAndHealDoneCh <- h.healItems(objAPI)
|
||||
xioutil.SafeClose(h.traverseAndHealDoneCh)
|
||||
}
|
||||
|
||||
// healMinioSysMeta - heals all files under a given meta prefix, returns a function
|
||||
// which in-turn heals the respective meta directory path and any files in int.
|
||||
func (h *healSequence) healMinioSysMeta(objAPI ObjectLayer, metaPrefix string) func() error {
|
||||
return func() error {
|
||||
// NOTE: Healing on meta is run regardless
|
||||
// of any bucket being selected, this is to ensure that
|
||||
// meta are always upto date and correct.
|
||||
h.settings.Recursive = true
|
||||
return objAPI.HealObjects(h.ctx, minioMetaBucket, metaPrefix, h.settings, func(bucket, object, versionID string, scanMode madmin.HealScanMode) error {
|
||||
if h.isQuitting() {
|
||||
return errHealStopSignalled
|
||||
}
|
||||
|
||||
err := h.queueHealTask(healSource{
|
||||
bucket: bucket,
|
||||
object: object,
|
||||
versionID: versionID,
|
||||
}, madmin.HealItemBucketMetadata)
|
||||
return err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// healBuckets - check for all buckets heal or just particular bucket.
|
||||
func (h *healSequence) healBuckets(objAPI ObjectLayer) error {
|
||||
if h.isQuitting() {
|
||||
return errHealStopSignalled
|
||||
}
|
||||
|
||||
// 1. If a bucket was specified, heal only the bucket.
|
||||
if h.bucket != "" {
|
||||
return h.healBucket(objAPI, h.bucket, false)
|
||||
}
|
||||
|
||||
buckets, err := objAPI.ListBuckets(h.ctx, BucketOptions{})
|
||||
if err != nil {
|
||||
return errFnHealFromAPIErr(h.ctx, err)
|
||||
}
|
||||
|
||||
// Heal latest buckets first.
|
||||
sort.Slice(buckets, func(i, j int) bool {
|
||||
return buckets[i].Created.After(buckets[j].Created)
|
||||
})
|
||||
|
||||
for _, bucket := range buckets {
|
||||
if err = h.healBucket(objAPI, bucket.Name, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// healBucket - traverses and heals given bucket
|
||||
func (h *healSequence) healBucket(objAPI ObjectLayer, bucket string, bucketsOnly bool) error {
|
||||
if err := h.queueHealTask(healSource{bucket: bucket}, madmin.HealItemBucket); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bucketsOnly {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := objAPI.HealObjects(h.ctx, bucket, h.object, h.settings, h.healObject); err != nil {
|
||||
return errFnHealFromAPIErr(h.ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// healObject - heal the given object and record result
|
||||
func (h *healSequence) healObject(bucket, object, versionID string, scanMode madmin.HealScanMode) error {
|
||||
if h.isQuitting() {
|
||||
return errHealStopSignalled
|
||||
}
|
||||
|
||||
err := h.queueHealTask(healSource{
|
||||
bucket: bucket,
|
||||
object: object,
|
||||
versionID: versionID,
|
||||
opts: &h.settings,
|
||||
}, madmin.HealItemObject)
|
||||
|
||||
// Wait and proceed if there are active requests
|
||||
waitForLowHTTPReq()
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -1,441 +0,0 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/klauspost/compress/gzhttp"
|
||||
"github.com/klauspost/compress/gzip"
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/logger"
|
||||
"github.com/minio/mux"
|
||||
)
|
||||
|
||||
const (
|
||||
adminPathPrefix = minioReservedBucketPath + "/admin"
|
||||
adminAPIVersion = madmin.AdminAPIVersion
|
||||
adminAPIVersionPrefix = SlashSeparator + adminAPIVersion
|
||||
adminAPISiteReplicationDevNull = "/site-replication/devnull"
|
||||
adminAPISiteReplicationNetPerf = "/site-replication/netperf"
|
||||
adminAPIClientDevNull = "/speedtest/client/devnull"
|
||||
adminAPIClientDevExtraTime = "/speedtest/client/devnull/extratime"
|
||||
)
|
||||
|
||||
var gzipHandler = func() func(http.Handler) http.HandlerFunc {
|
||||
gz, err := gzhttp.NewWrapper(gzhttp.MinSize(1000), gzhttp.CompressionLevel(gzip.BestSpeed))
|
||||
if err != nil {
|
||||
// Static params, so this is very unlikely.
|
||||
logger.Fatal(err, "Unable to initialize server")
|
||||
}
|
||||
return gz
|
||||
}()
|
||||
|
||||
// Set of handler options as bit flags
|
||||
type hFlag uint8
|
||||
|
||||
const (
|
||||
// this flag disables gzip compression of responses
|
||||
noGZFlag = 1 << iota
|
||||
|
||||
// this flag enables tracing body and headers instead of just headers
|
||||
traceAllFlag
|
||||
|
||||
// pass this flag to skip checking if object layer is available
|
||||
noObjLayerFlag
|
||||
)
|
||||
|
||||
// Has checks if the given flag is enabled in `h`.
|
||||
func (h hFlag) Has(flag hFlag) bool {
|
||||
// Use bitwise-AND and check if the result is non-zero.
|
||||
return h&flag != 0
|
||||
}
|
||||
|
||||
// adminMiddleware performs some common admin handler functionality for all
|
||||
// handlers:
|
||||
//
|
||||
// - updates request context with `logger.ReqInfo` and api name based on the
|
||||
// name of the function handler passed (this handler must be a method of
|
||||
// `adminAPIHandlers`).
|
||||
//
|
||||
// - sets up call to send AuditLog
|
||||
//
|
||||
// While this is a middleware function (i.e. it takes a handler function and
|
||||
// returns one), due to flags being passed based on required conditions, it is
|
||||
// done per-"handler function registration" in the router.
|
||||
//
|
||||
// The passed in handler function must be a method of `adminAPIHandlers` for the
|
||||
// name displayed in logs and trace to be accurate. The name is extracted via
|
||||
// reflection.
|
||||
//
|
||||
// When no flags are passed, gzip compression, http tracing of headers and
|
||||
// checking of object layer availability are all enabled. Use flags to modify
|
||||
// this behavior.
|
||||
func adminMiddleware(f http.HandlerFunc, flags ...hFlag) http.HandlerFunc {
|
||||
// Collect all flags with bitwise-OR and assign operator
|
||||
var handlerFlags hFlag
|
||||
for _, flag := range flags {
|
||||
handlerFlags |= flag
|
||||
}
|
||||
|
||||
// Get name of the handler using reflection.
|
||||
handlerName := getHandlerName(f, "adminAPIHandlers")
|
||||
|
||||
var handler http.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
|
||||
// Update request context with `logger.ReqInfo`.
|
||||
r = r.WithContext(newContext(r, w, handlerName))
|
||||
|
||||
defer logger.AuditLog(r.Context(), w, r, mustGetClaimsFromToken(r))
|
||||
|
||||
// Check if object layer is available, if not return error early.
|
||||
if !handlerFlags.Has(noObjLayerFlag) {
|
||||
objectAPI := newObjectLayerFn()
|
||||
if objectAPI == nil || globalNotificationSys == nil {
|
||||
writeErrorResponseJSON(r.Context(), w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Apply http tracing "middleware" based on presence of flag.
|
||||
var f2 http.HandlerFunc
|
||||
if handlerFlags.Has(traceAllFlag) {
|
||||
f2 = httpTraceAll(f)
|
||||
} else {
|
||||
f2 = httpTraceHdrs(f)
|
||||
}
|
||||
|
||||
// call the final handler
|
||||
f2(w, r)
|
||||
}
|
||||
|
||||
// Enable compression of responses based on presence of flag.
|
||||
if !handlerFlags.Has(noGZFlag) {
|
||||
handler = gzipHandler(handler)
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
||||
|
||||
// adminAPIHandlers provides HTTP handlers for MinIO admin API.
|
||||
type adminAPIHandlers struct{}
|
||||
|
||||
// registerAdminRouter - Add handler functions for each service REST API routes.
|
||||
func registerAdminRouter(router *mux.Router, enableConfigOps bool) {
|
||||
adminAPI := adminAPIHandlers{}
|
||||
// Admin router
|
||||
adminRouter := router.PathPrefix(adminPathPrefix).Subrouter()
|
||||
|
||||
adminVersions := []string{
|
||||
adminAPIVersionPrefix,
|
||||
}
|
||||
|
||||
for _, adminVersion := range adminVersions {
|
||||
// Restart and stop MinIO service type=2
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/service").HandlerFunc(adminMiddleware(adminAPI.ServiceV2Handler, traceAllFlag)).Queries("action", "{action:.*}", "type", "2")
|
||||
|
||||
// Deprecated: Restart and stop MinIO service.
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/service").HandlerFunc(adminMiddleware(adminAPI.ServiceHandler, traceAllFlag)).Queries("action", "{action:.*}")
|
||||
|
||||
// Update all MinIO servers type=2
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/update").HandlerFunc(adminMiddleware(adminAPI.ServerUpdateV2Handler, traceAllFlag)).Queries("updateURL", "{updateURL:.*}", "type", "2")
|
||||
|
||||
// Deprecated: Update MinIO servers.
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/update").HandlerFunc(adminMiddleware(adminAPI.ServerUpdateHandler, traceAllFlag)).Queries("updateURL", "{updateURL:.*}")
|
||||
|
||||
// Info operations
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/info").HandlerFunc(adminMiddleware(adminAPI.ServerInfoHandler, traceAllFlag, noObjLayerFlag))
|
||||
adminRouter.Methods(http.MethodGet, http.MethodPost).Path(adminVersion + "/inspect-data").HandlerFunc(adminMiddleware(adminAPI.InspectDataHandler, noGZFlag, traceHdrsS3HFlag))
|
||||
|
||||
// StorageInfo operations
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/storageinfo").HandlerFunc(adminMiddleware(adminAPI.StorageInfoHandler, traceAllFlag))
|
||||
// DataUsageInfo operations
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/datausageinfo").HandlerFunc(adminMiddleware(adminAPI.DataUsageInfoHandler, traceAllFlag))
|
||||
// Metrics operation
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/metrics").HandlerFunc(adminMiddleware(adminAPI.MetricsHandler, traceHdrsS3HFlag))
|
||||
|
||||
if globalIsDistErasure || globalIsErasure {
|
||||
// Heal operations
|
||||
|
||||
// Heal processing endpoint.
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/").HandlerFunc(adminMiddleware(adminAPI.HealHandler, traceAllFlag))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/{bucket}").HandlerFunc(adminMiddleware(adminAPI.HealHandler, traceAllFlag))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/heal/{bucket}/{prefix:.*}").HandlerFunc(adminMiddleware(adminAPI.HealHandler, traceAllFlag))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/background-heal/status").HandlerFunc(adminMiddleware(adminAPI.BackgroundHealStatusHandler, traceAllFlag))
|
||||
|
||||
// Pool operations
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/pools/list").HandlerFunc(adminMiddleware(adminAPI.ListPools, traceAllFlag))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/pools/status").HandlerFunc(adminMiddleware(adminAPI.StatusPool, traceAllFlag)).Queries("pool", "{pool:.*}")
|
||||
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/pools/decommission").HandlerFunc(adminMiddleware(adminAPI.StartDecommission, traceAllFlag)).Queries("pool", "{pool:.*}")
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/pools/cancel").HandlerFunc(adminMiddleware(adminAPI.CancelDecommission, traceAllFlag)).Queries("pool", "{pool:.*}")
|
||||
|
||||
// Rebalance operations
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/rebalance/start").HandlerFunc(adminMiddleware(adminAPI.RebalanceStart, traceAllFlag))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/rebalance/status").HandlerFunc(adminMiddleware(adminAPI.RebalanceStatus, traceAllFlag))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/rebalance/stop").HandlerFunc(adminMiddleware(adminAPI.RebalanceStop, traceAllFlag))
|
||||
}
|
||||
|
||||
// Profiling operations - deprecated API
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/profiling/start").HandlerFunc(adminMiddleware(adminAPI.StartProfilingHandler, traceAllFlag, noObjLayerFlag)).
|
||||
Queries("profilerType", "{profilerType:.*}")
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/profiling/download").HandlerFunc(adminMiddleware(adminAPI.DownloadProfilingHandler, traceHdrsS3HFlag, noObjLayerFlag))
|
||||
// Profiling operations
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/profile").HandlerFunc(adminMiddleware(adminAPI.ProfileHandler, traceHdrsS3HFlag, noObjLayerFlag))
|
||||
|
||||
// Config KV operations.
|
||||
if enableConfigOps {
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-config-kv").HandlerFunc(adminMiddleware(adminAPI.GetConfigKVHandler)).Queries("key", "{key:.*}")
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/set-config-kv").HandlerFunc(adminMiddleware(adminAPI.SetConfigKVHandler))
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/del-config-kv").HandlerFunc(adminMiddleware(adminAPI.DelConfigKVHandler))
|
||||
}
|
||||
|
||||
// Enable config help in all modes.
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/help-config-kv").HandlerFunc(adminMiddleware(adminAPI.HelpConfigKVHandler, traceAllFlag)).Queries("subSys", "{subSys:.*}", "key", "{key:.*}")
|
||||
|
||||
// Config KV history operations.
|
||||
if enableConfigOps {
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-config-history-kv").HandlerFunc(adminMiddleware(adminAPI.ListConfigHistoryKVHandler, traceAllFlag)).Queries("count", "{count:[0-9]+}")
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/clear-config-history-kv").HandlerFunc(adminMiddleware(adminAPI.ClearConfigHistoryKVHandler)).Queries("restoreId", "{restoreId:.*}")
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/restore-config-history-kv").HandlerFunc(adminMiddleware(adminAPI.RestoreConfigHistoryKVHandler)).Queries("restoreId", "{restoreId:.*}")
|
||||
}
|
||||
|
||||
// Config import/export bulk operations
|
||||
if enableConfigOps {
|
||||
// Get config
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/config").HandlerFunc(adminMiddleware(adminAPI.GetConfigHandler))
|
||||
// Set config
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/config").HandlerFunc(adminMiddleware(adminAPI.SetConfigHandler))
|
||||
}
|
||||
|
||||
// -- IAM APIs --
|
||||
|
||||
// Add policy IAM
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-canned-policy").HandlerFunc(adminMiddleware(adminAPI.AddCannedPolicy, traceAllFlag)).Queries("name", "{name:.*}")
|
||||
|
||||
// Add user IAM
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/accountinfo").HandlerFunc(adminMiddleware(adminAPI.AccountInfoHandler, traceAllFlag))
|
||||
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/add-user").HandlerFunc(adminMiddleware(adminAPI.AddUser)).Queries("accessKey", "{accessKey:.*}")
|
||||
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-user-status").HandlerFunc(adminMiddleware(adminAPI.SetUserStatus)).Queries("accessKey", "{accessKey:.*}").Queries("status", "{status:.*}")
|
||||
|
||||
// Service accounts ops
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/add-service-account").HandlerFunc(adminMiddleware(adminAPI.AddServiceAccount))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/update-service-account").HandlerFunc(adminMiddleware(adminAPI.UpdateServiceAccount)).Queries("accessKey", "{accessKey:.*}")
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-service-account").HandlerFunc(adminMiddleware(adminAPI.InfoServiceAccount)).Queries("accessKey", "{accessKey:.*}")
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-service-accounts").HandlerFunc(adminMiddleware(adminAPI.ListServiceAccounts))
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/delete-service-account").HandlerFunc(adminMiddleware(adminAPI.DeleteServiceAccount)).Queries("accessKey", "{accessKey:.*}")
|
||||
|
||||
// STS accounts ops
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/temporary-account-info").HandlerFunc(adminMiddleware(adminAPI.TemporaryAccountInfo)).Queries("accessKey", "{accessKey:.*}")
|
||||
|
||||
// Access key (service account/STS) operations
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-access-keys-bulk").HandlerFunc(adminMiddleware(adminAPI.ListAccessKeysBulk)).Queries("listType", "{listType:.*}")
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-access-key").HandlerFunc(adminMiddleware(adminAPI.InfoAccessKey)).Queries("accessKey", "{accessKey:.*}")
|
||||
|
||||
// Info policy IAM latest
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/info-canned-policy").HandlerFunc(adminMiddleware(adminAPI.InfoCannedPolicy)).Queries("name", "{name:.*}")
|
||||
// List policies latest
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-canned-policies").HandlerFunc(adminMiddleware(adminAPI.ListBucketPolicies)).Queries("bucket", "{bucket:.*}")
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-canned-policies").HandlerFunc(adminMiddleware(adminAPI.ListCannedPolicies))
|
||||
|
||||
// Builtin IAM policy associations
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp/builtin/policy-entities").HandlerFunc(adminMiddleware(adminAPI.ListPolicyMappingEntities))
|
||||
|
||||
// Remove policy IAM
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-canned-policy").HandlerFunc(adminMiddleware(adminAPI.RemoveCannedPolicy)).Queries("name", "{name:.*}")
|
||||
|
||||
// Set user or group policy
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-user-or-group-policy").
|
||||
HandlerFunc(adminMiddleware(adminAPI.SetPolicyForUserOrGroup)).
|
||||
Queries("policyName", "{policyName:.*}", "userOrGroup", "{userOrGroup:.*}", "isGroup", "{isGroup:true|false}")
|
||||
|
||||
// Attach/Detach policies to/from user or group
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp/builtin/policy/{operation}").HandlerFunc(adminMiddleware(adminAPI.AttachDetachPolicyBuiltin))
|
||||
|
||||
// Remove user IAM
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-user").HandlerFunc(adminMiddleware(adminAPI.RemoveUser)).Queries("accessKey", "{accessKey:.*}")
|
||||
|
||||
// List users
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-users").HandlerFunc(adminMiddleware(adminAPI.ListBucketUsers)).Queries("bucket", "{bucket:.*}")
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-users").HandlerFunc(adminMiddleware(adminAPI.ListUsers))
|
||||
|
||||
// User info
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/user-info").HandlerFunc(adminMiddleware(adminAPI.GetUserInfo)).Queries("accessKey", "{accessKey:.*}")
|
||||
// Add/Remove members from group
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/update-group-members").HandlerFunc(adminMiddleware(adminAPI.UpdateGroupMembers))
|
||||
|
||||
// Get Group
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/group").HandlerFunc(adminMiddleware(adminAPI.GetGroup)).Queries("group", "{group:.*}")
|
||||
|
||||
// List Groups
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/groups").HandlerFunc(adminMiddleware(adminAPI.ListGroups))
|
||||
|
||||
// Set Group Status
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-group-status").HandlerFunc(adminMiddleware(adminAPI.SetGroupStatus)).Queries("group", "{group:.*}").Queries("status", "{status:.*}")
|
||||
|
||||
// Export IAM info to zipped file
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/export-iam").HandlerFunc(adminMiddleware(adminAPI.ExportIAM, noGZFlag))
|
||||
|
||||
// Import IAM info
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/import-iam").HandlerFunc(adminMiddleware(adminAPI.ImportIAM, noGZFlag))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/import-iam-v2").HandlerFunc(adminMiddleware(adminAPI.ImportIAMV2, noGZFlag))
|
||||
|
||||
// Identity Provider configuration APIs
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/idp-config/{type}/{name}").HandlerFunc(adminMiddleware(adminAPI.AddIdentityProviderCfg))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp-config/{type}/{name}").HandlerFunc(adminMiddleware(adminAPI.UpdateIdentityProviderCfg))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp-config/{type}").HandlerFunc(adminMiddleware(adminAPI.ListIdentityProviderCfg))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp-config/{type}/{name}").HandlerFunc(adminMiddleware(adminAPI.GetIdentityProviderCfg))
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/idp-config/{type}/{name}").HandlerFunc(adminMiddleware(adminAPI.DeleteIdentityProviderCfg))
|
||||
|
||||
// LDAP specific service accounts ops
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/idp/ldap/add-service-account").HandlerFunc(adminMiddleware(adminAPI.AddServiceAccountLDAP))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/idp/ldap/list-access-keys").
|
||||
HandlerFunc(adminMiddleware(adminAPI.ListAccessKeysLDAP)).Queries("userDN", "{userDN:.*}", "listType", "{listType:.*}")
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/idp/ldap/list-access-keys-bulk").
|
||||
HandlerFunc(adminMiddleware(adminAPI.ListAccessKeysLDAPBulk)).Queries("listType", "{listType:.*}")
|
||||
|
||||
// LDAP IAM operations
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/idp/ldap/policy-entities").HandlerFunc(adminMiddleware(adminAPI.ListLDAPPolicyMappingEntities))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/idp/ldap/policy/{operation}").HandlerFunc(adminMiddleware(adminAPI.AttachDetachPolicyLDAP))
|
||||
|
||||
// OpenID specific service accounts ops
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/idp/openid/list-access-keys-bulk").
|
||||
HandlerFunc(adminMiddleware(adminAPI.ListAccessKeysOpenIDBulk)).Queries("listType", "{listType:.*}")
|
||||
|
||||
// -- END IAM APIs --
|
||||
|
||||
// GetBucketQuotaConfig
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/get-bucket-quota").HandlerFunc(
|
||||
adminMiddleware(adminAPI.GetBucketQuotaConfigHandler)).Queries("bucket", "{bucket:.*}")
|
||||
// PutBucketQuotaConfig
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-bucket-quota").HandlerFunc(
|
||||
adminMiddleware(adminAPI.PutBucketQuotaConfigHandler)).Queries("bucket", "{bucket:.*}")
|
||||
|
||||
// Bucket replication operations
|
||||
// GetBucketTargetHandler
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/list-remote-targets").HandlerFunc(
|
||||
adminMiddleware(adminAPI.ListRemoteTargetsHandler)).Queries("bucket", "{bucket:.*}", "type", "{type:.*}")
|
||||
// SetRemoteTargetHandler
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/set-remote-target").HandlerFunc(
|
||||
adminMiddleware(adminAPI.SetRemoteTargetHandler)).Queries("bucket", "{bucket:.*}")
|
||||
// RemoveRemoteTargetHandler
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion+"/remove-remote-target").HandlerFunc(
|
||||
adminMiddleware(adminAPI.RemoveRemoteTargetHandler)).Queries("bucket", "{bucket:.*}", "arn", "{arn:.*}")
|
||||
// ReplicationDiff - MinIO extension API
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/replication/diff").HandlerFunc(
|
||||
adminMiddleware(adminAPI.ReplicationDiffHandler)).Queries("bucket", "{bucket:.*}")
|
||||
// ReplicationMRFHandler - MinIO extension API
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion+"/replication/mrf").HandlerFunc(
|
||||
adminMiddleware(adminAPI.ReplicationMRFHandler)).Queries("bucket", "{bucket:.*}")
|
||||
|
||||
// Batch job operations
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/start-job").HandlerFunc(
|
||||
adminMiddleware(adminAPI.StartBatchJob))
|
||||
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/list-jobs").HandlerFunc(
|
||||
adminMiddleware(adminAPI.ListBatchJobs))
|
||||
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/status-job").HandlerFunc(
|
||||
adminMiddleware(adminAPI.BatchJobStatus))
|
||||
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/describe-job").HandlerFunc(
|
||||
adminMiddleware(adminAPI.DescribeBatchJob))
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/cancel-job").HandlerFunc(
|
||||
adminMiddleware(adminAPI.CancelBatchJob))
|
||||
|
||||
// Bucket migration operations
|
||||
// ExportBucketMetaHandler
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/export-bucket-metadata").HandlerFunc(
|
||||
adminMiddleware(adminAPI.ExportBucketMetadataHandler))
|
||||
// ImportBucketMetaHandler
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/import-bucket-metadata").HandlerFunc(
|
||||
adminMiddleware(adminAPI.ImportBucketMetadataHandler))
|
||||
|
||||
// Remote Tier management operations
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/tier").HandlerFunc(adminMiddleware(adminAPI.AddTierHandler))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/tier/{tier}").HandlerFunc(adminMiddleware(adminAPI.EditTierHandler))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier").HandlerFunc(adminMiddleware(adminAPI.ListTierHandler))
|
||||
adminRouter.Methods(http.MethodDelete).Path(adminVersion + "/tier/{tier}").HandlerFunc(adminMiddleware(adminAPI.RemoveTierHandler))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier/{tier}").HandlerFunc(adminMiddleware(adminAPI.VerifyTierHandler))
|
||||
// Tier stats
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/tier-stats").HandlerFunc(adminMiddleware(adminAPI.TierStatsHandler))
|
||||
|
||||
// Cluster Replication APIs
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/add").HandlerFunc(adminMiddleware(adminAPI.SiteReplicationAdd))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/remove").HandlerFunc(adminMiddleware(adminAPI.SiteReplicationRemove))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/info").HandlerFunc(adminMiddleware(adminAPI.SiteReplicationInfo))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/metainfo").HandlerFunc(adminMiddleware(adminAPI.SiteReplicationMetaInfo))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/status").HandlerFunc(adminMiddleware(adminAPI.SiteReplicationStatus))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + adminAPISiteReplicationDevNull).HandlerFunc(adminMiddleware(adminAPI.SiteReplicationDevNull, noObjLayerFlag))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + adminAPISiteReplicationNetPerf).HandlerFunc(adminMiddleware(adminAPI.SiteReplicationNetPerf, noObjLayerFlag))
|
||||
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/join").HandlerFunc(adminMiddleware(adminAPI.SRPeerJoin))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/site-replication/peer/bucket-ops").HandlerFunc(adminMiddleware(adminAPI.SRPeerBucketOps)).Queries("bucket", "{bucket:.*}").Queries("operation", "{operation:.*}")
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/iam-item").HandlerFunc(adminMiddleware(adminAPI.SRPeerReplicateIAMItem))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/bucket-meta").HandlerFunc(adminMiddleware(adminAPI.SRPeerReplicateBucketItem))
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/site-replication/peer/idp-settings").HandlerFunc(adminMiddleware(adminAPI.SRPeerGetIDPSettings))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/edit").HandlerFunc(adminMiddleware(adminAPI.SiteReplicationEdit))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/edit").HandlerFunc(adminMiddleware(adminAPI.SRPeerEdit))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/peer/remove").HandlerFunc(adminMiddleware(adminAPI.SRPeerRemove))
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion+"/site-replication/resync/op").HandlerFunc(adminMiddleware(adminAPI.SiteReplicationResyncOp)).Queries("operation", "{operation:.*}")
|
||||
adminRouter.Methods(http.MethodPut).Path(adminVersion + "/site-replication/state/edit").HandlerFunc(adminMiddleware(adminAPI.SRStateEdit))
|
||||
|
||||
if globalIsDistErasure {
|
||||
// Top locks
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/top/locks").HandlerFunc(adminMiddleware(adminAPI.TopLocksHandler))
|
||||
// Force unlocks paths
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/force-unlock").
|
||||
Queries("paths", "{paths:.*}").HandlerFunc(adminMiddleware(adminAPI.ForceUnlockHandler))
|
||||
}
|
||||
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest").HandlerFunc(adminMiddleware(adminAPI.ObjectSpeedTestHandler, noGZFlag))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/object").HandlerFunc(adminMiddleware(adminAPI.ObjectSpeedTestHandler, noGZFlag))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/drive").HandlerFunc(adminMiddleware(adminAPI.DriveSpeedtestHandler, noGZFlag))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/net").HandlerFunc(adminMiddleware(adminAPI.NetperfHandler, noGZFlag))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/speedtest/site").HandlerFunc(adminMiddleware(adminAPI.SitePerfHandler, noGZFlag))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + adminAPIClientDevNull).HandlerFunc(adminMiddleware(adminAPI.ClientDevNull, noGZFlag))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + adminAPIClientDevExtraTime).HandlerFunc(adminMiddleware(adminAPI.ClientDevNullExtraTime, noGZFlag))
|
||||
|
||||
// HTTP Trace
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/trace").HandlerFunc(adminMiddleware(adminAPI.TraceHandler, noObjLayerFlag))
|
||||
|
||||
// Console Logs
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/log").HandlerFunc(adminMiddleware(adminAPI.ConsoleLogHandler, traceAllFlag))
|
||||
|
||||
// -- KMS APIs --
|
||||
//
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/kms/status").HandlerFunc(adminMiddleware(adminAPI.KMSStatusHandler, traceAllFlag))
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion+"/kms/key/create").HandlerFunc(adminMiddleware(adminAPI.KMSCreateKeyHandler, traceAllFlag)).Queries("key-id", "{key-id:.*}")
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/kms/key/status").HandlerFunc(adminMiddleware(adminAPI.KMSKeyStatusHandler, traceAllFlag))
|
||||
|
||||
// Keep obdinfo for backward compatibility with mc
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/obdinfo").
|
||||
HandlerFunc(adminMiddleware(adminAPI.HealthInfoHandler))
|
||||
// -- Health API --
|
||||
adminRouter.Methods(http.MethodGet).Path(adminVersion + "/healthinfo").
|
||||
HandlerFunc(adminMiddleware(adminAPI.HealthInfoHandler))
|
||||
|
||||
// STS Revocation
|
||||
adminRouter.Methods(http.MethodPost).Path(adminVersion + "/revoke-tokens/{userProvider}").HandlerFunc(adminMiddleware(adminAPI.RevokeTokens))
|
||||
}
|
||||
|
||||
// If none of the routes match add default error handler routes
|
||||
adminRouter.NotFoundHandler = httpTraceAll(errorResponseHandler)
|
||||
adminRouter.MethodNotAllowedHandler = httpTraceAll(methodNotAllowedHandler("Admin"))
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
// Copyright (c) 2015-2024 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/minio/madmin-go/v3"
|
||||
"github.com/minio/minio/internal/config"
|
||||
"github.com/minio/minio/internal/kms"
|
||||
xnet "github.com/pgsty/silo-pkg/v3/net"
|
||||
)
|
||||
|
||||
// getLocalServerProperty - returns madmin.ServerProperties for only the
|
||||
// local endpoints from given list of endpoints
|
||||
func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Request, metrics bool) madmin.ServerProperties {
|
||||
addr := globalLocalNodeName
|
||||
if r != nil {
|
||||
addr = r.Host
|
||||
}
|
||||
if globalIsDistErasure {
|
||||
addr = globalLocalNodeName
|
||||
}
|
||||
poolNumbers := make(map[int]struct{})
|
||||
network := make(map[string]string)
|
||||
for _, ep := range endpointServerPools {
|
||||
for _, endpoint := range ep.Endpoints {
|
||||
if endpoint.IsLocal {
|
||||
poolNumbers[endpoint.PoolIdx+1] = struct{}{}
|
||||
}
|
||||
nodeName := endpoint.Host
|
||||
if nodeName == "" {
|
||||
nodeName = addr
|
||||
}
|
||||
if endpoint.IsLocal {
|
||||
// Only proceed for local endpoints
|
||||
network[nodeName] = string(madmin.ItemOnline)
|
||||
continue
|
||||
}
|
||||
_, present := network[nodeName]
|
||||
if !present {
|
||||
if err := isServerResolvable(endpoint, 5*time.Second); err == nil {
|
||||
network[nodeName] = string(madmin.ItemOnline)
|
||||
} else {
|
||||
if xnet.IsNetworkOrHostDown(err, false) {
|
||||
network[nodeName] = string(madmin.ItemOffline)
|
||||
} else if xnet.IsNetworkOrHostDown(err, true) {
|
||||
network[nodeName] = "connection attempt timedout"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var memstats runtime.MemStats
|
||||
runtime.ReadMemStats(&memstats)
|
||||
|
||||
gcStats := debug.GCStats{
|
||||
// If stats.PauseQuantiles is non-empty, ReadGCStats fills
|
||||
// it with quantiles summarizing the distribution of pause time.
|
||||
// For example, if len(stats.PauseQuantiles) is 5, it will be
|
||||
// filled with the minimum, 25%, 50%, 75%, and maximum pause times.
|
||||
PauseQuantiles: make([]time.Duration, 5),
|
||||
}
|
||||
debug.ReadGCStats(&gcStats)
|
||||
// Truncate GC stats to max 5 entries.
|
||||
if len(gcStats.PauseEnd) > 5 {
|
||||
gcStats.PauseEnd = gcStats.PauseEnd[len(gcStats.PauseEnd)-5:]
|
||||
}
|
||||
if len(gcStats.Pause) > 5 {
|
||||
gcStats.Pause = gcStats.Pause[len(gcStats.Pause)-5:]
|
||||
}
|
||||
|
||||
props := madmin.ServerProperties{
|
||||
Endpoint: addr,
|
||||
Uptime: UTCNow().Unix() - globalBootTime.Unix(),
|
||||
Version: Version,
|
||||
CommitID: CommitID,
|
||||
Network: network,
|
||||
MemStats: madmin.MemStats{
|
||||
Alloc: memstats.Alloc,
|
||||
TotalAlloc: memstats.TotalAlloc,
|
||||
Mallocs: memstats.Mallocs,
|
||||
Frees: memstats.Frees,
|
||||
HeapAlloc: memstats.HeapAlloc,
|
||||
},
|
||||
GoMaxProcs: runtime.GOMAXPROCS(0),
|
||||
NumCPU: runtime.NumCPU(),
|
||||
RuntimeVersion: runtime.Version(),
|
||||
GCStats: &madmin.GCStats{
|
||||
LastGC: gcStats.LastGC,
|
||||
NumGC: gcStats.NumGC,
|
||||
PauseTotal: gcStats.PauseTotal,
|
||||
Pause: gcStats.Pause,
|
||||
PauseEnd: gcStats.PauseEnd,
|
||||
},
|
||||
MinioEnvVars: make(map[string]string, 10),
|
||||
}
|
||||
|
||||
for poolNumber := range poolNumbers {
|
||||
props.PoolNumbers = append(props.PoolNumbers, poolNumber)
|
||||
}
|
||||
sort.Ints(props.PoolNumbers)
|
||||
props.PoolNumber = func() int {
|
||||
if len(props.PoolNumbers) == 1 {
|
||||
return props.PoolNumbers[0]
|
||||
}
|
||||
return math.MaxInt // this indicates that its unset.
|
||||
}()
|
||||
|
||||
sensitive := map[string]struct{}{
|
||||
config.EnvAccessKey: {},
|
||||
config.EnvSecretKey: {},
|
||||
config.EnvRootUser: {},
|
||||
config.EnvRootPassword: {},
|
||||
config.EnvMinIOSubnetAPIKey: {},
|
||||
kms.EnvKMSSecretKey: {},
|
||||
}
|
||||
for _, v := range os.Environ() {
|
||||
if !strings.HasPrefix(v, "MINIO") && !strings.HasPrefix(v, "_MINIO") {
|
||||
continue
|
||||
}
|
||||
split := strings.SplitN(v, "=", 2)
|
||||
key := split[0]
|
||||
value := ""
|
||||
if len(split) > 1 {
|
||||
value = split[1]
|
||||
}
|
||||
|
||||
// Do not send sensitive creds.
|
||||
if _, ok := sensitive[key]; ok || strings.Contains(strings.ToLower(key), "password") || strings.HasSuffix(strings.ToLower(key), "key") {
|
||||
props.MinioEnvVars[key] = "*** EXISTS, REDACTED ***"
|
||||
continue
|
||||
}
|
||||
props.MinioEnvVars[key] = value
|
||||
}
|
||||
|
||||
objLayer := newObjectLayerFn()
|
||||
if objLayer != nil {
|
||||
storageInfo := objLayer.LocalStorageInfo(GlobalContext, metrics)
|
||||
props.State = string(madmin.ItemOnline)
|
||||
props.Disks = storageInfo.Disks
|
||||
} else {
|
||||
props.State = string(madmin.ItemInitializing)
|
||||
props.Disks = getOfflineDisks("", globalEndpoints)
|
||||
}
|
||||
|
||||
return props
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DeletedObject objects deleted
|
||||
type DeletedObject struct {
|
||||
DeleteMarker bool `xml:"DeleteMarker,omitempty"`
|
||||
DeleteMarkerVersionID string `xml:"DeleteMarkerVersionId,omitempty"`
|
||||
ObjectName string `xml:"Key,omitempty"`
|
||||
VersionID string `xml:"VersionId,omitempty"`
|
||||
// MTime of DeleteMarker on source that needs to be propagated to replica
|
||||
DeleteMarkerMTime DeleteMarkerMTime `xml:"-"`
|
||||
// MinIO extensions to support delete marker replication
|
||||
ReplicationState ReplicationState `xml:"-"`
|
||||
|
||||
found bool // the object was found during deletion
|
||||
}
|
||||
|
||||
// DeleteMarkerMTime is an embedded type containing time.Time for XML marshal
|
||||
type DeleteMarkerMTime struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
// MarshalXML encodes expiration date if it is non-zero and encodes
|
||||
// empty string otherwise
|
||||
func (t DeleteMarkerMTime) MarshalXML(e *xml.Encoder, startElement xml.StartElement) error {
|
||||
if t.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return e.EncodeElement(t.Format(time.RFC3339), startElement)
|
||||
}
|
||||
|
||||
// ObjectV object version key/versionId
|
||||
type ObjectV struct {
|
||||
ObjectName string `xml:"Key"`
|
||||
VersionID string `xml:"VersionId"`
|
||||
}
|
||||
|
||||
// ObjectToDelete carries key name for the object to delete.
|
||||
type ObjectToDelete struct {
|
||||
ObjectV
|
||||
// Replication status of DeleteMarker
|
||||
DeleteMarkerReplicationStatus string `xml:"DeleteMarkerReplicationStatus"`
|
||||
// Status of versioned delete (of object or DeleteMarker)
|
||||
VersionPurgeStatus VersionPurgeStatusType `xml:"VersionPurgeStatus"`
|
||||
// VersionPurgeStatuses holds the internal
|
||||
VersionPurgeStatuses string `xml:"VersionPurgeStatuses"`
|
||||
// ReplicateDecisionStr stringified representation of replication decision
|
||||
ReplicateDecisionStr string `xml:"-"`
|
||||
}
|
||||
|
||||
// createBucketLocationConfiguration container for bucket configuration request from client.
|
||||
// Used for parsing the location from the request body for Makebucket.
|
||||
type createBucketLocationConfiguration struct {
|
||||
XMLName xml.Name `xml:"CreateBucketConfiguration" json:"-"`
|
||||
Location string `xml:"LocationConstraint"`
|
||||
}
|
||||
|
||||
// DeleteObjectsRequest - xml carrying the object key names which needs to be deleted.
|
||||
type DeleteObjectsRequest struct {
|
||||
// Element to enable quiet mode for the request
|
||||
Quiet bool
|
||||
// List of objects to be deleted
|
||||
Objects []ObjectToDelete `xml:"Object"`
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user