commit 73453ddd107d133411783c5769587b94727980a4 Author: dailz Date: Wed Apr 8 18:28:32 2026 +0800 chore: 初始化 Slurm Go SDK 项目骨架和核心客户端 添加 Go 模块定义、.gitignore、包文档、HTTP Client 核心、Token 认证和错误处理。 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c077bc4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +bin/ +*.exe +.sisyphus/ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..57dee1c --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module slurm-client + +go 1.22 diff --git a/internal/slurm/auth.go b/internal/slurm/auth.go new file mode 100644 index 0000000..2161f9b --- /dev/null +++ b/internal/slurm/auth.go @@ -0,0 +1,39 @@ +package slurm + +import "net/http" + +// TokenAuthTransport implements http.RoundTripper and injects +// X-SLURM-USER-NAME and X-SLURM-USER-TOKEN headers into every request. +type TokenAuthTransport struct { + UserName string + Token string + + // Base is the underlying RoundTripper. If nil, http.DefaultTransport is used. + Base http.RoundTripper +} + +// RoundTrip executes a single HTTP request, adding Slurm authentication headers. +func (t *TokenAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { + req2 := cloneRequest(req) + req2.Header.Set("X-SLURM-USER-NAME", t.UserName) + req2.Header.Set("X-SLURM-USER-TOKEN", t.Token) + return t.transport().RoundTrip(req2) +} + +// Client returns a new http.Client that uses this TokenAuthTransport. +func (t *TokenAuthTransport) Client() *http.Client { + return &http.Client{Transport: t} +} + +func (t *TokenAuthTransport) transport() http.RoundTripper { + if t.Base != nil { + return t.Base + } + return http.DefaultTransport +} + +// cloneRequest creates a shallow copy of the request with a deep copy of the headers. +func cloneRequest(req *http.Request) *http.Request { + r := req.Clone(req.Context()) + return r +} diff --git a/internal/slurm/client.go b/internal/slurm/client.go new file mode 100644 index 0000000..c0025d2 --- /dev/null +++ b/internal/slurm/client.go @@ -0,0 +1,153 @@ +package slurm + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +const ( + // DefaultBaseURL is the default Slurm REST API endpoint. + DefaultBaseURL = "http://localhost:6820/" + // DefaultUserAgent is the default User-Agent header value. + DefaultUserAgent = "slurm-go-sdk" +) + +// Client manages communication with the Slurm REST API. +type Client struct { + client *http.Client + baseURL *url.URL + UserAgent string + + common service + + Jobs *JobsService + Nodes *NodesService + Partitions *PartitionsService + Reservations *ReservationsService + Diag *DiagService + Ping *PingService + Licenses *LicensesService + Reconfigure *ReconfigureService + Shares *SharesService +} + +// service is the base struct for all API services. +type service struct { + client *Client +} + +type JobsService service +type NodesService service +type PartitionsService service +type ReservationsService service +type DiagService service +type PingService service +type LicensesService service +type ReconfigureService service +type SharesService service + +// Response wraps an http.Response and will later hold Meta/Errors/Warnings +// fields parsed from Slurm API responses. +type Response struct { + *http.Response +} + +// NewClient returns a new Slurm API client. If httpClient is nil, +// http.DefaultClient is used. +func NewClient(baseURL string, httpClient *http.Client) (*Client, error) { + if httpClient == nil { + httpClient = http.DefaultClient + } + + parsedURL, err := url.Parse(baseURL) + if err != nil { + return nil, fmt.Errorf("invalid base URL %q: %w", baseURL, err) + } + if !strings.HasSuffix(parsedURL.Path, "/") { + parsedURL.Path += "/" + } + + c := &Client{ + client: httpClient, + baseURL: parsedURL, + UserAgent: DefaultUserAgent, + } + c.common.client = c + + c.Jobs = (*JobsService)(&c.common) + c.Nodes = (*NodesService)(&c.common) + c.Partitions = (*PartitionsService)(&c.common) + c.Reservations = (*ReservationsService)(&c.common) + c.Diag = (*DiagService)(&c.common) + c.Ping = (*PingService)(&c.common) + c.Licenses = (*LicensesService)(&c.common) + c.Reconfigure = (*ReconfigureService)(&c.common) + c.Shares = (*SharesService)(&c.common) + + return c, nil +} + +// NewRequest creates an API request. A relative URL path can be provided in +// urlStr, which will be resolved against the base URL of the Client. +// If body is not nil, it will be JSON-encoded and set as the request body. +func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { + u, err := c.baseURL.Parse(urlStr) + if err != nil { + return nil, fmt.Errorf("invalid relative URL %q: %w", urlStr, err) + } + + var buf io.Reader + if body != nil { + b, err := json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("failed to marshal request body: %w", err) + } + buf = bytes.NewReader(b) + } + + req, err := http.NewRequest(method, u.String(), buf) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + if c.UserAgent != "" { + req.Header.Set("User-Agent", c.UserAgent) + } + + return req, nil +} + +// Do sends an API request and returns the API response. The API response is +// JSON-decoded into the value pointed to by v (if v is not nil). +func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) { + req = req.WithContext(ctx) + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if err := CheckResponse(resp); err != nil { + return nil, err + } + + response := &Response{Response: resp} + + if v != nil { + if err := json.NewDecoder(resp.Body).Decode(v); err != nil { + return response, fmt.Errorf("failed to decode response: %w", err) + } + } + + return response, nil +} diff --git a/internal/slurm/errors.go b/internal/slurm/errors.go new file mode 100644 index 0000000..3e5e537 --- /dev/null +++ b/internal/slurm/errors.go @@ -0,0 +1,38 @@ +package slurm + +import ( + "fmt" + "io" + "net/http" +) + +// ErrorResponse represents an error returned by the Slurm REST API. +type ErrorResponse struct { + Response *http.Response + Message string +} + +func (r *ErrorResponse) Error() string { + return fmt.Sprintf("%v %v: %d %s", + r.Response.Request.Method, r.Response.Request.URL, + r.Response.StatusCode, r.Message) +} + +// CheckResponse checks the API response for errors. It returns nil if the +// response is a 2xx status code. For non-2xx codes, it reads the response +// body and returns an ErrorResponse. +func CheckResponse(r *http.Response) error { + if c := r.StatusCode; c >= 200 && c <= 299 { + return nil + } + + errorResponse := &ErrorResponse{Response: r} + data, err := io.ReadAll(r.Body) + if err != nil || len(data) == 0 { + errorResponse.Message = r.Status + return errorResponse + } + + errorResponse.Message = string(data) + return errorResponse +} diff --git a/internal/slurm/slurm.go b/internal/slurm/slurm.go new file mode 100644 index 0000000..34d2522 --- /dev/null +++ b/internal/slurm/slurm.go @@ -0,0 +1,18 @@ +// Package slurm provides a Go client for the Slurm REST API (v0.0.40). +// +// The client handles authentication via X-SLURM-USER-NAME and X-SLURM-USER-TOKEN +// headers, request/response marshaling, and error handling. +// +// Basic usage: +// +// httpClient := &http.Client{ +// Transport: &slurm.TokenAuthTransport{ +// UserName: "slurm", +// Token: "your-token", +// }, +// } +// client, err := slurm.NewClient("http://localhost:6820", httpClient) +// if err != nil { +// log.Fatal(err) +// } +package slurm