Skip to content

Instantly share code, notes, and snippets.

@Hunsin
Created July 3, 2025 16:38
Show Gist options
  • Save Hunsin/00b285c3af83723df88b25055eb1fa60 to your computer and use it in GitHub Desktop.
Save Hunsin/00b285c3af83723df88b25055eb1fa60 to your computer and use it in GitHub Desktop.
KCD Taipei 2025 CLBO Contest: MinIO

KCD Taipei 2025 CLBO 挑戰賽

說明

ClashApp 是一個 Golang 應用服務,提供以下三個 API

  • GET /health: 健康檢查
  • GET /ready: 服務是否就緒
  • POST /upload: 上傳檔案,並存到 MinIO

這個服務需要四個環境變數:

  • MINIO_ENDPOINT
  • MINIO_ACCESS_KEY
  • MINIO_SECRET_KEY
  • MINIO_BUCKET_NAME

Dockerfile 已經就緒,參賽者可以直接建立你專屬的 Image。

噢不!

不幸地,健忘的小精靈沒有更新 deployment.yaml,啟動這個 K8s 叢集遇到好多錯誤。而且,她也忘記附上 MinIO 的配置檔,讓同事好著急⋯⋯聰明的你,可以幫我們建立 MinIO 服務,並讓 ClashApp 順利運作起來嗎?

# bad-app-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: clash-app
labels:
app: clash-app
spec:
replicas: 1
selector:
matchLabels:
app: clash-app
template:
metadata:
labels:
app: clash-app
spec:
containers:
- name: clash-app
image: your-app-image:latest # please build your image
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: clash-app-config
env:
- name: MINIO_SECRET_KEY
value: "my_secret"
resources:
requests:
memory: "5Mi"
cpu: "10m"
limits:
memory: "10Mi"
cpu: "50m"
livenessProbe:
httpGet:
path: /haelth
port: 8080
initialDelaySeconds: 1
periodSeconds: 3
failureThreshold: 1
readinessProbe:
httpGet:
path: /readyz
port: 8080
initialDelaySeconds: 1
periodSeconds: 3
failureThreshold: 1
FROM golang:1.24.4-alpine3.22
RUN mkdir -p /app
COPY go.mod go.sum main.go /app
WORKDIR /app
RUN go build -o clbo main.go
CMD ["./clbo"]
EXPOSE 8080
module example.com/kcd_clbo
go 1.24.1
require github.com/minio/minio-go/v7 v7.0.93
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/minio/crc64nvme v1.0.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/tinylib/msgp v1.3.0 // indirect
golang.org/x/crypto v0.36.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY=
github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.93 h1:lAB4QJp8Nq3vDMOU0eKgMuyBiEGMNlXQ5Glc8qAxqSU=
github.com/minio/minio-go/v7 v7.0.93/go.mod h1:71t2CqDt3ThzESgZUlU1rBN54mksGGlkLcFgguDnnAc=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
// ClashApp - 簡單的 Golang 應用程式
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"time"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
var (
endpoint string = os.Getenv("MINIO_ENDPOINT")
accessKeyID string = os.Getenv("MINIO_ACCESS_KEY")
secretAccessKey string = os.Getenv("MINIO_SECRET_KEY")
bucketName string = os.Getenv("MINIO_BUCKET_NAME")
minioClient *minio.Client
)
func main() {
log.Println("正在啟動 ClashApp...")
if endpoint == "" || accessKeyID == "" || secretAccessKey == "" || bucketName == "" {
log.Fatalf("錯誤: MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEY, BUCKET_NAME 環境變數必須設定!")
}
// 初始化 MinIO 客戶端 (如果配置錯誤,這裡可能會 panic 或後續操作失敗)
log.Printf("嘗試連接 MinIO: %s\n", endpoint)
var err error
minioClient, err = minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
})
if err != nil {
log.Fatalf("初始化 MinIO 客戶端失敗: %v", err)
}
log.Printf("成功初始化 MinIO 客戶端")
log.Println("檢查 MinIO Bucket...")
ok, err := minioClient.BucketExists(context.Background(), bucketName)
if err != nil {
log.Fatalf("檢查 Bucket '%s' 失敗: %v", bucketName, err)
}
if !ok {
log.Fatalf("Bucket '%s' 不存在!", bucketName)
}
log.Printf("Bucket '%s' 存在。\n", bucketName)
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
// 一個非常基礎的健康檢查,實際應用應更完善
fmt.Fprintf(w, "ok")
})
http.HandleFunc("/ready", func(w http.ResponseWriter, r *http.Request) {
// 模擬準備就緒探針,應檢查 MinIO 連接等
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
_, err := minioClient.BucketExists(ctx, bucketName)
if err != nil {
http.Error(w, "MinIO not ready", http.StatusServiceUnavailable)
return
}
fmt.Fprintf(w, "ready")
})
http.HandleFunc("/upload", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 限制最大上傳大小 (例如 10MB)
r.Body = http.MaxBytesReader(w, r.Body, 10<<20)
err := r.ParseMultipartForm(10 << 20)
if err != nil {
http.Error(w, "檔案太大或格式錯誤", http.StatusBadRequest)
return
}
file, handler, err := r.FormFile("file")
if err != nil {
http.Error(w, "無法取得上傳檔案", http.StatusBadRequest)
return
}
defer file.Close()
objectName := handler.Filename
contentType := handler.Header.Get("Content-Type")
if contentType == "" {
contentType = "application/octet-stream"
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
info, err := minioClient.PutObject(ctx, bucketName, objectName, file, handler.Size, minio.PutObjectOptions{
ContentType: contentType,
})
if err != nil {
log.Printf("上傳到 MinIO 失敗: %v", err)
http.Error(w, "上傳失敗", http.StatusInternalServerError)
return
}
log.Printf("成功上傳: %s (%d bytes)", info.Key, info.Size)
fmt.Fprintf(w, "uploaded: %s\n", info.Key)
})
log.Println("ClashApp 正在監聽 :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatalf("無法啟動 HTTP 伺服器: %v", err)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment