From 74230630c804e949f42aee2219a8453eab03577f Mon Sep 17 00:00:00 2001 From: dailz Date: Wed, 8 Apr 2026 21:34:42 +0800 Subject: [PATCH] feat(slurmdb): add ClustersService Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- internal/slurm/slurmdb_clusters.go | 221 ++++++++++++++++++++++++ internal/slurm/slurmdb_clusters_test.go | 221 ++++++++++++++++++++++++ 2 files changed, 442 insertions(+) create mode 100644 internal/slurm/slurmdb_clusters.go create mode 100644 internal/slurm/slurmdb_clusters_test.go diff --git a/internal/slurm/slurmdb_clusters.go b/internal/slurm/slurmdb_clusters.go new file mode 100644 index 0000000..33eafd9 --- /dev/null +++ b/internal/slurm/slurmdb_clusters.go @@ -0,0 +1,221 @@ +package slurm + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +// Cluster classification constants for GetClustersOptions.Classification +// and DeleteClusterOptions.Classification. +const ( + ClusterClassificationUNCLASSIFIED = "UNCLASSIFIED" + ClusterClassificationCapability = "CAPABILITY" + ClusterClassificationCapacity = "CAPACITY" + ClusterClassificationCapapacity = "CAPAPACITY" +) + +// Cluster flags constants for GetClustersOptions.Flags +// and DeleteClusterOptions.Flags. +const ( + ClusterFlagRegistering = "REGISTERING" + ClusterFlagMultipleSlurmd = "MULTIPLE_SLURMD" + ClusterFlagFrontEnd = "FRONT_END" + ClusterFlagCrayNative = "CRAY_NATIVE" + ClusterFlagFederation = "FEDERATION" + ClusterFlagExternal = "EXTERNAL" +) + +// GetClustersOptions specifies optional query parameters for GetClusters and GetCluster. +type GetClustersOptions struct { + Classification *string `url:"classification,omitempty"` + Cluster *string `url:"cluster,omitempty"` // CSV cluster list + Federation *string `url:"federation,omitempty"` // CSV federation list + Flags *string `url:"flags,omitempty"` // Use ClusterFlag* constants + Format *string `url:"format,omitempty"` // CSV format list + RpcVersion *string `url:"rpc_version,omitempty"` // CSV RPC version list + UsageEnd *string `url:"usage_end,omitempty"` // Usage end UNIX timestamp (seconds) + UsageStart *string `url:"usage_start,omitempty"` // Usage start UNIX timestamp (seconds) + WithDeleted *string `url:"with_deleted,omitempty"` // Include deleted clusters + WithUsage *string `url:"with_usage,omitempty"` // Query usage +} + +// DeleteClusterOptions specifies optional query parameters for DeleteCluster. +type DeleteClusterOptions struct { + Classification *string `url:"classification,omitempty"` + Cluster *string `url:"cluster,omitempty"` + Federation *string `url:"federation,omitempty"` + Flags *string `url:"flags,omitempty"` + Format *string `url:"format,omitempty"` + RpcVersion *string `url:"rpc_version,omitempty"` + UsageEnd *string `url:"usage_end,omitempty"` + UsageStart *string `url:"usage_start,omitempty"` + WithDeleted *string `url:"with_deleted,omitempty"` + WithUsage *string `url:"with_usage,omitempty"` +} + +func addGetClustersQueryParams(req *http.Request, opts *GetClustersOptions) error { + if opts == nil { + return nil + } + u, err := url.Parse(req.URL.String()) + if err != nil { + return err + } + q := u.Query() + if opts.Classification != nil { + q.Set("classification", *opts.Classification) + } + if opts.Cluster != nil { + q.Set("cluster", *opts.Cluster) + } + if opts.Federation != nil { + q.Set("federation", *opts.Federation) + } + if opts.Flags != nil { + q.Set("flags", *opts.Flags) + } + if opts.Format != nil { + q.Set("format", *opts.Format) + } + if opts.RpcVersion != nil { + q.Set("rpc_version", *opts.RpcVersion) + } + if opts.UsageEnd != nil { + q.Set("usage_end", *opts.UsageEnd) + } + if opts.UsageStart != nil { + q.Set("usage_start", *opts.UsageStart) + } + if opts.WithDeleted != nil { + q.Set("with_deleted", *opts.WithDeleted) + } + if opts.WithUsage != nil { + q.Set("with_usage", *opts.WithUsage) + } + u.RawQuery = q.Encode() + req.URL = u + return nil +} + +// addDeleteClusterQueryParams applies DeleteClusterOptions query params to the request URL. +func addDeleteClusterQueryParams(req *http.Request, opts *DeleteClusterOptions) error { + if opts == nil { + return nil + } + u, err := url.Parse(req.URL.String()) + if err != nil { + return err + } + q := u.Query() + if opts.Classification != nil { + q.Set("classification", *opts.Classification) + } + if opts.Cluster != nil { + q.Set("cluster", *opts.Cluster) + } + if opts.Federation != nil { + q.Set("federation", *opts.Federation) + } + if opts.Flags != nil { + q.Set("flags", *opts.Flags) + } + if opts.Format != nil { + q.Set("format", *opts.Format) + } + if opts.RpcVersion != nil { + q.Set("rpc_version", *opts.RpcVersion) + } + if opts.UsageEnd != nil { + q.Set("usage_end", *opts.UsageEnd) + } + if opts.UsageStart != nil { + q.Set("usage_start", *opts.UsageStart) + } + if opts.WithDeleted != nil { + q.Set("with_deleted", *opts.WithDeleted) + } + if opts.WithUsage != nil { + q.Set("with_usage", *opts.WithUsage) + } + u.RawQuery = q.Encode() + req.URL = u + return nil +} + +// GetClusters lists all clusters. +func (s *SlurmdbClustersService) GetClusters(ctx context.Context, opts *GetClustersOptions) (*OpenapiClustersResp, *Response, error) { + path := "slurmdb/v0.0.40/clusters" + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + + if err := addGetClustersQueryParams(req, opts); err != nil { + return nil, nil, err + } + + var result OpenapiClustersResp + resp, err := s.client.Do(ctx, req, &result) + if err != nil { + return nil, resp, err + } + return &result, resp, nil +} + +// GetCluster gets a single cluster by name. +func (s *SlurmdbClustersService) GetCluster(ctx context.Context, clusterName string, opts *GetClustersOptions) (*OpenapiClustersResp, *Response, error) { + path := fmt.Sprintf("slurmdb/v0.0.40/cluster/%s", clusterName) + req, err := s.client.NewRequest("GET", path, nil) + if err != nil { + return nil, nil, err + } + + if err := addGetClustersQueryParams(req, opts); err != nil { + return nil, nil, err + } + + var result OpenapiClustersResp + resp, err := s.client.Do(ctx, req, &result) + if err != nil { + return nil, resp, err + } + return &result, resp, nil +} + +// PostClusters creates or updates clusters. +func (s *SlurmdbClustersService) PostClusters(ctx context.Context, body *OpenapiClustersResp) (*OpenapiResp, *Response, error) { + path := "slurmdb/v0.0.40/clusters" + req, err := s.client.NewRequest("POST", path, body) + if err != nil { + return nil, nil, err + } + + var result OpenapiResp + resp, err := s.client.Do(ctx, req, &result) + if err != nil { + return nil, resp, err + } + return &result, resp, nil +} + +// DeleteCluster deletes a cluster by name. +func (s *SlurmdbClustersService) DeleteCluster(ctx context.Context, clusterName string, opts *DeleteClusterOptions) (*OpenapiClustersRemovedResp, *Response, error) { + path := fmt.Sprintf("slurmdb/v0.0.40/cluster/%s", clusterName) + req, err := s.client.NewRequest("DELETE", path, nil) + if err != nil { + return nil, nil, err + } + + if err := addDeleteClusterQueryParams(req, opts); err != nil { + return nil, nil, err + } + + var result OpenapiClustersRemovedResp + resp, err := s.client.Do(ctx, req, &result) + if err != nil { + return nil, resp, err + } + return &result, resp, nil +} diff --git a/internal/slurm/slurmdb_clusters_test.go b/internal/slurm/slurmdb_clusters_test.go new file mode 100644 index 0000000..d0d0f29 --- /dev/null +++ b/internal/slurm/slurmdb_clusters_test.go @@ -0,0 +1,221 @@ +package slurm + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestSlurmdbClustersService_GetClusters(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurmdb/v0.0.40/clusters", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"clusters": []}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + resp, _, err := client.SlurmdbClusters.GetClusters(context.Background(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestSlurmdbClustersService_GetClusters_WithOptions(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurmdb/v0.0.40/clusters", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + q := r.URL.Query() + if q.Get("classification") != ClusterClassificationCapability { + t.Errorf("expected classification=%s, got %s", ClusterClassificationCapability, q.Get("classification")) + } + if q.Get("flags") != ClusterFlagFederation { + t.Errorf("expected flags=%s, got %s", ClusterFlagFederation, q.Get("flags")) + } + if q.Get("with_deleted") != "true" { + t.Errorf("expected with_deleted=true, got %s", q.Get("with_deleted")) + } + if q.Get("with_usage") != "true" { + t.Errorf("expected with_usage=true, got %s", q.Get("with_usage")) + } + fmt.Fprint(w, `{"clusters": []}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + opts := &GetClustersOptions{ + Classification: Ptr(ClusterClassificationCapability), + Flags: Ptr(ClusterFlagFederation), + WithDeleted: Ptr("true"), + WithUsage: Ptr("true"), + } + resp, _, err := client.SlurmdbClusters.GetClusters(context.Background(), opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestSlurmdbClustersService_GetCluster(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurmdb/v0.0.40/cluster/test-cluster", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"clusters": [{"name": "test-cluster"}]}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + resp, _, err := client.SlurmdbClusters.GetCluster(context.Background(), "test-cluster", nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } + if len(resp.Clusters) != 1 { + t.Fatalf("expected 1 cluster, got %d", len(resp.Clusters)) + } +} + +func TestSlurmdbClustersService_GetCluster_WithOptions(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurmdb/v0.0.40/cluster/mycluster", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + q := r.URL.Query() + if q.Get("classification") != ClusterClassificationCapacity { + t.Errorf("expected classification=%s, got %s", ClusterClassificationCapacity, q.Get("classification")) + } + if q.Get("usage_start") != "1000000" { + t.Errorf("expected usage_start=1000000, got %s", q.Get("usage_start")) + } + if q.Get("usage_end") != "2000000" { + t.Errorf("expected usage_end=2000000, got %s", q.Get("usage_end")) + } + fmt.Fprint(w, `{"clusters": []}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + opts := &GetClustersOptions{ + Classification: Ptr(ClusterClassificationCapacity), + UsageStart: Ptr("1000000"), + UsageEnd: Ptr("2000000"), + } + resp, _, err := client.SlurmdbClusters.GetCluster(context.Background(), "mycluster", opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestSlurmdbClustersService_PostClusters(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurmdb/v0.0.40/clusters", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + if ct := r.Header.Get("Content-Type"); ct != "application/json" { + t.Errorf("expected Content-Type application/json, got %s", ct) + } + fmt.Fprint(w, `{}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + body := &OpenapiClustersResp{} + resp, _, err := client.SlurmdbClusters.PostClusters(context.Background(), body) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestSlurmdbClustersService_DeleteCluster(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurmdb/v0.0.40/cluster/old-cluster", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + q := r.URL.Query() + if q.Get("classification") != ClusterClassificationUNCLASSIFIED { + t.Errorf("expected classification=%s, got %s", ClusterClassificationUNCLASSIFIED, q.Get("classification")) + } + if q.Get("with_deleted") != "true" { + t.Errorf("expected with_deleted=true, got %s", q.Get("with_deleted")) + } + fmt.Fprint(w, `{"deleted_clusters": ["old-cluster"]}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + opts := &DeleteClusterOptions{ + Classification: Ptr(ClusterClassificationUNCLASSIFIED), + WithDeleted: Ptr("true"), + } + resp, _, err := client.SlurmdbClusters.DeleteCluster(context.Background(), "old-cluster", opts) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } + if len(resp.DeletedClusters) != 1 { + t.Fatalf("expected 1 deleted cluster, got %d", len(resp.DeletedClusters)) + } +} + +func TestSlurmdbClustersService_DeleteCluster_NoOptions(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurmdb/v0.0.40/cluster/test-cluster", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + q := r.URL.Query() + if len(q) != 0 { + t.Errorf("expected no query params, got %v", q) + } + fmt.Fprint(w, `{"deleted_clusters": []}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + resp, _, err := client.SlurmdbClusters.DeleteCluster(context.Background(), "test-cluster", nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp == nil { + t.Fatal("expected non-nil response") + } +} + +func TestSlurmdbClustersService_Error(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/slurmdb/v0.0.40/clusters", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprint(w, `{"errors": [{"error": "internal error"}]}`) + }) + server := httptest.NewServer(mux) + defer server.Close() + + client, _ := NewClient(server.URL, nil) + _, _, err := client.SlurmdbClusters.GetClusters(context.Background(), nil) + if err == nil { + t.Fatal("expected error for 500 response") + } + if !strings.Contains(err.Error(), "500") { + t.Errorf("expected error to contain 500, got %v", err) + } +}