From 0daf69cb49bbb1405af1a035d0450e731d73f6ec Mon Sep 17 00:00:00 2001 From: Giulio Calzolari Date: Tue, 23 Jun 2026 14:27:14 +0200 Subject: [PATCH 1/2] feat: add Account, User, and Association types for v0042-v0045 Add typed objects (Account, User, Assoc) and their list variants for the v0042, v0043, v0044, and v0045 accounting APIs, implementing the object.Object interface. Wire CRUD client methods into the version-specific clients, register them in each ClientInterface, and integrate with the high-level client, informer cache, and fake client. Include unit tests for the typed objects, client methods, and fake-client round trips. Signed-off-by: Giulio Calzolari --- pkg/client/api/v0042/account.go | 121 +++++++++ pkg/client/api/v0042/account_test.go | 182 ++++++++++++++ pkg/client/api/v0042/association.go | 93 +++++++ pkg/client/api/v0042/association_test.go | 113 +++++++++ pkg/client/api/v0042/user.go | 121 +++++++++ pkg/client/api/v0042/user_test.go | 160 ++++++++++++ pkg/client/api/v0042/v0042.go | 3 + pkg/client/api/v0043/account.go | 121 +++++++++ pkg/client/api/v0043/account_test.go | 182 ++++++++++++++ pkg/client/api/v0043/association.go | 93 +++++++ pkg/client/api/v0043/association_test.go | 113 +++++++++ pkg/client/api/v0043/user.go | 121 +++++++++ pkg/client/api/v0043/user_test.go | 160 ++++++++++++ pkg/client/api/v0043/v0043.go | 3 + pkg/client/api/v0044/account.go | 121 +++++++++ pkg/client/api/v0044/account_test.go | 306 +++++++++++++++++++++++ pkg/client/api/v0044/association.go | 93 +++++++ pkg/client/api/v0044/association_test.go | 113 +++++++++ pkg/client/api/v0044/user.go | 121 +++++++++ pkg/client/api/v0044/user_test.go | 160 ++++++++++++ pkg/client/api/v0044/v0044.go | 3 + pkg/client/api/v0045/account.go | 121 +++++++++ pkg/client/api/v0045/account_test.go | 182 ++++++++++++++ pkg/client/api/v0045/association.go | 93 +++++++ pkg/client/api/v0045/association_test.go | 113 +++++++++ pkg/client/api/v0045/user.go | 121 +++++++++ pkg/client/api/v0045/user_test.go | 160 ++++++++++++ pkg/client/api/v0045/v0045.go | 3 + pkg/client/client.go | 290 ++++++++++++++++++++- pkg/client/fake/client.go | 36 +++ pkg/client/fake/fake_account_test.go | 105 ++++++++ pkg/client/fake/fake_association_test.go | 90 +++++++ pkg/client/fake/fake_user_test.go | 105 ++++++++ pkg/client/informer.go | 84 +++++++ pkg/types/V0042Account.go | 76 ++++++ pkg/types/V0042Account_test.go | 31 +++ pkg/types/V0042Assoc.go | 85 +++++++ pkg/types/V0042Assoc_test.go | 39 +++ pkg/types/V0042User.go | 76 ++++++ pkg/types/V0042User_test.go | 30 +++ pkg/types/V0043Account.go | 76 ++++++ pkg/types/V0043Account_test.go | 31 +++ pkg/types/V0043Assoc.go | 85 +++++++ pkg/types/V0043Assoc_test.go | 39 +++ pkg/types/V0043User.go | 76 ++++++ pkg/types/V0043User_test.go | 30 +++ pkg/types/V0044Account.go | 76 ++++++ pkg/types/V0044Account_test.go | 31 +++ pkg/types/V0044Assoc.go | 85 +++++++ pkg/types/V0044Assoc_test.go | 39 +++ pkg/types/V0044User.go | 76 ++++++ pkg/types/V0044User_test.go | 30 +++ pkg/types/V0045Account.go | 76 ++++++ pkg/types/V0045Account_test.go | 31 +++ pkg/types/V0045Assoc.go | 85 +++++++ pkg/types/V0045Assoc_test.go | 39 +++ pkg/types/V0045User.go | 76 ++++++ pkg/types/V0045User_test.go | 30 +++ 58 files changed, 5353 insertions(+), 1 deletion(-) create mode 100644 pkg/client/api/v0042/account.go create mode 100644 pkg/client/api/v0042/account_test.go create mode 100644 pkg/client/api/v0042/association.go create mode 100644 pkg/client/api/v0042/association_test.go create mode 100644 pkg/client/api/v0042/user.go create mode 100644 pkg/client/api/v0042/user_test.go create mode 100644 pkg/client/api/v0043/account.go create mode 100644 pkg/client/api/v0043/account_test.go create mode 100644 pkg/client/api/v0043/association.go create mode 100644 pkg/client/api/v0043/association_test.go create mode 100644 pkg/client/api/v0043/user.go create mode 100644 pkg/client/api/v0043/user_test.go create mode 100644 pkg/client/api/v0044/account.go create mode 100644 pkg/client/api/v0044/account_test.go create mode 100644 pkg/client/api/v0044/association.go create mode 100644 pkg/client/api/v0044/association_test.go create mode 100644 pkg/client/api/v0044/user.go create mode 100644 pkg/client/api/v0044/user_test.go create mode 100644 pkg/client/api/v0045/account.go create mode 100644 pkg/client/api/v0045/account_test.go create mode 100644 pkg/client/api/v0045/association.go create mode 100644 pkg/client/api/v0045/association_test.go create mode 100644 pkg/client/api/v0045/user.go create mode 100644 pkg/client/api/v0045/user_test.go create mode 100644 pkg/client/fake/fake_account_test.go create mode 100644 pkg/client/fake/fake_association_test.go create mode 100644 pkg/client/fake/fake_user_test.go create mode 100644 pkg/types/V0042Account.go create mode 100644 pkg/types/V0042Account_test.go create mode 100644 pkg/types/V0042Assoc.go create mode 100644 pkg/types/V0042Assoc_test.go create mode 100644 pkg/types/V0042User.go create mode 100644 pkg/types/V0042User_test.go create mode 100644 pkg/types/V0043Account.go create mode 100644 pkg/types/V0043Account_test.go create mode 100644 pkg/types/V0043Assoc.go create mode 100644 pkg/types/V0043Assoc_test.go create mode 100644 pkg/types/V0043User.go create mode 100644 pkg/types/V0043User_test.go create mode 100644 pkg/types/V0044Account.go create mode 100644 pkg/types/V0044Account_test.go create mode 100644 pkg/types/V0044Assoc.go create mode 100644 pkg/types/V0044Assoc_test.go create mode 100644 pkg/types/V0044User.go create mode 100644 pkg/types/V0044User_test.go create mode 100644 pkg/types/V0045Account.go create mode 100644 pkg/types/V0045Account_test.go create mode 100644 pkg/types/V0045Assoc.go create mode 100644 pkg/types/V0045Assoc_test.go create mode 100644 pkg/types/V0045User.go create mode 100644 pkg/types/V0045User_test.go diff --git a/pkg/client/api/v0042/account.go b/pkg/client/api/v0042/account.go new file mode 100644 index 0000000..a242ee2 --- /dev/null +++ b/pkg/client/api/v0042/account.go @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0042 + +import ( + "context" + "errors" + "net/http" + + utilerrors "k8s.io/apimachinery/pkg/util/errors" + + api "github.com/SlinkyProject/slurm-client/api/v0042" + "github.com/SlinkyProject/slurm-client/pkg/types" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +type AccountInterface interface { + CreateAccount(ctx context.Context, req any) (string, error) + UpdateAccount(ctx context.Context, name string, req any) error + DeleteAccount(ctx context.Context, name string) error + GetAccount(ctx context.Context, name string) (*types.V0042Account, error) + ListAccount(ctx context.Context) (*types.V0042AccountList, error) +} + +var _ AccountInterface = &SlurmClient{} + +// CreateAccount implements AccountInterface. +func (c *SlurmClient) CreateAccount(ctx context.Context, req any) (string, error) { + r, ok := req.(api.V0042Account) + if !ok { + return "", errors.New("expected req to be api.V0042Account") + } + body := api.SlurmdbV0042PostAccountsJSONRequestBody{ + Accounts: api.V0042AccountList{r}, + } + res, err := c.SlurmdbV0042PostAccountsWithResponse(ctx, body) + if err != nil { + return "", err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return "", utilerrors.NewAggregate(errs) + } + return r.Name, nil +} + +// UpdateAccount implements AccountInterface. POST is upsert in slurmdbd. +func (c *SlurmClient) UpdateAccount(ctx context.Context, name string, req any) error { + r, ok := req.(api.V0042Account) + if !ok { + return errors.New("expected req to be api.V0042Account") + } + r.Name = name + _, err := c.CreateAccount(ctx, r) + return err +} + +// DeleteAccount implements AccountInterface. +func (c *SlurmClient) DeleteAccount(ctx context.Context, name string) error { + res, err := c.SlurmdbV0042DeleteAccountWithResponse(ctx, name) + if err != nil { + return err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return utilerrors.NewAggregate(errs) + } + return nil +} + +// GetAccount implements AccountInterface. +func (c *SlurmClient) GetAccount(ctx context.Context, name string) (*types.V0042Account, error) { + params := &api.SlurmdbV0042GetAccountParams{} + res, err := c.SlurmdbV0042GetAccountWithResponse(ctx, name, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + if res.JSON200 == nil || len(res.JSON200.Accounts) == 0 { + return nil, errors.New(http.StatusText(http.StatusNotFound)) + } + out := &types.V0042Account{} + utils.RemarshalOrDie(res.JSON200.Accounts[0], out) + return out, nil +} + +// ListAccount implements AccountInterface. +func (c *SlurmClient) ListAccount(ctx context.Context) (*types.V0042AccountList, error) { + params := &api.SlurmdbV0042GetAccountsParams{} + res, err := c.SlurmdbV0042GetAccountsWithResponse(ctx, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + list := &types.V0042AccountList{ + Items: make([]types.V0042Account, len(res.JSON200.Accounts)), + } + for i, item := range res.JSON200.Accounts { + utils.RemarshalOrDie(item, &list.Items[i]) + } + return list, nil +} diff --git a/pkg/client/api/v0042/account_test.go b/pkg/client/api/v0042/account_test.go new file mode 100644 index 0000000..4b0fd11 --- /dev/null +++ b/pkg/client/api/v0042/account_test.go @@ -0,0 +1,182 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0042 + +import ( + "context" + "errors" + "net/http" + "reflect" + "testing" + + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0042" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0042/fake" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0042/interceptor" + "github.com/SlinkyProject/slurm-client/pkg/types" +) + +func TestAccountInterface_Compiles(t *testing.T) { + var _ AccountInterface = &SlurmClient{} +} + +func TestSlurmClient_GetAccount(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + want *types.V0042Account + wantErr bool + }{ + { + name: "Not Found", + client: fake.NewFakeClient(), + want: nil, + wantErr: true, + }, + { + name: "Found", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0042GetAccountWithResponse: func(ctx context.Context, accountName string, params *api.SlurmdbV0042GetAccountParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0042GetAccountResponse, error) { + return &api.SlurmdbV0042GetAccountResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0042OpenapiAccountsResp{ + Accounts: api.V0042AccountList{{Name: "research"}}, + }, + }, nil + }, + }). + Build(), + want: &types.V0042Account{V0042Account: api.V0042Account{Name: "research"}}, + wantErr: false, + }, + { + name: "HTTP Status != 200", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0042GetAccountWithResponse: func(ctx context.Context, accountName string, params *api.SlurmdbV0042GetAccountParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0042GetAccountResponse, error) { + return &api.SlurmdbV0042GetAccountResponse{ + HTTPResponse: &http.Response{ + Status: http.StatusText(http.StatusInternalServerError), + StatusCode: http.StatusInternalServerError, + }, + JSONDefault: &api.V0042OpenapiAccountsResp{ + Errors: &[]api.V0042OpenapiError{{Error: ptr.To("error 1")}}, + }, + }, nil + }, + }). + Build(), + want: nil, + wantErr: true, + }, + { + name: "HTTP Error", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0042GetAccountWithResponse: func(ctx context.Context, accountName string, params *api.SlurmdbV0042GetAccountParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0042GetAccountResponse, error) { + return nil, errors.New(http.StatusText(http.StatusBadGateway)) + }, + }). + Build(), + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.GetAccount(context.Background(), "research") + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.GetAccount() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.GetAccount() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_ListAccount(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + want *types.V0042AccountList + wantErr bool + }{ + { + name: "Empty list", + client: fake.NewFakeClient(), + want: &types.V0042AccountList{Items: make([]types.V0042Account, 0)}, + wantErr: false, + }, + { + name: "Non-empty list", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0042GetAccountsWithResponse: func(ctx context.Context, params *api.SlurmdbV0042GetAccountsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0042GetAccountsResponse, error) { + return &api.SlurmdbV0042GetAccountsResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0042OpenapiAccountsResp{ + Accounts: api.V0042AccountList{{Name: "a"}, {Name: "b"}}, + }, + }, nil + }, + }). + Build(), + want: &types.V0042AccountList{Items: []types.V0042Account{ + {V0042Account: api.V0042Account{Name: "a"}}, + {V0042Account: api.V0042Account{Name: "b"}}, + }}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.ListAccount(context.Background()) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.ListAccount() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.ListAccount() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_CreateAccount(t *testing.T) { + tests := []struct { + name string + req any + want string + wantErr bool + }{ + {name: "default", req: api.V0042Account{Name: "research"}, want: "research", wantErr: false}, + {name: "invalid type provided", req: api.V0042User{Name: "research"}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + got, err := c.CreateAccount(context.Background(), tt.req) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.CreateAccount() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("SlurmClient.CreateAccount() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_DeleteAccount(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + if err := c.DeleteAccount(context.Background(), "research"); err != nil { + t.Errorf("SlurmClient.DeleteAccount() error = %v, wantErr false", err) + } +} diff --git a/pkg/client/api/v0042/association.go b/pkg/client/api/v0042/association.go new file mode 100644 index 0000000..de8accf --- /dev/null +++ b/pkg/client/api/v0042/association.go @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0042 + +import ( + "context" + "errors" + "net/http" + + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0042" + "github.com/SlinkyProject/slurm-client/pkg/types" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +type AssocInterface interface { + CreateAssoc(ctx context.Context, req any) error + DeleteAssoc(ctx context.Context, assoc api.V0042Assoc) error + ListAssoc(ctx context.Context) (*types.V0042AssocList, error) +} + +var _ AssocInterface = &SlurmClient{} + +// CreateAssoc implements AssocInterface (POST is upsert). +func (c *SlurmClient) CreateAssoc(ctx context.Context, req any) error { + r, ok := req.(api.V0042Assoc) + if !ok { + return errors.New("expected req to be api.V0042Assoc") + } + body := api.SlurmdbV0042PostAssociationsJSONRequestBody{ + Associations: api.V0042AssocList{r}, + } + res, err := c.SlurmdbV0042PostAssociationsWithResponse(ctx, body) + if err != nil { + return err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return utilerrors.NewAggregate(errs) + } + return nil +} + +// DeleteAssoc implements AssocInterface using a filter param set. +func (c *SlurmClient) DeleteAssoc(ctx context.Context, assoc api.V0042Assoc) error { + params := &api.SlurmdbV0042DeleteAssociationParams{ + Account: assoc.Account, + User: ptr.To(assoc.User), + Cluster: assoc.Cluster, + Partition: assoc.Partition, + } + res, err := c.SlurmdbV0042DeleteAssociationWithResponse(ctx, params) + if err != nil { + return err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return utilerrors.NewAggregate(errs) + } + return nil +} + +// ListAssoc implements AssocInterface. +func (c *SlurmClient) ListAssoc(ctx context.Context) (*types.V0042AssocList, error) { + params := &api.SlurmdbV0042GetAssociationsParams{} + res, err := c.SlurmdbV0042GetAssociationsWithResponse(ctx, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + list := &types.V0042AssocList{ + Items: make([]types.V0042Assoc, len(res.JSON200.Associations)), + } + for i, item := range res.JSON200.Associations { + utils.RemarshalOrDie(item, &list.Items[i]) + } + return list, nil +} diff --git a/pkg/client/api/v0042/association_test.go b/pkg/client/api/v0042/association_test.go new file mode 100644 index 0000000..de6a5b3 --- /dev/null +++ b/pkg/client/api/v0042/association_test.go @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0042 + +import ( + "context" + "errors" + "net/http" + "reflect" + "testing" + + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0042" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0042/fake" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0042/interceptor" + "github.com/SlinkyProject/slurm-client/pkg/types" +) + +func TestAssocInterface_Compiles(t *testing.T) { + var _ AssocInterface = &SlurmClient{} +} + +func TestSlurmClient_ListAssoc(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + want *types.V0042AssocList + wantErr bool + }{ + { + name: "Empty list", + client: fake.NewFakeClient(), + want: &types.V0042AssocList{Items: make([]types.V0042Assoc, 0)}, + wantErr: false, + }, + { + name: "Non-empty list", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0042GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0042GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0042GetAssociationsResponse, error) { + return &api.SlurmdbV0042GetAssociationsResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0042OpenapiAssocsResp{ + Associations: api.V0042AssocList{ + {User: "alice", Account: ptr.To("research")}, + }, + }, + }, nil + }, + }). + Build(), + want: &types.V0042AssocList{Items: []types.V0042Assoc{ + {V0042Assoc: api.V0042Assoc{User: "alice", Account: ptr.To("research")}}, + }}, + wantErr: false, + }, + { + name: "HTTP Error", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0042GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0042GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0042GetAssociationsResponse, error) { + return nil, errors.New(http.StatusText(http.StatusBadGateway)) + }, + }). + Build(), + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.ListAssoc(context.Background()) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.ListAssoc() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.ListAssoc() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_CreateAssoc(t *testing.T) { + tests := []struct { + name string + req any + wantErr bool + }{ + {name: "default", req: api.V0042Assoc{User: "alice", Account: ptr.To("research")}, wantErr: false}, + {name: "invalid type provided", req: api.V0042Account{Name: "research"}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + err := c.CreateAssoc(context.Background(), tt.req) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.CreateAssoc() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestSlurmClient_DeleteAssoc(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + err := c.DeleteAssoc(context.Background(), api.V0042Assoc{User: "alice", Account: ptr.To("research")}) + if err != nil { + t.Errorf("SlurmClient.DeleteAssoc() error = %v, wantErr false", err) + } +} diff --git a/pkg/client/api/v0042/user.go b/pkg/client/api/v0042/user.go new file mode 100644 index 0000000..d456847 --- /dev/null +++ b/pkg/client/api/v0042/user.go @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0042 + +import ( + "context" + "errors" + "net/http" + + utilerrors "k8s.io/apimachinery/pkg/util/errors" + + api "github.com/SlinkyProject/slurm-client/api/v0042" + "github.com/SlinkyProject/slurm-client/pkg/types" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +type UserInterface interface { + CreateUser(ctx context.Context, req any) (string, error) + UpdateUser(ctx context.Context, name string, req any) error + DeleteUser(ctx context.Context, name string) error + GetUser(ctx context.Context, name string) (*types.V0042User, error) + ListUser(ctx context.Context) (*types.V0042UserList, error) +} + +var _ UserInterface = &SlurmClient{} + +// CreateUser implements UserInterface. +func (c *SlurmClient) CreateUser(ctx context.Context, req any) (string, error) { + r, ok := req.(api.V0042User) + if !ok { + return "", errors.New("expected req to be api.V0042User") + } + body := api.SlurmdbV0042PostUsersJSONRequestBody{ + Users: api.V0042UserList{r}, + } + res, err := c.SlurmdbV0042PostUsersWithResponse(ctx, body) + if err != nil { + return "", err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return "", utilerrors.NewAggregate(errs) + } + return r.Name, nil +} + +// UpdateUser implements UserInterface. POST is upsert in slurmdbd. +func (c *SlurmClient) UpdateUser(ctx context.Context, name string, req any) error { + r, ok := req.(api.V0042User) + if !ok { + return errors.New("expected req to be api.V0042User") + } + r.Name = name + _, err := c.CreateUser(ctx, r) + return err +} + +// DeleteUser implements UserInterface. +func (c *SlurmClient) DeleteUser(ctx context.Context, name string) error { + res, err := c.SlurmdbV0042DeleteUserWithResponse(ctx, name) + if err != nil { + return err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return utilerrors.NewAggregate(errs) + } + return nil +} + +// GetUser implements UserInterface. +func (c *SlurmClient) GetUser(ctx context.Context, name string) (*types.V0042User, error) { + params := &api.SlurmdbV0042GetUserParams{} + res, err := c.SlurmdbV0042GetUserWithResponse(ctx, name, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + if res.JSON200 == nil || len(res.JSON200.Users) == 0 { + return nil, errors.New(http.StatusText(http.StatusNotFound)) + } + out := &types.V0042User{} + utils.RemarshalOrDie(res.JSON200.Users[0], out) + return out, nil +} + +// ListUser implements UserInterface. +func (c *SlurmClient) ListUser(ctx context.Context) (*types.V0042UserList, error) { + params := &api.SlurmdbV0042GetUsersParams{} + res, err := c.SlurmdbV0042GetUsersWithResponse(ctx, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + list := &types.V0042UserList{ + Items: make([]types.V0042User, len(res.JSON200.Users)), + } + for i, item := range res.JSON200.Users { + utils.RemarshalOrDie(item, &list.Items[i]) + } + return list, nil +} diff --git a/pkg/client/api/v0042/user_test.go b/pkg/client/api/v0042/user_test.go new file mode 100644 index 0000000..1faf256 --- /dev/null +++ b/pkg/client/api/v0042/user_test.go @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0042 + +import ( + "context" + "errors" + "net/http" + "reflect" + "testing" + + api "github.com/SlinkyProject/slurm-client/api/v0042" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0042/fake" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0042/interceptor" + "github.com/SlinkyProject/slurm-client/pkg/types" +) + +func TestUserInterface_Compiles(t *testing.T) { + var _ UserInterface = &SlurmClient{} +} + +func TestSlurmClient_GetUser(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + want *types.V0042User + wantErr bool + }{ + { + name: "Not Found", + client: fake.NewFakeClient(), + want: nil, + wantErr: true, + }, + { + name: "Found", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0042GetUserWithResponse: func(ctx context.Context, name string, params *api.SlurmdbV0042GetUserParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0042GetUserResponse, error) { + return &api.SlurmdbV0042GetUserResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0042OpenapiUsersResp{ + Users: api.V0042UserList{{Name: "alice"}}, + }, + }, nil + }, + }). + Build(), + want: &types.V0042User{V0042User: api.V0042User{Name: "alice"}}, + wantErr: false, + }, + { + name: "HTTP Error", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0042GetUserWithResponse: func(ctx context.Context, name string, params *api.SlurmdbV0042GetUserParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0042GetUserResponse, error) { + return nil, errors.New(http.StatusText(http.StatusBadGateway)) + }, + }). + Build(), + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.GetUser(context.Background(), "alice") + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.GetUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.GetUser() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_ListUser(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + want *types.V0042UserList + wantErr bool + }{ + { + name: "Empty list", + client: fake.NewFakeClient(), + want: &types.V0042UserList{Items: make([]types.V0042User, 0)}, + wantErr: false, + }, + { + name: "Non-empty list", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0042GetUsersWithResponse: func(ctx context.Context, params *api.SlurmdbV0042GetUsersParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0042GetUsersResponse, error) { + return &api.SlurmdbV0042GetUsersResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0042OpenapiUsersResp{ + Users: api.V0042UserList{{Name: "alice"}, {Name: "bob"}}, + }, + }, nil + }, + }). + Build(), + want: &types.V0042UserList{Items: []types.V0042User{ + {V0042User: api.V0042User{Name: "alice"}}, + {V0042User: api.V0042User{Name: "bob"}}, + }}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.ListUser(context.Background()) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.ListUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.ListUser() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_CreateUser(t *testing.T) { + tests := []struct { + name string + req any + want string + wantErr bool + }{ + {name: "default", req: api.V0042User{Name: "alice"}, want: "alice", wantErr: false}, + {name: "invalid type provided", req: api.V0042Account{Name: "alice"}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + got, err := c.CreateUser(context.Background(), tt.req) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.CreateUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("SlurmClient.CreateUser() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_DeleteUser(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + if err := c.DeleteUser(context.Background(), "alice"); err != nil { + t.Errorf("SlurmClient.DeleteUser() error = %v, wantErr false", err) + } +} diff --git a/pkg/client/api/v0042/v0042.go b/pkg/client/api/v0042/v0042.go index 77bf7f4..2954099 100644 --- a/pkg/client/api/v0042/v0042.go +++ b/pkg/client/api/v0042/v0042.go @@ -23,12 +23,15 @@ const ( type ClientInterface interface { api.ClientWithResponsesInterface + AccountInterface + AssocInterface ControllerPingInfoInterface JobInfoInterface NodeInterface PartitionInterface ReconfigureInterface StatsInterface + UserInterface } type SlurmClient struct { diff --git a/pkg/client/api/v0043/account.go b/pkg/client/api/v0043/account.go new file mode 100644 index 0000000..e9a94fa --- /dev/null +++ b/pkg/client/api/v0043/account.go @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0043 + +import ( + "context" + "errors" + "net/http" + + utilerrors "k8s.io/apimachinery/pkg/util/errors" + + api "github.com/SlinkyProject/slurm-client/api/v0043" + "github.com/SlinkyProject/slurm-client/pkg/types" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +type AccountInterface interface { + CreateAccount(ctx context.Context, req any) (string, error) + UpdateAccount(ctx context.Context, name string, req any) error + DeleteAccount(ctx context.Context, name string) error + GetAccount(ctx context.Context, name string) (*types.V0043Account, error) + ListAccount(ctx context.Context) (*types.V0043AccountList, error) +} + +var _ AccountInterface = &SlurmClient{} + +// CreateAccount implements AccountInterface. +func (c *SlurmClient) CreateAccount(ctx context.Context, req any) (string, error) { + r, ok := req.(api.V0043Account) + if !ok { + return "", errors.New("expected req to be api.V0043Account") + } + body := api.SlurmdbV0043PostAccountsJSONRequestBody{ + Accounts: api.V0043AccountList{r}, + } + res, err := c.SlurmdbV0043PostAccountsWithResponse(ctx, body) + if err != nil { + return "", err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return "", utilerrors.NewAggregate(errs) + } + return r.Name, nil +} + +// UpdateAccount implements AccountInterface. POST is upsert in slurmdbd. +func (c *SlurmClient) UpdateAccount(ctx context.Context, name string, req any) error { + r, ok := req.(api.V0043Account) + if !ok { + return errors.New("expected req to be api.V0043Account") + } + r.Name = name + _, err := c.CreateAccount(ctx, r) + return err +} + +// DeleteAccount implements AccountInterface. +func (c *SlurmClient) DeleteAccount(ctx context.Context, name string) error { + res, err := c.SlurmdbV0043DeleteAccountWithResponse(ctx, name) + if err != nil { + return err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return utilerrors.NewAggregate(errs) + } + return nil +} + +// GetAccount implements AccountInterface. +func (c *SlurmClient) GetAccount(ctx context.Context, name string) (*types.V0043Account, error) { + params := &api.SlurmdbV0043GetAccountParams{} + res, err := c.SlurmdbV0043GetAccountWithResponse(ctx, name, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + if res.JSON200 == nil || len(res.JSON200.Accounts) == 0 { + return nil, errors.New(http.StatusText(http.StatusNotFound)) + } + out := &types.V0043Account{} + utils.RemarshalOrDie(res.JSON200.Accounts[0], out) + return out, nil +} + +// ListAccount implements AccountInterface. +func (c *SlurmClient) ListAccount(ctx context.Context) (*types.V0043AccountList, error) { + params := &api.SlurmdbV0043GetAccountsParams{} + res, err := c.SlurmdbV0043GetAccountsWithResponse(ctx, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + list := &types.V0043AccountList{ + Items: make([]types.V0043Account, len(res.JSON200.Accounts)), + } + for i, item := range res.JSON200.Accounts { + utils.RemarshalOrDie(item, &list.Items[i]) + } + return list, nil +} diff --git a/pkg/client/api/v0043/account_test.go b/pkg/client/api/v0043/account_test.go new file mode 100644 index 0000000..47d4272 --- /dev/null +++ b/pkg/client/api/v0043/account_test.go @@ -0,0 +1,182 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0043 + +import ( + "context" + "errors" + "net/http" + "reflect" + "testing" + + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0043" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0043/fake" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0043/interceptor" + "github.com/SlinkyProject/slurm-client/pkg/types" +) + +func TestAccountInterface_Compiles(t *testing.T) { + var _ AccountInterface = &SlurmClient{} +} + +func TestSlurmClient_GetAccount(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + want *types.V0043Account + wantErr bool + }{ + { + name: "Not Found", + client: fake.NewFakeClient(), + want: nil, + wantErr: true, + }, + { + name: "Found", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0043GetAccountWithResponse: func(ctx context.Context, accountName string, params *api.SlurmdbV0043GetAccountParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0043GetAccountResponse, error) { + return &api.SlurmdbV0043GetAccountResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0043OpenapiAccountsResp{ + Accounts: api.V0043AccountList{{Name: "research"}}, + }, + }, nil + }, + }). + Build(), + want: &types.V0043Account{V0043Account: api.V0043Account{Name: "research"}}, + wantErr: false, + }, + { + name: "HTTP Status != 200", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0043GetAccountWithResponse: func(ctx context.Context, accountName string, params *api.SlurmdbV0043GetAccountParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0043GetAccountResponse, error) { + return &api.SlurmdbV0043GetAccountResponse{ + HTTPResponse: &http.Response{ + Status: http.StatusText(http.StatusInternalServerError), + StatusCode: http.StatusInternalServerError, + }, + JSONDefault: &api.V0043OpenapiAccountsResp{ + Errors: &[]api.V0043OpenapiError{{Error: ptr.To("error 1")}}, + }, + }, nil + }, + }). + Build(), + want: nil, + wantErr: true, + }, + { + name: "HTTP Error", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0043GetAccountWithResponse: func(ctx context.Context, accountName string, params *api.SlurmdbV0043GetAccountParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0043GetAccountResponse, error) { + return nil, errors.New(http.StatusText(http.StatusBadGateway)) + }, + }). + Build(), + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.GetAccount(context.Background(), "research") + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.GetAccount() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.GetAccount() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_ListAccount(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + want *types.V0043AccountList + wantErr bool + }{ + { + name: "Empty list", + client: fake.NewFakeClient(), + want: &types.V0043AccountList{Items: make([]types.V0043Account, 0)}, + wantErr: false, + }, + { + name: "Non-empty list", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0043GetAccountsWithResponse: func(ctx context.Context, params *api.SlurmdbV0043GetAccountsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0043GetAccountsResponse, error) { + return &api.SlurmdbV0043GetAccountsResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0043OpenapiAccountsResp{ + Accounts: api.V0043AccountList{{Name: "a"}, {Name: "b"}}, + }, + }, nil + }, + }). + Build(), + want: &types.V0043AccountList{Items: []types.V0043Account{ + {V0043Account: api.V0043Account{Name: "a"}}, + {V0043Account: api.V0043Account{Name: "b"}}, + }}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.ListAccount(context.Background()) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.ListAccount() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.ListAccount() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_CreateAccount(t *testing.T) { + tests := []struct { + name string + req any + want string + wantErr bool + }{ + {name: "default", req: api.V0043Account{Name: "research"}, want: "research", wantErr: false}, + {name: "invalid type provided", req: api.V0043User{Name: "research"}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + got, err := c.CreateAccount(context.Background(), tt.req) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.CreateAccount() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("SlurmClient.CreateAccount() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_DeleteAccount(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + if err := c.DeleteAccount(context.Background(), "research"); err != nil { + t.Errorf("SlurmClient.DeleteAccount() error = %v, wantErr false", err) + } +} diff --git a/pkg/client/api/v0043/association.go b/pkg/client/api/v0043/association.go new file mode 100644 index 0000000..8df66c1 --- /dev/null +++ b/pkg/client/api/v0043/association.go @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0043 + +import ( + "context" + "errors" + "net/http" + + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0043" + "github.com/SlinkyProject/slurm-client/pkg/types" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +type AssocInterface interface { + CreateAssoc(ctx context.Context, req any) error + DeleteAssoc(ctx context.Context, assoc api.V0043Assoc) error + ListAssoc(ctx context.Context) (*types.V0043AssocList, error) +} + +var _ AssocInterface = &SlurmClient{} + +// CreateAssoc implements AssocInterface (POST is upsert). +func (c *SlurmClient) CreateAssoc(ctx context.Context, req any) error { + r, ok := req.(api.V0043Assoc) + if !ok { + return errors.New("expected req to be api.V0043Assoc") + } + body := api.SlurmdbV0043PostAssociationsJSONRequestBody{ + Associations: api.V0043AssocList{r}, + } + res, err := c.SlurmdbV0043PostAssociationsWithResponse(ctx, body) + if err != nil { + return err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return utilerrors.NewAggregate(errs) + } + return nil +} + +// DeleteAssoc implements AssocInterface using a filter param set. +func (c *SlurmClient) DeleteAssoc(ctx context.Context, assoc api.V0043Assoc) error { + params := &api.SlurmdbV0043DeleteAssociationParams{ + Account: assoc.Account, + User: ptr.To(assoc.User), + Cluster: assoc.Cluster, + Partition: assoc.Partition, + } + res, err := c.SlurmdbV0043DeleteAssociationWithResponse(ctx, params) + if err != nil { + return err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return utilerrors.NewAggregate(errs) + } + return nil +} + +// ListAssoc implements AssocInterface. +func (c *SlurmClient) ListAssoc(ctx context.Context) (*types.V0043AssocList, error) { + params := &api.SlurmdbV0043GetAssociationsParams{} + res, err := c.SlurmdbV0043GetAssociationsWithResponse(ctx, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + list := &types.V0043AssocList{ + Items: make([]types.V0043Assoc, len(res.JSON200.Associations)), + } + for i, item := range res.JSON200.Associations { + utils.RemarshalOrDie(item, &list.Items[i]) + } + return list, nil +} diff --git a/pkg/client/api/v0043/association_test.go b/pkg/client/api/v0043/association_test.go new file mode 100644 index 0000000..3628588 --- /dev/null +++ b/pkg/client/api/v0043/association_test.go @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0043 + +import ( + "context" + "errors" + "net/http" + "reflect" + "testing" + + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0043" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0043/fake" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0043/interceptor" + "github.com/SlinkyProject/slurm-client/pkg/types" +) + +func TestAssocInterface_Compiles(t *testing.T) { + var _ AssocInterface = &SlurmClient{} +} + +func TestSlurmClient_ListAssoc(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + want *types.V0043AssocList + wantErr bool + }{ + { + name: "Empty list", + client: fake.NewFakeClient(), + want: &types.V0043AssocList{Items: make([]types.V0043Assoc, 0)}, + wantErr: false, + }, + { + name: "Non-empty list", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0043GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0043GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0043GetAssociationsResponse, error) { + return &api.SlurmdbV0043GetAssociationsResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0043OpenapiAssocsResp{ + Associations: api.V0043AssocList{ + {User: "alice", Account: ptr.To("research")}, + }, + }, + }, nil + }, + }). + Build(), + want: &types.V0043AssocList{Items: []types.V0043Assoc{ + {V0043Assoc: api.V0043Assoc{User: "alice", Account: ptr.To("research")}}, + }}, + wantErr: false, + }, + { + name: "HTTP Error", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0043GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0043GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0043GetAssociationsResponse, error) { + return nil, errors.New(http.StatusText(http.StatusBadGateway)) + }, + }). + Build(), + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.ListAssoc(context.Background()) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.ListAssoc() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.ListAssoc() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_CreateAssoc(t *testing.T) { + tests := []struct { + name string + req any + wantErr bool + }{ + {name: "default", req: api.V0043Assoc{User: "alice", Account: ptr.To("research")}, wantErr: false}, + {name: "invalid type provided", req: api.V0043Account{Name: "research"}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + err := c.CreateAssoc(context.Background(), tt.req) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.CreateAssoc() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestSlurmClient_DeleteAssoc(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + err := c.DeleteAssoc(context.Background(), api.V0043Assoc{User: "alice", Account: ptr.To("research")}) + if err != nil { + t.Errorf("SlurmClient.DeleteAssoc() error = %v, wantErr false", err) + } +} diff --git a/pkg/client/api/v0043/user.go b/pkg/client/api/v0043/user.go new file mode 100644 index 0000000..e224637 --- /dev/null +++ b/pkg/client/api/v0043/user.go @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0043 + +import ( + "context" + "errors" + "net/http" + + utilerrors "k8s.io/apimachinery/pkg/util/errors" + + api "github.com/SlinkyProject/slurm-client/api/v0043" + "github.com/SlinkyProject/slurm-client/pkg/types" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +type UserInterface interface { + CreateUser(ctx context.Context, req any) (string, error) + UpdateUser(ctx context.Context, name string, req any) error + DeleteUser(ctx context.Context, name string) error + GetUser(ctx context.Context, name string) (*types.V0043User, error) + ListUser(ctx context.Context) (*types.V0043UserList, error) +} + +var _ UserInterface = &SlurmClient{} + +// CreateUser implements UserInterface. +func (c *SlurmClient) CreateUser(ctx context.Context, req any) (string, error) { + r, ok := req.(api.V0043User) + if !ok { + return "", errors.New("expected req to be api.V0043User") + } + body := api.SlurmdbV0043PostUsersJSONRequestBody{ + Users: api.V0043UserList{r}, + } + res, err := c.SlurmdbV0043PostUsersWithResponse(ctx, body) + if err != nil { + return "", err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return "", utilerrors.NewAggregate(errs) + } + return r.Name, nil +} + +// UpdateUser implements UserInterface. POST is upsert in slurmdbd. +func (c *SlurmClient) UpdateUser(ctx context.Context, name string, req any) error { + r, ok := req.(api.V0043User) + if !ok { + return errors.New("expected req to be api.V0043User") + } + r.Name = name + _, err := c.CreateUser(ctx, r) + return err +} + +// DeleteUser implements UserInterface. +func (c *SlurmClient) DeleteUser(ctx context.Context, name string) error { + res, err := c.SlurmdbV0043DeleteUserWithResponse(ctx, name) + if err != nil { + return err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return utilerrors.NewAggregate(errs) + } + return nil +} + +// GetUser implements UserInterface. +func (c *SlurmClient) GetUser(ctx context.Context, name string) (*types.V0043User, error) { + params := &api.SlurmdbV0043GetUserParams{} + res, err := c.SlurmdbV0043GetUserWithResponse(ctx, name, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + if res.JSON200 == nil || len(res.JSON200.Users) == 0 { + return nil, errors.New(http.StatusText(http.StatusNotFound)) + } + out := &types.V0043User{} + utils.RemarshalOrDie(res.JSON200.Users[0], out) + return out, nil +} + +// ListUser implements UserInterface. +func (c *SlurmClient) ListUser(ctx context.Context) (*types.V0043UserList, error) { + params := &api.SlurmdbV0043GetUsersParams{} + res, err := c.SlurmdbV0043GetUsersWithResponse(ctx, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + list := &types.V0043UserList{ + Items: make([]types.V0043User, len(res.JSON200.Users)), + } + for i, item := range res.JSON200.Users { + utils.RemarshalOrDie(item, &list.Items[i]) + } + return list, nil +} diff --git a/pkg/client/api/v0043/user_test.go b/pkg/client/api/v0043/user_test.go new file mode 100644 index 0000000..c73867e --- /dev/null +++ b/pkg/client/api/v0043/user_test.go @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0043 + +import ( + "context" + "errors" + "net/http" + "reflect" + "testing" + + api "github.com/SlinkyProject/slurm-client/api/v0043" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0043/fake" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0043/interceptor" + "github.com/SlinkyProject/slurm-client/pkg/types" +) + +func TestUserInterface_Compiles(t *testing.T) { + var _ UserInterface = &SlurmClient{} +} + +func TestSlurmClient_GetUser(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + want *types.V0043User + wantErr bool + }{ + { + name: "Not Found", + client: fake.NewFakeClient(), + want: nil, + wantErr: true, + }, + { + name: "Found", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0043GetUserWithResponse: func(ctx context.Context, name string, params *api.SlurmdbV0043GetUserParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0043GetUserResponse, error) { + return &api.SlurmdbV0043GetUserResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0043OpenapiUsersResp{ + Users: api.V0043UserList{{Name: "alice"}}, + }, + }, nil + }, + }). + Build(), + want: &types.V0043User{V0043User: api.V0043User{Name: "alice"}}, + wantErr: false, + }, + { + name: "HTTP Error", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0043GetUserWithResponse: func(ctx context.Context, name string, params *api.SlurmdbV0043GetUserParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0043GetUserResponse, error) { + return nil, errors.New(http.StatusText(http.StatusBadGateway)) + }, + }). + Build(), + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.GetUser(context.Background(), "alice") + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.GetUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.GetUser() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_ListUser(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + want *types.V0043UserList + wantErr bool + }{ + { + name: "Empty list", + client: fake.NewFakeClient(), + want: &types.V0043UserList{Items: make([]types.V0043User, 0)}, + wantErr: false, + }, + { + name: "Non-empty list", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0043GetUsersWithResponse: func(ctx context.Context, params *api.SlurmdbV0043GetUsersParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0043GetUsersResponse, error) { + return &api.SlurmdbV0043GetUsersResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0043OpenapiUsersResp{ + Users: api.V0043UserList{{Name: "alice"}, {Name: "bob"}}, + }, + }, nil + }, + }). + Build(), + want: &types.V0043UserList{Items: []types.V0043User{ + {V0043User: api.V0043User{Name: "alice"}}, + {V0043User: api.V0043User{Name: "bob"}}, + }}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.ListUser(context.Background()) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.ListUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.ListUser() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_CreateUser(t *testing.T) { + tests := []struct { + name string + req any + want string + wantErr bool + }{ + {name: "default", req: api.V0043User{Name: "alice"}, want: "alice", wantErr: false}, + {name: "invalid type provided", req: api.V0043Account{Name: "alice"}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + got, err := c.CreateUser(context.Background(), tt.req) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.CreateUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("SlurmClient.CreateUser() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_DeleteUser(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + if err := c.DeleteUser(context.Background(), "alice"); err != nil { + t.Errorf("SlurmClient.DeleteUser() error = %v, wantErr false", err) + } +} diff --git a/pkg/client/api/v0043/v0043.go b/pkg/client/api/v0043/v0043.go index 7bfa3d0..25d6c04 100644 --- a/pkg/client/api/v0043/v0043.go +++ b/pkg/client/api/v0043/v0043.go @@ -24,12 +24,15 @@ const ( type ClientInterface interface { api.ClientWithResponsesInterface + AccountInterface + AssocInterface ControllerPingInfoInterface JobInfoInterface NodeInterface PartitionInterface ReconfigureInterface StatsInterface + UserInterface } type SlurmClient struct { diff --git a/pkg/client/api/v0044/account.go b/pkg/client/api/v0044/account.go new file mode 100644 index 0000000..d132689 --- /dev/null +++ b/pkg/client/api/v0044/account.go @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0044 + +import ( + "context" + "errors" + "net/http" + + utilerrors "k8s.io/apimachinery/pkg/util/errors" + + api "github.com/SlinkyProject/slurm-client/api/v0044" + "github.com/SlinkyProject/slurm-client/pkg/types" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +type AccountInterface interface { + CreateAccount(ctx context.Context, req any) (string, error) + UpdateAccount(ctx context.Context, name string, req any) error + DeleteAccount(ctx context.Context, name string) error + GetAccount(ctx context.Context, name string) (*types.V0044Account, error) + ListAccount(ctx context.Context) (*types.V0044AccountList, error) +} + +var _ AccountInterface = &SlurmClient{} + +// CreateAccount implements AccountInterface. +func (c *SlurmClient) CreateAccount(ctx context.Context, req any) (string, error) { + r, ok := req.(api.V0044Account) + if !ok { + return "", errors.New("expected req to be api.V0044Account") + } + body := api.SlurmdbV0044PostAccountsJSONRequestBody{ + Accounts: api.V0044AccountList{r}, + } + res, err := c.SlurmdbV0044PostAccountsWithResponse(ctx, body) + if err != nil { + return "", err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return "", utilerrors.NewAggregate(errs) + } + return r.Name, nil +} + +// UpdateAccount implements AccountInterface. POST is upsert in slurmdbd. +func (c *SlurmClient) UpdateAccount(ctx context.Context, name string, req any) error { + r, ok := req.(api.V0044Account) + if !ok { + return errors.New("expected req to be api.V0044Account") + } + r.Name = name + _, err := c.CreateAccount(ctx, r) + return err +} + +// DeleteAccount implements AccountInterface. +func (c *SlurmClient) DeleteAccount(ctx context.Context, name string) error { + res, err := c.SlurmdbV0044DeleteAccountWithResponse(ctx, name) + if err != nil { + return err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return utilerrors.NewAggregate(errs) + } + return nil +} + +// GetAccount implements AccountInterface. +func (c *SlurmClient) GetAccount(ctx context.Context, name string) (*types.V0044Account, error) { + params := &api.SlurmdbV0044GetAccountParams{} + res, err := c.SlurmdbV0044GetAccountWithResponse(ctx, name, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + if res.JSON200 == nil || len(res.JSON200.Accounts) == 0 { + return nil, errors.New(http.StatusText(http.StatusNotFound)) + } + out := &types.V0044Account{} + utils.RemarshalOrDie(res.JSON200.Accounts[0], out) + return out, nil +} + +// ListAccount implements AccountInterface. +func (c *SlurmClient) ListAccount(ctx context.Context) (*types.V0044AccountList, error) { + params := &api.SlurmdbV0044GetAccountsParams{} + res, err := c.SlurmdbV0044GetAccountsWithResponse(ctx, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + list := &types.V0044AccountList{ + Items: make([]types.V0044Account, len(res.JSON200.Accounts)), + } + for i, item := range res.JSON200.Accounts { + utils.RemarshalOrDie(item, &list.Items[i]) + } + return list, nil +} diff --git a/pkg/client/api/v0044/account_test.go b/pkg/client/api/v0044/account_test.go new file mode 100644 index 0000000..c9dc2ba --- /dev/null +++ b/pkg/client/api/v0044/account_test.go @@ -0,0 +1,306 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0044 + +import ( + "context" + "errors" + "net/http" + "reflect" + "testing" + + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0044" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0044/fake" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0044/interceptor" + "github.com/SlinkyProject/slurm-client/pkg/types" +) + +func TestAccountInterface_Compiles(t *testing.T) { + var _ AccountInterface = &SlurmClient{} +} + +func TestSlurmClient_GetAccount(t *testing.T) { + type fields struct { + ClientWithResponsesInterface api.ClientWithResponsesInterface + } + type args struct { + ctx context.Context + name string + } + tests := []struct { + name string + fields fields + args args + want *types.V0044Account + wantErr bool + }{ + { + name: "Not Found", + fields: fields{ + ClientWithResponsesInterface: fake.NewFakeClient(), + }, + args: args{ + ctx: context.Background(), + name: "research", + }, + want: nil, + wantErr: true, + }, + { + name: "Found", + fields: fields{ + ClientWithResponsesInterface: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0044GetAccountWithResponse: func(ctx context.Context, accountName string, params *api.SlurmdbV0044GetAccountParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0044GetAccountResponse, error) { + res := &api.SlurmdbV0044GetAccountResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0044OpenapiAccountsResp{ + Accounts: api.V0044AccountList{ + {Name: "research"}, + }, + }, + } + return res, nil + }, + }). + Build(), + }, + args: args{ + ctx: context.Background(), + name: "research", + }, + want: &types.V0044Account{ + V0044Account: api.V0044Account{Name: "research"}, + }, + wantErr: false, + }, + { + name: "HTTP Status != 200", + fields: fields{ + ClientWithResponsesInterface: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0044GetAccountWithResponse: func(ctx context.Context, accountName string, params *api.SlurmdbV0044GetAccountParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0044GetAccountResponse, error) { + res := &api.SlurmdbV0044GetAccountResponse{ + HTTPResponse: &http.Response{ + Status: http.StatusText(http.StatusInternalServerError), + StatusCode: http.StatusInternalServerError, + }, + JSONDefault: &api.V0044OpenapiAccountsResp{ + Errors: &[]api.V0044OpenapiError{ + {Error: ptr.To("error 1")}, + }, + }, + } + return res, nil + }, + }). + Build(), + }, + args: args{ + ctx: context.Background(), + name: "research", + }, + want: nil, + wantErr: true, + }, + { + name: "HTTP Error", + fields: fields{ + ClientWithResponsesInterface: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0044GetAccountWithResponse: func(ctx context.Context, accountName string, params *api.SlurmdbV0044GetAccountParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0044GetAccountResponse, error) { + return nil, errors.New(http.StatusText(http.StatusBadGateway)) + }, + }). + Build(), + }, + args: args{ + ctx: context.Background(), + name: "research", + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ + ClientWithResponsesInterface: tt.fields.ClientWithResponsesInterface, + } + got, err := c.GetAccount(tt.args.ctx, tt.args.name) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.GetAccount() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.GetAccount() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_ListAccount(t *testing.T) { + type fields struct { + ClientWithResponsesInterface api.ClientWithResponsesInterface + } + tests := []struct { + name string + fields fields + want *types.V0044AccountList + wantErr bool + }{ + { + name: "Empty list", + fields: fields{ + ClientWithResponsesInterface: fake.NewFakeClient(), + }, + want: &types.V0044AccountList{ + Items: make([]types.V0044Account, 0), + }, + wantErr: false, + }, + { + name: "Non-empty list", + fields: fields{ + ClientWithResponsesInterface: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0044GetAccountsWithResponse: func(ctx context.Context, params *api.SlurmdbV0044GetAccountsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0044GetAccountsResponse, error) { + res := &api.SlurmdbV0044GetAccountsResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0044OpenapiAccountsResp{ + Accounts: api.V0044AccountList{ + {Name: "a"}, + {Name: "b"}, + }, + }, + } + return res, nil + }, + }). + Build(), + }, + want: &types.V0044AccountList{ + Items: []types.V0044Account{ + {V0044Account: api.V0044Account{Name: "a"}}, + {V0044Account: api.V0044Account{Name: "b"}}, + }, + }, + wantErr: false, + }, + { + name: "HTTP Error", + fields: fields{ + ClientWithResponsesInterface: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0044GetAccountsWithResponse: func(ctx context.Context, params *api.SlurmdbV0044GetAccountsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0044GetAccountsResponse, error) { + return nil, errors.New(http.StatusText(http.StatusBadGateway)) + }, + }). + Build(), + }, + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ + ClientWithResponsesInterface: tt.fields.ClientWithResponsesInterface, + } + got, err := c.ListAccount(context.Background()) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.ListAccount() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.ListAccount() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_CreateAccount(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + req any + want string + wantErr bool + }{ + { + name: "default", + client: fake.NewFakeClient(), + req: api.V0044Account{Name: "research"}, + want: "research", + wantErr: false, + }, + { + name: "invalid type provided", + client: fake.NewFakeClient(), + req: api.V0044User{Name: "research"}, + wantErr: true, + }, + { + name: "HTTP Error", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0044PostAccountsWithResponse: func(ctx context.Context, body api.SlurmdbV0044PostAccountsJSONRequestBody, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0044PostAccountsResponse, error) { + return nil, errors.New(http.StatusText(http.StatusBadGateway)) + }, + }). + Build(), + req: api.V0044Account{Name: "research"}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.CreateAccount(context.Background(), tt.req) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.CreateAccount() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("SlurmClient.CreateAccount() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_DeleteAccount(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + wantErr bool + }{ + { + name: "account does not exist", + client: fake.NewFakeClient(), + wantErr: false, + }, + { + name: "HTTP Error", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0044DeleteAccountWithResponse: func(ctx context.Context, accountName string, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0044DeleteAccountResponse, error) { + return nil, errors.New(http.StatusText(http.StatusBadGateway)) + }, + }). + Build(), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + err := c.DeleteAccount(context.Background(), "research") + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.DeleteAccount() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/pkg/client/api/v0044/association.go b/pkg/client/api/v0044/association.go new file mode 100644 index 0000000..8072b00 --- /dev/null +++ b/pkg/client/api/v0044/association.go @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0044 + +import ( + "context" + "errors" + "net/http" + + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0044" + "github.com/SlinkyProject/slurm-client/pkg/types" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +type AssocInterface interface { + CreateAssoc(ctx context.Context, req any) error + DeleteAssoc(ctx context.Context, assoc api.V0044Assoc) error + ListAssoc(ctx context.Context) (*types.V0044AssocList, error) +} + +var _ AssocInterface = &SlurmClient{} + +// CreateAssoc implements AssocInterface (POST is upsert). +func (c *SlurmClient) CreateAssoc(ctx context.Context, req any) error { + r, ok := req.(api.V0044Assoc) + if !ok { + return errors.New("expected req to be api.V0044Assoc") + } + body := api.SlurmdbV0044PostAssociationsJSONRequestBody{ + Associations: api.V0044AssocList{r}, + } + res, err := c.SlurmdbV0044PostAssociationsWithResponse(ctx, body) + if err != nil { + return err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return utilerrors.NewAggregate(errs) + } + return nil +} + +// DeleteAssoc implements AssocInterface using a filter param set. +func (c *SlurmClient) DeleteAssoc(ctx context.Context, assoc api.V0044Assoc) error { + params := &api.SlurmdbV0044DeleteAssociationParams{ + Account: assoc.Account, + User: ptr.To(assoc.User), + Cluster: assoc.Cluster, + Partition: assoc.Partition, + } + res, err := c.SlurmdbV0044DeleteAssociationWithResponse(ctx, params) + if err != nil { + return err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return utilerrors.NewAggregate(errs) + } + return nil +} + +// ListAssoc implements AssocInterface. +func (c *SlurmClient) ListAssoc(ctx context.Context) (*types.V0044AssocList, error) { + params := &api.SlurmdbV0044GetAssociationsParams{} + res, err := c.SlurmdbV0044GetAssociationsWithResponse(ctx, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + list := &types.V0044AssocList{ + Items: make([]types.V0044Assoc, len(res.JSON200.Associations)), + } + for i, item := range res.JSON200.Associations { + utils.RemarshalOrDie(item, &list.Items[i]) + } + return list, nil +} diff --git a/pkg/client/api/v0044/association_test.go b/pkg/client/api/v0044/association_test.go new file mode 100644 index 0000000..04f6636 --- /dev/null +++ b/pkg/client/api/v0044/association_test.go @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0044 + +import ( + "context" + "errors" + "net/http" + "reflect" + "testing" + + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0044" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0044/fake" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0044/interceptor" + "github.com/SlinkyProject/slurm-client/pkg/types" +) + +func TestAssocInterface_Compiles(t *testing.T) { + var _ AssocInterface = &SlurmClient{} +} + +func TestSlurmClient_ListAssoc(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + want *types.V0044AssocList + wantErr bool + }{ + { + name: "Empty list", + client: fake.NewFakeClient(), + want: &types.V0044AssocList{Items: make([]types.V0044Assoc, 0)}, + wantErr: false, + }, + { + name: "Non-empty list", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0044GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0044GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0044GetAssociationsResponse, error) { + return &api.SlurmdbV0044GetAssociationsResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0044OpenapiAssocsResp{ + Associations: api.V0044AssocList{ + {User: "alice", Account: ptr.To("research")}, + }, + }, + }, nil + }, + }). + Build(), + want: &types.V0044AssocList{Items: []types.V0044Assoc{ + {V0044Assoc: api.V0044Assoc{User: "alice", Account: ptr.To("research")}}, + }}, + wantErr: false, + }, + { + name: "HTTP Error", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0044GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0044GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0044GetAssociationsResponse, error) { + return nil, errors.New(http.StatusText(http.StatusBadGateway)) + }, + }). + Build(), + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.ListAssoc(context.Background()) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.ListAssoc() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.ListAssoc() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_CreateAssoc(t *testing.T) { + tests := []struct { + name string + req any + wantErr bool + }{ + {name: "default", req: api.V0044Assoc{User: "alice", Account: ptr.To("research")}, wantErr: false}, + {name: "invalid type provided", req: api.V0044Account{Name: "research"}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + err := c.CreateAssoc(context.Background(), tt.req) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.CreateAssoc() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestSlurmClient_DeleteAssoc(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + err := c.DeleteAssoc(context.Background(), api.V0044Assoc{User: "alice", Account: ptr.To("research")}) + if err != nil { + t.Errorf("SlurmClient.DeleteAssoc() error = %v, wantErr false", err) + } +} diff --git a/pkg/client/api/v0044/user.go b/pkg/client/api/v0044/user.go new file mode 100644 index 0000000..11daf3a --- /dev/null +++ b/pkg/client/api/v0044/user.go @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0044 + +import ( + "context" + "errors" + "net/http" + + utilerrors "k8s.io/apimachinery/pkg/util/errors" + + api "github.com/SlinkyProject/slurm-client/api/v0044" + "github.com/SlinkyProject/slurm-client/pkg/types" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +type UserInterface interface { + CreateUser(ctx context.Context, req any) (string, error) + UpdateUser(ctx context.Context, name string, req any) error + DeleteUser(ctx context.Context, name string) error + GetUser(ctx context.Context, name string) (*types.V0044User, error) + ListUser(ctx context.Context) (*types.V0044UserList, error) +} + +var _ UserInterface = &SlurmClient{} + +// CreateUser implements UserInterface. +func (c *SlurmClient) CreateUser(ctx context.Context, req any) (string, error) { + r, ok := req.(api.V0044User) + if !ok { + return "", errors.New("expected req to be api.V0044User") + } + body := api.SlurmdbV0044PostUsersJSONRequestBody{ + Users: api.V0044UserList{r}, + } + res, err := c.SlurmdbV0044PostUsersWithResponse(ctx, body) + if err != nil { + return "", err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return "", utilerrors.NewAggregate(errs) + } + return r.Name, nil +} + +// UpdateUser implements UserInterface. POST is upsert in slurmdbd. +func (c *SlurmClient) UpdateUser(ctx context.Context, name string, req any) error { + r, ok := req.(api.V0044User) + if !ok { + return errors.New("expected req to be api.V0044User") + } + r.Name = name + _, err := c.CreateUser(ctx, r) + return err +} + +// DeleteUser implements UserInterface. +func (c *SlurmClient) DeleteUser(ctx context.Context, name string) error { + res, err := c.SlurmdbV0044DeleteUserWithResponse(ctx, name) + if err != nil { + return err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return utilerrors.NewAggregate(errs) + } + return nil +} + +// GetUser implements UserInterface. +func (c *SlurmClient) GetUser(ctx context.Context, name string) (*types.V0044User, error) { + params := &api.SlurmdbV0044GetUserParams{} + res, err := c.SlurmdbV0044GetUserWithResponse(ctx, name, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + if res.JSON200 == nil || len(res.JSON200.Users) == 0 { + return nil, errors.New(http.StatusText(http.StatusNotFound)) + } + out := &types.V0044User{} + utils.RemarshalOrDie(res.JSON200.Users[0], out) + return out, nil +} + +// ListUser implements UserInterface. +func (c *SlurmClient) ListUser(ctx context.Context) (*types.V0044UserList, error) { + params := &api.SlurmdbV0044GetUsersParams{} + res, err := c.SlurmdbV0044GetUsersWithResponse(ctx, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + list := &types.V0044UserList{ + Items: make([]types.V0044User, len(res.JSON200.Users)), + } + for i, item := range res.JSON200.Users { + utils.RemarshalOrDie(item, &list.Items[i]) + } + return list, nil +} diff --git a/pkg/client/api/v0044/user_test.go b/pkg/client/api/v0044/user_test.go new file mode 100644 index 0000000..abd1e3b --- /dev/null +++ b/pkg/client/api/v0044/user_test.go @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0044 + +import ( + "context" + "errors" + "net/http" + "reflect" + "testing" + + api "github.com/SlinkyProject/slurm-client/api/v0044" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0044/fake" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0044/interceptor" + "github.com/SlinkyProject/slurm-client/pkg/types" +) + +func TestUserInterface_Compiles(t *testing.T) { + var _ UserInterface = &SlurmClient{} +} + +func TestSlurmClient_GetUser(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + want *types.V0044User + wantErr bool + }{ + { + name: "Not Found", + client: fake.NewFakeClient(), + want: nil, + wantErr: true, + }, + { + name: "Found", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0044GetUserWithResponse: func(ctx context.Context, name string, params *api.SlurmdbV0044GetUserParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0044GetUserResponse, error) { + return &api.SlurmdbV0044GetUserResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0044OpenapiUsersResp{ + Users: api.V0044UserList{{Name: "alice"}}, + }, + }, nil + }, + }). + Build(), + want: &types.V0044User{V0044User: api.V0044User{Name: "alice"}}, + wantErr: false, + }, + { + name: "HTTP Error", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0044GetUserWithResponse: func(ctx context.Context, name string, params *api.SlurmdbV0044GetUserParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0044GetUserResponse, error) { + return nil, errors.New(http.StatusText(http.StatusBadGateway)) + }, + }). + Build(), + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.GetUser(context.Background(), "alice") + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.GetUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.GetUser() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_ListUser(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + want *types.V0044UserList + wantErr bool + }{ + { + name: "Empty list", + client: fake.NewFakeClient(), + want: &types.V0044UserList{Items: make([]types.V0044User, 0)}, + wantErr: false, + }, + { + name: "Non-empty list", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0044GetUsersWithResponse: func(ctx context.Context, params *api.SlurmdbV0044GetUsersParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0044GetUsersResponse, error) { + return &api.SlurmdbV0044GetUsersResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0044OpenapiUsersResp{ + Users: api.V0044UserList{{Name: "alice"}, {Name: "bob"}}, + }, + }, nil + }, + }). + Build(), + want: &types.V0044UserList{Items: []types.V0044User{ + {V0044User: api.V0044User{Name: "alice"}}, + {V0044User: api.V0044User{Name: "bob"}}, + }}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.ListUser(context.Background()) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.ListUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.ListUser() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_CreateUser(t *testing.T) { + tests := []struct { + name string + req any + want string + wantErr bool + }{ + {name: "default", req: api.V0044User{Name: "alice"}, want: "alice", wantErr: false}, + {name: "invalid type provided", req: api.V0044Account{Name: "alice"}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + got, err := c.CreateUser(context.Background(), tt.req) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.CreateUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("SlurmClient.CreateUser() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_DeleteUser(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + if err := c.DeleteUser(context.Background(), "alice"); err != nil { + t.Errorf("SlurmClient.DeleteUser() error = %v, wantErr false", err) + } +} diff --git a/pkg/client/api/v0044/v0044.go b/pkg/client/api/v0044/v0044.go index 194eeab..a0c56ee 100644 --- a/pkg/client/api/v0044/v0044.go +++ b/pkg/client/api/v0044/v0044.go @@ -24,6 +24,8 @@ const ( type ClientInterface interface { api.ClientWithResponsesInterface + AccountInterface + AssocInterface ControllerPingInfoInterface JobInfoInterface NodeInterface @@ -32,6 +34,7 @@ type ClientInterface interface { ReconfigureInterface ReservationInterface StatsInterface + UserInterface } type SlurmClient struct { diff --git a/pkg/client/api/v0045/account.go b/pkg/client/api/v0045/account.go new file mode 100644 index 0000000..472e33e --- /dev/null +++ b/pkg/client/api/v0045/account.go @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0045 + +import ( + "context" + "errors" + "net/http" + + utilerrors "k8s.io/apimachinery/pkg/util/errors" + + api "github.com/SlinkyProject/slurm-client/api/v0045" + "github.com/SlinkyProject/slurm-client/pkg/types" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +type AccountInterface interface { + CreateAccount(ctx context.Context, req any) (string, error) + UpdateAccount(ctx context.Context, name string, req any) error + DeleteAccount(ctx context.Context, name string) error + GetAccount(ctx context.Context, name string) (*types.V0045Account, error) + ListAccount(ctx context.Context) (*types.V0045AccountList, error) +} + +var _ AccountInterface = &SlurmClient{} + +// CreateAccount implements AccountInterface. +func (c *SlurmClient) CreateAccount(ctx context.Context, req any) (string, error) { + r, ok := req.(api.V0045Account) + if !ok { + return "", errors.New("expected req to be api.V0045Account") + } + body := api.SlurmdbV0045PostAccountsJSONRequestBody{ + Accounts: api.V0045AccountList{r}, + } + res, err := c.SlurmdbV0045PostAccountsWithResponse(ctx, body) + if err != nil { + return "", err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return "", utilerrors.NewAggregate(errs) + } + return r.Name, nil +} + +// UpdateAccount implements AccountInterface. POST is upsert in slurmdbd. +func (c *SlurmClient) UpdateAccount(ctx context.Context, name string, req any) error { + r, ok := req.(api.V0045Account) + if !ok { + return errors.New("expected req to be api.V0045Account") + } + r.Name = name + _, err := c.CreateAccount(ctx, r) + return err +} + +// DeleteAccount implements AccountInterface. +func (c *SlurmClient) DeleteAccount(ctx context.Context, name string) error { + res, err := c.SlurmdbV0045DeleteAccountWithResponse(ctx, name) + if err != nil { + return err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return utilerrors.NewAggregate(errs) + } + return nil +} + +// GetAccount implements AccountInterface. +func (c *SlurmClient) GetAccount(ctx context.Context, name string) (*types.V0045Account, error) { + params := &api.SlurmdbV0045GetAccountParams{} + res, err := c.SlurmdbV0045GetAccountWithResponse(ctx, name, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + if res.JSON200 == nil || len(res.JSON200.Accounts) == 0 { + return nil, errors.New(http.StatusText(http.StatusNotFound)) + } + out := &types.V0045Account{} + utils.RemarshalOrDie(res.JSON200.Accounts[0], out) + return out, nil +} + +// ListAccount implements AccountInterface. +func (c *SlurmClient) ListAccount(ctx context.Context) (*types.V0045AccountList, error) { + params := &api.SlurmdbV0045GetAccountsParams{} + res, err := c.SlurmdbV0045GetAccountsWithResponse(ctx, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + list := &types.V0045AccountList{ + Items: make([]types.V0045Account, len(res.JSON200.Accounts)), + } + for i, item := range res.JSON200.Accounts { + utils.RemarshalOrDie(item, &list.Items[i]) + } + return list, nil +} diff --git a/pkg/client/api/v0045/account_test.go b/pkg/client/api/v0045/account_test.go new file mode 100644 index 0000000..a634fb8 --- /dev/null +++ b/pkg/client/api/v0045/account_test.go @@ -0,0 +1,182 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0045 + +import ( + "context" + "errors" + "net/http" + "reflect" + "testing" + + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0045" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0045/fake" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0045/interceptor" + "github.com/SlinkyProject/slurm-client/pkg/types" +) + +func TestAccountInterface_Compiles(t *testing.T) { + var _ AccountInterface = &SlurmClient{} +} + +func TestSlurmClient_GetAccount(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + want *types.V0045Account + wantErr bool + }{ + { + name: "Not Found", + client: fake.NewFakeClient(), + want: nil, + wantErr: true, + }, + { + name: "Found", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0045GetAccountWithResponse: func(ctx context.Context, accountName string, params *api.SlurmdbV0045GetAccountParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0045GetAccountResponse, error) { + return &api.SlurmdbV0045GetAccountResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0045OpenapiAccountsResp{ + Accounts: api.V0045AccountList{{Name: "research"}}, + }, + }, nil + }, + }). + Build(), + want: &types.V0045Account{V0045Account: api.V0045Account{Name: "research"}}, + wantErr: false, + }, + { + name: "HTTP Status != 200", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0045GetAccountWithResponse: func(ctx context.Context, accountName string, params *api.SlurmdbV0045GetAccountParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0045GetAccountResponse, error) { + return &api.SlurmdbV0045GetAccountResponse{ + HTTPResponse: &http.Response{ + Status: http.StatusText(http.StatusInternalServerError), + StatusCode: http.StatusInternalServerError, + }, + JSONDefault: &api.V0045OpenapiAccountsResp{ + Errors: &[]api.V0045OpenapiError{{Error: ptr.To("error 1")}}, + }, + }, nil + }, + }). + Build(), + want: nil, + wantErr: true, + }, + { + name: "HTTP Error", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0045GetAccountWithResponse: func(ctx context.Context, accountName string, params *api.SlurmdbV0045GetAccountParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0045GetAccountResponse, error) { + return nil, errors.New(http.StatusText(http.StatusBadGateway)) + }, + }). + Build(), + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.GetAccount(context.Background(), "research") + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.GetAccount() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.GetAccount() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_ListAccount(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + want *types.V0045AccountList + wantErr bool + }{ + { + name: "Empty list", + client: fake.NewFakeClient(), + want: &types.V0045AccountList{Items: make([]types.V0045Account, 0)}, + wantErr: false, + }, + { + name: "Non-empty list", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0045GetAccountsWithResponse: func(ctx context.Context, params *api.SlurmdbV0045GetAccountsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0045GetAccountsResponse, error) { + return &api.SlurmdbV0045GetAccountsResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0045OpenapiAccountsResp{ + Accounts: api.V0045AccountList{{Name: "a"}, {Name: "b"}}, + }, + }, nil + }, + }). + Build(), + want: &types.V0045AccountList{Items: []types.V0045Account{ + {V0045Account: api.V0045Account{Name: "a"}}, + {V0045Account: api.V0045Account{Name: "b"}}, + }}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.ListAccount(context.Background()) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.ListAccount() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.ListAccount() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_CreateAccount(t *testing.T) { + tests := []struct { + name string + req any + want string + wantErr bool + }{ + {name: "default", req: api.V0045Account{Name: "research"}, want: "research", wantErr: false}, + {name: "invalid type provided", req: api.V0045User{Name: "research"}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + got, err := c.CreateAccount(context.Background(), tt.req) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.CreateAccount() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("SlurmClient.CreateAccount() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_DeleteAccount(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + if err := c.DeleteAccount(context.Background(), "research"); err != nil { + t.Errorf("SlurmClient.DeleteAccount() error = %v, wantErr false", err) + } +} diff --git a/pkg/client/api/v0045/association.go b/pkg/client/api/v0045/association.go new file mode 100644 index 0000000..7ae94d2 --- /dev/null +++ b/pkg/client/api/v0045/association.go @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0045 + +import ( + "context" + "errors" + "net/http" + + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0045" + "github.com/SlinkyProject/slurm-client/pkg/types" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +type AssocInterface interface { + CreateAssoc(ctx context.Context, req any) error + DeleteAssoc(ctx context.Context, assoc api.V0045Assoc) error + ListAssoc(ctx context.Context) (*types.V0045AssocList, error) +} + +var _ AssocInterface = &SlurmClient{} + +// CreateAssoc implements AssocInterface (POST is upsert). +func (c *SlurmClient) CreateAssoc(ctx context.Context, req any) error { + r, ok := req.(api.V0045Assoc) + if !ok { + return errors.New("expected req to be api.V0045Assoc") + } + body := api.SlurmdbV0045PostAssociationsJSONRequestBody{ + Associations: api.V0045AssocList{r}, + } + res, err := c.SlurmdbV0045PostAssociationsWithResponse(ctx, body) + if err != nil { + return err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return utilerrors.NewAggregate(errs) + } + return nil +} + +// DeleteAssoc implements AssocInterface using a filter param set. +func (c *SlurmClient) DeleteAssoc(ctx context.Context, assoc api.V0045Assoc) error { + params := &api.SlurmdbV0045DeleteAssociationParams{ + Account: assoc.Account, + User: ptr.To(assoc.User), + Cluster: assoc.Cluster, + Partition: assoc.Partition, + } + res, err := c.SlurmdbV0045DeleteAssociationWithResponse(ctx, params) + if err != nil { + return err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return utilerrors.NewAggregate(errs) + } + return nil +} + +// ListAssoc implements AssocInterface. +func (c *SlurmClient) ListAssoc(ctx context.Context) (*types.V0045AssocList, error) { + params := &api.SlurmdbV0045GetAssociationsParams{} + res, err := c.SlurmdbV0045GetAssociationsWithResponse(ctx, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + list := &types.V0045AssocList{ + Items: make([]types.V0045Assoc, len(res.JSON200.Associations)), + } + for i, item := range res.JSON200.Associations { + utils.RemarshalOrDie(item, &list.Items[i]) + } + return list, nil +} diff --git a/pkg/client/api/v0045/association_test.go b/pkg/client/api/v0045/association_test.go new file mode 100644 index 0000000..ddbd657 --- /dev/null +++ b/pkg/client/api/v0045/association_test.go @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0045 + +import ( + "context" + "errors" + "net/http" + "reflect" + "testing" + + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0045" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0045/fake" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0045/interceptor" + "github.com/SlinkyProject/slurm-client/pkg/types" +) + +func TestAssocInterface_Compiles(t *testing.T) { + var _ AssocInterface = &SlurmClient{} +} + +func TestSlurmClient_ListAssoc(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + want *types.V0045AssocList + wantErr bool + }{ + { + name: "Empty list", + client: fake.NewFakeClient(), + want: &types.V0045AssocList{Items: make([]types.V0045Assoc, 0)}, + wantErr: false, + }, + { + name: "Non-empty list", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0045GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0045GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0045GetAssociationsResponse, error) { + return &api.SlurmdbV0045GetAssociationsResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0045OpenapiAssocsResp{ + Associations: api.V0045AssocList{ + {User: "alice", Account: ptr.To("research")}, + }, + }, + }, nil + }, + }). + Build(), + want: &types.V0045AssocList{Items: []types.V0045Assoc{ + {V0045Assoc: api.V0045Assoc{User: "alice", Account: ptr.To("research")}}, + }}, + wantErr: false, + }, + { + name: "HTTP Error", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0045GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0045GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0045GetAssociationsResponse, error) { + return nil, errors.New(http.StatusText(http.StatusBadGateway)) + }, + }). + Build(), + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.ListAssoc(context.Background()) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.ListAssoc() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.ListAssoc() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_CreateAssoc(t *testing.T) { + tests := []struct { + name string + req any + wantErr bool + }{ + {name: "default", req: api.V0045Assoc{User: "alice", Account: ptr.To("research")}, wantErr: false}, + {name: "invalid type provided", req: api.V0045Account{Name: "research"}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + err := c.CreateAssoc(context.Background(), tt.req) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.CreateAssoc() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestSlurmClient_DeleteAssoc(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + err := c.DeleteAssoc(context.Background(), api.V0045Assoc{User: "alice", Account: ptr.To("research")}) + if err != nil { + t.Errorf("SlurmClient.DeleteAssoc() error = %v, wantErr false", err) + } +} diff --git a/pkg/client/api/v0045/user.go b/pkg/client/api/v0045/user.go new file mode 100644 index 0000000..c579eed --- /dev/null +++ b/pkg/client/api/v0045/user.go @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0045 + +import ( + "context" + "errors" + "net/http" + + utilerrors "k8s.io/apimachinery/pkg/util/errors" + + api "github.com/SlinkyProject/slurm-client/api/v0045" + "github.com/SlinkyProject/slurm-client/pkg/types" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +type UserInterface interface { + CreateUser(ctx context.Context, req any) (string, error) + UpdateUser(ctx context.Context, name string, req any) error + DeleteUser(ctx context.Context, name string) error + GetUser(ctx context.Context, name string) (*types.V0045User, error) + ListUser(ctx context.Context) (*types.V0045UserList, error) +} + +var _ UserInterface = &SlurmClient{} + +// CreateUser implements UserInterface. +func (c *SlurmClient) CreateUser(ctx context.Context, req any) (string, error) { + r, ok := req.(api.V0045User) + if !ok { + return "", errors.New("expected req to be api.V0045User") + } + body := api.SlurmdbV0045PostUsersJSONRequestBody{ + Users: api.V0045UserList{r}, + } + res, err := c.SlurmdbV0045PostUsersWithResponse(ctx, body) + if err != nil { + return "", err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return "", utilerrors.NewAggregate(errs) + } + return r.Name, nil +} + +// UpdateUser implements UserInterface. POST is upsert in slurmdbd. +func (c *SlurmClient) UpdateUser(ctx context.Context, name string, req any) error { + r, ok := req.(api.V0045User) + if !ok { + return errors.New("expected req to be api.V0045User") + } + r.Name = name + _, err := c.CreateUser(ctx, r) + return err +} + +// DeleteUser implements UserInterface. +func (c *SlurmClient) DeleteUser(ctx context.Context, name string) error { + res, err := c.SlurmdbV0045DeleteUserWithResponse(ctx, name) + if err != nil { + return err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return utilerrors.NewAggregate(errs) + } + return nil +} + +// GetUser implements UserInterface. +func (c *SlurmClient) GetUser(ctx context.Context, name string) (*types.V0045User, error) { + params := &api.SlurmdbV0045GetUserParams{} + res, err := c.SlurmdbV0045GetUserWithResponse(ctx, name, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + if res.JSON200 == nil || len(res.JSON200.Users) == 0 { + return nil, errors.New(http.StatusText(http.StatusNotFound)) + } + out := &types.V0045User{} + utils.RemarshalOrDie(res.JSON200.Users[0], out) + return out, nil +} + +// ListUser implements UserInterface. +func (c *SlurmClient) ListUser(ctx context.Context) (*types.V0045UserList, error) { + params := &api.SlurmdbV0045GetUsersParams{} + res, err := c.SlurmdbV0045GetUsersWithResponse(ctx, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + list := &types.V0045UserList{ + Items: make([]types.V0045User, len(res.JSON200.Users)), + } + for i, item := range res.JSON200.Users { + utils.RemarshalOrDie(item, &list.Items[i]) + } + return list, nil +} diff --git a/pkg/client/api/v0045/user_test.go b/pkg/client/api/v0045/user_test.go new file mode 100644 index 0000000..fa4e265 --- /dev/null +++ b/pkg/client/api/v0045/user_test.go @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package v0045 + +import ( + "context" + "errors" + "net/http" + "reflect" + "testing" + + api "github.com/SlinkyProject/slurm-client/api/v0045" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0045/fake" + "github.com/SlinkyProject/slurm-client/pkg/client/api/v0045/interceptor" + "github.com/SlinkyProject/slurm-client/pkg/types" +) + +func TestUserInterface_Compiles(t *testing.T) { + var _ UserInterface = &SlurmClient{} +} + +func TestSlurmClient_GetUser(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + want *types.V0045User + wantErr bool + }{ + { + name: "Not Found", + client: fake.NewFakeClient(), + want: nil, + wantErr: true, + }, + { + name: "Found", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0045GetUserWithResponse: func(ctx context.Context, name string, params *api.SlurmdbV0045GetUserParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0045GetUserResponse, error) { + return &api.SlurmdbV0045GetUserResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0045OpenapiUsersResp{ + Users: api.V0045UserList{{Name: "alice"}}, + }, + }, nil + }, + }). + Build(), + want: &types.V0045User{V0045User: api.V0045User{Name: "alice"}}, + wantErr: false, + }, + { + name: "HTTP Error", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0045GetUserWithResponse: func(ctx context.Context, name string, params *api.SlurmdbV0045GetUserParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0045GetUserResponse, error) { + return nil, errors.New(http.StatusText(http.StatusBadGateway)) + }, + }). + Build(), + want: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.GetUser(context.Background(), "alice") + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.GetUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.GetUser() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_ListUser(t *testing.T) { + tests := []struct { + name string + client api.ClientWithResponsesInterface + want *types.V0045UserList + wantErr bool + }{ + { + name: "Empty list", + client: fake.NewFakeClient(), + want: &types.V0045UserList{Items: make([]types.V0045User, 0)}, + wantErr: false, + }, + { + name: "Non-empty list", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0045GetUsersWithResponse: func(ctx context.Context, params *api.SlurmdbV0045GetUsersParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0045GetUsersResponse, error) { + return &api.SlurmdbV0045GetUsersResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0045OpenapiUsersResp{ + Users: api.V0045UserList{{Name: "alice"}, {Name: "bob"}}, + }, + }, nil + }, + }). + Build(), + want: &types.V0045UserList{Items: []types.V0045User{ + {V0045User: api.V0045User{Name: "alice"}}, + {V0045User: api.V0045User{Name: "bob"}}, + }}, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.ListUser(context.Background()) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.ListUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.ListUser() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_CreateUser(t *testing.T) { + tests := []struct { + name string + req any + want string + wantErr bool + }{ + {name: "default", req: api.V0045User{Name: "alice"}, want: "alice", wantErr: false}, + {name: "invalid type provided", req: api.V0045Account{Name: "alice"}, wantErr: true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + got, err := c.CreateUser(context.Background(), tt.req) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.CreateUser() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("SlurmClient.CreateUser() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_DeleteUser(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} + if err := c.DeleteUser(context.Background(), "alice"); err != nil { + t.Errorf("SlurmClient.DeleteUser() error = %v, wantErr false", err) + } +} diff --git a/pkg/client/api/v0045/v0045.go b/pkg/client/api/v0045/v0045.go index 1a0774c..51d5e1a 100644 --- a/pkg/client/api/v0045/v0045.go +++ b/pkg/client/api/v0045/v0045.go @@ -24,6 +24,8 @@ const ( type ClientInterface interface { api.ClientWithResponsesInterface + AccountInterface + AssocInterface ControllerPingInfoInterface JobInfoInterface NodeInterface @@ -32,6 +34,7 @@ type ClientInterface interface { ReconfigureInterface ReservationInterface StatsInterface + UserInterface } type SlurmClient struct { diff --git a/pkg/client/client.go b/pkg/client/client.go index 4f95d61..f56e606 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -163,6 +163,20 @@ func (c *client) Create( jobId, err = c.v0042Client.CreateJobInfo(ctx, req) key = object.ObjectKey(fmt.Sprintf("%d", ptr.Deref(jobId, 0))) + case *types.V0042Account: + var name string + name, err = c.v0042Client.CreateAccount(ctx, req) + key = object.ObjectKey(name) + + case *types.V0042User: + var name string + name, err = c.v0042Client.CreateUser(ctx, req) + key = object.ObjectKey(name) + + case *types.V0042Assoc: + err = c.v0042Client.CreateAssoc(ctx, req) + key = obj.GetKey() + ///////////////////////////////////////////////////////////////////////////////// case *types.V0043JobInfo: @@ -170,6 +184,20 @@ func (c *client) Create( jobId, err = c.v0043Client.CreateJobInfo(ctx, req) key = object.ObjectKey(fmt.Sprintf("%d", ptr.Deref(jobId, 0))) + case *types.V0043Account: + var name string + name, err = c.v0043Client.CreateAccount(ctx, req) + key = object.ObjectKey(name) + + case *types.V0043User: + var name string + name, err = c.v0043Client.CreateUser(ctx, req) + key = object.ObjectKey(name) + + case *types.V0043Assoc: + err = c.v0043Client.CreateAssoc(ctx, req) + key = obj.GetKey() + ///////////////////////////////////////////////////////////////////////////////// case *types.V0044JobInfo: @@ -187,6 +215,20 @@ func (c *client) Create( nodeName, err = c.v0044Client.CreateNewNode(ctx, req) key = object.ObjectKey(ptr.Deref(nodeName, "")) + case *types.V0044Account: + var name string + name, err = c.v0044Client.CreateAccount(ctx, req) + key = object.ObjectKey(name) + + case *types.V0044User: + var name string + name, err = c.v0044Client.CreateUser(ctx, req) + key = object.ObjectKey(name) + + case *types.V0044Assoc: + err = c.v0044Client.CreateAssoc(ctx, req) + key = obj.GetKey() + ///////////////////////////////////////////////////////////////////////////////// case *types.V0045JobInfo: @@ -204,6 +246,20 @@ func (c *client) Create( nodeName, err = c.v0045Client.CreateNewNode(ctx, req) key = object.ObjectKey(ptr.Deref(nodeName, "")) + case *types.V0045Account: + var name string + name, err = c.v0045Client.CreateAccount(ctx, req) + key = object.ObjectKey(name) + + case *types.V0045User: + var name string + name, err = c.v0045Client.CreateUser(ctx, req) + key = object.ObjectKey(name) + + case *types.V0045Assoc: + err = c.v0045Client.CreateAssoc(ctx, req) + key = obj.GetKey() + ///////////////////////////////////////////////////////////////////////////////// default: @@ -229,13 +285,19 @@ func (c *client) Delete( var err error key := string(obj.GetKey()) - switch obj.(type) { + switch o := obj.(type) { ///////////////////////////////////////////////////////////////////////////////// case *types.V0042JobInfo: err = c.v0042Client.DeleteJobInfo(ctx, key) case *types.V0042Node: err = c.v0042Client.DeleteNode(ctx, key) + case *types.V0042Account: + err = c.v0042Client.DeleteAccount(ctx, key) + case *types.V0042User: + err = c.v0042Client.DeleteUser(ctx, key) + case *types.V0042Assoc: + err = c.v0042Client.DeleteAssoc(ctx, o.V0042Assoc) ///////////////////////////////////////////////////////////////////////////////// @@ -243,6 +305,12 @@ func (c *client) Delete( err = c.v0043Client.DeleteJobInfo(ctx, key) case *types.V0043Node: err = c.v0043Client.DeleteNode(ctx, key) + case *types.V0043Account: + err = c.v0043Client.DeleteAccount(ctx, key) + case *types.V0043User: + err = c.v0043Client.DeleteUser(ctx, key) + case *types.V0043Assoc: + err = c.v0043Client.DeleteAssoc(ctx, o.V0043Assoc) ///////////////////////////////////////////////////////////////////////////////// @@ -252,6 +320,12 @@ func (c *client) Delete( err = c.v0044Client.DeleteNode(ctx, key) case *types.V0044ReservationInfo: err = c.v0044Client.DeleteReservationInfo(ctx, key) + case *types.V0044Account: + err = c.v0044Client.DeleteAccount(ctx, key) + case *types.V0044User: + err = c.v0044Client.DeleteUser(ctx, key) + case *types.V0044Assoc: + err = c.v0044Client.DeleteAssoc(ctx, o.V0044Assoc) ///////////////////////////////////////////////////////////////////////////////// @@ -261,6 +335,12 @@ func (c *client) Delete( err = c.v0045Client.DeleteNode(ctx, key) case *types.V0045ReservationInfo: err = c.v0045Client.DeleteReservationInfo(ctx, key) + case *types.V0045Account: + err = c.v0045Client.DeleteAccount(ctx, key) + case *types.V0045User: + err = c.v0045Client.DeleteUser(ctx, key) + case *types.V0045Assoc: + err = c.v0045Client.DeleteAssoc(ctx, o.V0045Assoc) ///////////////////////////////////////////////////////////////////////////////// @@ -305,6 +385,12 @@ func (c *client) Update( err = c.v0042Client.UpdateJobInfo(ctx, key, req) case *types.V0042Node: err = c.v0042Client.UpdateNode(ctx, key, req) + case *types.V0042Account: + err = c.v0042Client.UpdateAccount(ctx, key, req) + case *types.V0042User: + err = c.v0042Client.UpdateUser(ctx, key, req) + case *types.V0042Assoc: + err = c.v0042Client.CreateAssoc(ctx, req) ///////////////////////////////////////////////////////////////////////////////// @@ -312,6 +398,12 @@ func (c *client) Update( err = c.v0043Client.UpdateJobInfo(ctx, key, req) case *types.V0043Node: err = c.v0043Client.UpdateNode(ctx, key, req) + case *types.V0043Account: + err = c.v0043Client.UpdateAccount(ctx, key, req) + case *types.V0043User: + err = c.v0043Client.UpdateUser(ctx, key, req) + case *types.V0043Assoc: + err = c.v0043Client.CreateAssoc(ctx, req) ///////////////////////////////////////////////////////////////////////////////// @@ -321,6 +413,12 @@ func (c *client) Update( err = c.v0044Client.UpdateNode(ctx, key, req) case *types.V0044ReservationInfo: err = c.v0044Client.UpdateReservationInfo(ctx, key, req) + case *types.V0044Account: + err = c.v0044Client.UpdateAccount(ctx, key, req) + case *types.V0044User: + err = c.v0044Client.UpdateUser(ctx, key, req) + case *types.V0044Assoc: + err = c.v0044Client.CreateAssoc(ctx, req) ///////////////////////////////////////////////////////////////////////////////// @@ -330,6 +428,12 @@ func (c *client) Update( err = c.v0045Client.UpdateNode(ctx, key, req) case *types.V0045ReservationInfo: err = c.v0045Client.UpdateReservationInfo(ctx, key, req) + case *types.V0045Account: + err = c.v0045Client.UpdateAccount(ctx, key, req) + case *types.V0045User: + err = c.v0045Client.UpdateUser(ctx, key, req) + case *types.V0045Assoc: + err = c.v0045Client.CreateAssoc(ctx, req) ///////////////////////////////////////////////////////////////////////////////// @@ -397,6 +501,34 @@ func (c *client) Get( return err } *o = *out + case *types.V0042Account: + out, err := c.v0042Client.GetAccount(ctx, string(key)) + if err != nil { + return err + } + *o = *out + case *types.V0042User: + out, err := c.v0042Client.GetUser(ctx, string(key)) + if err != nil { + return err + } + *o = *out + case *types.V0042Assoc: + list, err := c.v0042Client.ListAssoc(ctx) + if err != nil { + return err + } + found := false + for i := range list.Items { + if list.Items[i].GetKey() == key { + *o = list.Items[i] + found = true + break + } + } + if !found { + return errors.New(http.StatusText(http.StatusNotFound)) + } case *types.V0042Stats: out, err := c.v0042Client.GetStats(ctx) if err != nil { @@ -436,6 +568,34 @@ func (c *client) Get( return err } *o = *out + case *types.V0043Account: + out, err := c.v0043Client.GetAccount(ctx, string(key)) + if err != nil { + return err + } + *o = *out + case *types.V0043User: + out, err := c.v0043Client.GetUser(ctx, string(key)) + if err != nil { + return err + } + *o = *out + case *types.V0043Assoc: + list, err := c.v0043Client.ListAssoc(ctx) + if err != nil { + return err + } + found := false + for i := range list.Items { + if list.Items[i].GetKey() == key { + *o = list.Items[i] + found = true + break + } + } + if !found { + return errors.New(http.StatusText(http.StatusNotFound)) + } case *types.V0043Stats: out, err := c.v0043Client.GetStats(ctx) if err != nil { @@ -487,6 +647,34 @@ func (c *client) Get( return err } *o = *out + case *types.V0044Account: + out, err := c.v0044Client.GetAccount(ctx, string(key)) + if err != nil { + return err + } + *o = *out + case *types.V0044User: + out, err := c.v0044Client.GetUser(ctx, string(key)) + if err != nil { + return err + } + *o = *out + case *types.V0044Assoc: + list, err := c.v0044Client.ListAssoc(ctx) + if err != nil { + return err + } + found := false + for i := range list.Items { + if list.Items[i].GetKey() == key { + *o = list.Items[i] + found = true + break + } + } + if !found { + return errors.New(http.StatusText(http.StatusNotFound)) + } case *types.V0044Stats: out, err := c.v0044Client.GetStats(ctx) if err != nil { @@ -538,6 +726,34 @@ func (c *client) Get( return err } *o = *out + case *types.V0045Account: + out, err := c.v0045Client.GetAccount(ctx, string(key)) + if err != nil { + return err + } + *o = *out + case *types.V0045User: + out, err := c.v0045Client.GetUser(ctx, string(key)) + if err != nil { + return err + } + *o = *out + case *types.V0045Assoc: + list, err := c.v0045Client.ListAssoc(ctx) + if err != nil { + return err + } + found := false + for i := range list.Items { + if list.Items[i].GetKey() == key { + *o = list.Items[i] + found = true + break + } + } + if !found { + return errors.New(http.StatusText(http.StatusNotFound)) + } case *types.V0045Stats: out, err := c.v0045Client.GetStats(ctx) if err != nil { @@ -607,6 +823,24 @@ func (c *client) List( return err } *objList = *out + case *types.V0042AccountList: + out, err := c.v0042Client.ListAccount(ctx) + if err != nil { + return err + } + *objList = *out + case *types.V0042UserList: + out, err := c.v0042Client.ListUser(ctx) + if err != nil { + return err + } + *objList = *out + case *types.V0042AssocList: + out, err := c.v0042Client.ListAssoc(ctx) + if err != nil { + return err + } + *objList = *out case *types.V0042StatsList: out, err := c.v0042Client.ListStats(ctx) if err != nil { @@ -646,6 +880,24 @@ func (c *client) List( return err } *objList = *out + case *types.V0043AccountList: + out, err := c.v0043Client.ListAccount(ctx) + if err != nil { + return err + } + *objList = *out + case *types.V0043UserList: + out, err := c.v0043Client.ListUser(ctx) + if err != nil { + return err + } + *objList = *out + case *types.V0043AssocList: + out, err := c.v0043Client.ListAssoc(ctx) + if err != nil { + return err + } + *objList = *out case *types.V0043StatsList: out, err := c.v0043Client.ListStats(ctx) if err != nil { @@ -691,6 +943,24 @@ func (c *client) List( return err } *objList = *out + case *types.V0044AccountList: + out, err := c.v0044Client.ListAccount(ctx) + if err != nil { + return err + } + *objList = *out + case *types.V0044UserList: + out, err := c.v0044Client.ListUser(ctx) + if err != nil { + return err + } + *objList = *out + case *types.V0044AssocList: + out, err := c.v0044Client.ListAssoc(ctx) + if err != nil { + return err + } + *objList = *out case *types.V0044StatsList: out, err := c.v0044Client.ListStats(ctx) if err != nil { @@ -736,6 +1006,24 @@ func (c *client) List( return err } *objList = *out + case *types.V0045AccountList: + out, err := c.v0045Client.ListAccount(ctx) + if err != nil { + return err + } + *objList = *out + case *types.V0045UserList: + out, err := c.v0045Client.ListUser(ctx) + if err != nil { + return err + } + *objList = *out + case *types.V0045AssocList: + out, err := c.v0045Client.ListAssoc(ctx) + if err != nil { + return err + } + *objList = *out case *types.V0045StatsList: out, err := c.v0045Client.ListStats(ctx) if err != nil { diff --git a/pkg/client/fake/client.go b/pkg/client/fake/client.go index 349715d..df9895d 100644 --- a/pkg/client/fake/client.go +++ b/pkg/client/fake/client.go @@ -131,6 +131,15 @@ func (c *fakeClient) Get(ctx context.Context, key object.ObjectKey, obj object.O case *types.V0042PartitionInfo: cache := entry.(*types.V0042PartitionInfo) *o = *cache + case *types.V0042Account: + cache := entry.(*types.V0042Account) + *o = *cache + case *types.V0042User: + cache := entry.(*types.V0042User) + *o = *cache + case *types.V0042Assoc: + cache := entry.(*types.V0042Assoc) + *o = *cache case *types.V0042Stats: cache := entry.(*types.V0042Stats) *o = *cache @@ -149,6 +158,15 @@ func (c *fakeClient) Get(ctx context.Context, key object.ObjectKey, obj object.O case *types.V0043PartitionInfo: cache := entry.(*types.V0043PartitionInfo) *o = *cache + case *types.V0043Account: + cache := entry.(*types.V0043Account) + *o = *cache + case *types.V0043User: + cache := entry.(*types.V0043User) + *o = *cache + case *types.V0043Assoc: + cache := entry.(*types.V0043Assoc) + *o = *cache case *types.V0043Stats: cache := entry.(*types.V0043Stats) *o = *cache @@ -173,6 +191,15 @@ func (c *fakeClient) Get(ctx context.Context, key object.ObjectKey, obj object.O case *types.V0044ReservationInfo: cache := entry.(*types.V0044ReservationInfo) *o = *cache + case *types.V0044Account: + cache := entry.(*types.V0044Account) + *o = *cache + case *types.V0044User: + cache := entry.(*types.V0044User) + *o = *cache + case *types.V0044Assoc: + cache := entry.(*types.V0044Assoc) + *o = *cache case *types.V0044Stats: cache := entry.(*types.V0044Stats) *o = *cache @@ -197,6 +224,15 @@ func (c *fakeClient) Get(ctx context.Context, key object.ObjectKey, obj object.O case *types.V0045ReservationInfo: cache := entry.(*types.V0045ReservationInfo) *o = *cache + case *types.V0045Account: + cache := entry.(*types.V0045Account) + *o = *cache + case *types.V0045User: + cache := entry.(*types.V0045User) + *o = *cache + case *types.V0045Assoc: + cache := entry.(*types.V0045Assoc) + *o = *cache case *types.V0045Stats: cache := entry.(*types.V0045Stats) *o = *cache diff --git a/pkg/client/fake/fake_account_test.go b/pkg/client/fake/fake_account_test.go new file mode 100644 index 0000000..3809c0a --- /dev/null +++ b/pkg/client/fake/fake_account_test.go @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package fake + +import ( + "context" + "testing" + + apiv0042 "github.com/SlinkyProject/slurm-client/api/v0042" + apiv0043 "github.com/SlinkyProject/slurm-client/api/v0043" + apiv0044 "github.com/SlinkyProject/slurm-client/api/v0044" + apiv0045 "github.com/SlinkyProject/slurm-client/api/v0045" + "github.com/SlinkyProject/slurm-client/pkg/client" + "github.com/SlinkyProject/slurm-client/pkg/object" + "github.com/SlinkyProject/slurm-client/pkg/types" +) + +func TestFakeClient_V0042AccountRoundTrip(t *testing.T) { + ctx := context.Background() + acct := &types.V0042Account{V0042Account: apiv0042.V0042Account{Name: "research"}} + c := NewClientBuilder().WithObjects(acct).Build() + + got := &types.V0042Account{} + if err := c.Get(ctx, object.ObjectKey("research"), got, &client.GetOptions{SkipCache: true}); err != nil { + t.Fatalf("Get() error = %v", err) + } + if got.Name != "research" { + t.Errorf("got account %q, want research", got.Name) + } + + list := &types.V0042AccountList{} + if err := c.List(ctx, list); err != nil { + t.Fatalf("List() error = %v", err) + } + if len(list.Items) != 1 { + t.Errorf("List() len = %d, want 1", len(list.Items)) + } +} + +func TestFakeClient_V0043AccountRoundTrip(t *testing.T) { + ctx := context.Background() + acct := &types.V0043Account{V0043Account: apiv0043.V0043Account{Name: "research"}} + c := NewClientBuilder().WithObjects(acct).Build() + + got := &types.V0043Account{} + if err := c.Get(ctx, object.ObjectKey("research"), got, &client.GetOptions{SkipCache: true}); err != nil { + t.Fatalf("Get() error = %v", err) + } + if got.Name != "research" { + t.Errorf("got account %q, want research", got.Name) + } + + list := &types.V0043AccountList{} + if err := c.List(ctx, list); err != nil { + t.Fatalf("List() error = %v", err) + } + if len(list.Items) != 1 { + t.Errorf("List() len = %d, want 1", len(list.Items)) + } +} + +func TestFakeClient_V0044AccountRoundTrip(t *testing.T) { + ctx := context.Background() + acct := &types.V0044Account{V0044Account: apiv0044.V0044Account{Name: "research"}} + c := NewClientBuilder().WithObjects(acct).Build() + + got := &types.V0044Account{} + if err := c.Get(ctx, object.ObjectKey("research"), got, &client.GetOptions{SkipCache: true}); err != nil { + t.Fatalf("Get() error = %v", err) + } + if got.Name != "research" { + t.Errorf("got account %q, want research", got.Name) + } + + list := &types.V0044AccountList{} + if err := c.List(ctx, list); err != nil { + t.Fatalf("List() error = %v", err) + } + if len(list.Items) != 1 { + t.Errorf("List() len = %d, want 1", len(list.Items)) + } +} + +func TestFakeClient_V0045AccountRoundTrip(t *testing.T) { + ctx := context.Background() + acct := &types.V0045Account{V0045Account: apiv0045.V0045Account{Name: "research"}} + c := NewClientBuilder().WithObjects(acct).Build() + + got := &types.V0045Account{} + if err := c.Get(ctx, object.ObjectKey("research"), got, &client.GetOptions{SkipCache: true}); err != nil { + t.Fatalf("Get() error = %v", err) + } + if got.Name != "research" { + t.Errorf("got account %q, want research", got.Name) + } + + list := &types.V0045AccountList{} + if err := c.List(ctx, list); err != nil { + t.Fatalf("List() error = %v", err) + } + if len(list.Items) != 1 { + t.Errorf("List() len = %d, want 1", len(list.Items)) + } +} diff --git a/pkg/client/fake/fake_association_test.go b/pkg/client/fake/fake_association_test.go new file mode 100644 index 0000000..187b1dd --- /dev/null +++ b/pkg/client/fake/fake_association_test.go @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package fake + +import ( + "context" + "testing" + + "k8s.io/utils/ptr" + + apiv0042 "github.com/SlinkyProject/slurm-client/api/v0042" + apiv0043 "github.com/SlinkyProject/slurm-client/api/v0043" + apiv0044 "github.com/SlinkyProject/slurm-client/api/v0044" + apiv0045 "github.com/SlinkyProject/slurm-client/api/v0045" + "github.com/SlinkyProject/slurm-client/pkg/client" + "github.com/SlinkyProject/slurm-client/pkg/types" +) + +func TestFakeClient_V0042AssocRoundTrip(t *testing.T) { + ctx := context.Background() + assoc := &types.V0042Assoc{V0042Assoc: apiv0042.V0042Assoc{ + Cluster: ptr.To("linux"), + Account: ptr.To("research"), + User: "alice", + }} + c := NewClientBuilder().WithObjects(assoc).Build() + + got := &types.V0042Assoc{} + if err := c.Get(ctx, assoc.GetKey(), got, &client.GetOptions{SkipCache: true}); err != nil { + t.Fatalf("Get() error = %v", err) + } + if got.User != "alice" { + t.Errorf("got assoc user %q, want alice", got.User) + } +} + +func TestFakeClient_V0043AssocRoundTrip(t *testing.T) { + ctx := context.Background() + assoc := &types.V0043Assoc{V0043Assoc: apiv0043.V0043Assoc{ + Cluster: ptr.To("linux"), + Account: ptr.To("research"), + User: "alice", + }} + c := NewClientBuilder().WithObjects(assoc).Build() + + got := &types.V0043Assoc{} + if err := c.Get(ctx, assoc.GetKey(), got, &client.GetOptions{SkipCache: true}); err != nil { + t.Fatalf("Get() error = %v", err) + } + if got.User != "alice" { + t.Errorf("got assoc user %q, want alice", got.User) + } +} + +func TestFakeClient_V0044AssocRoundTrip(t *testing.T) { + ctx := context.Background() + assoc := &types.V0044Assoc{V0044Assoc: apiv0044.V0044Assoc{ + Cluster: ptr.To("linux"), + Account: ptr.To("research"), + User: "alice", + }} + c := NewClientBuilder().WithObjects(assoc).Build() + + got := &types.V0044Assoc{} + if err := c.Get(ctx, assoc.GetKey(), got, &client.GetOptions{SkipCache: true}); err != nil { + t.Fatalf("Get() error = %v", err) + } + if got.User != "alice" { + t.Errorf("got assoc user %q, want alice", got.User) + } +} + +func TestFakeClient_V0045AssocRoundTrip(t *testing.T) { + ctx := context.Background() + assoc := &types.V0045Assoc{V0045Assoc: apiv0045.V0045Assoc{ + Cluster: ptr.To("linux"), + Account: ptr.To("research"), + User: "alice", + }} + c := NewClientBuilder().WithObjects(assoc).Build() + + got := &types.V0045Assoc{} + if err := c.Get(ctx, assoc.GetKey(), got, &client.GetOptions{SkipCache: true}); err != nil { + t.Fatalf("Get() error = %v", err) + } + if got.User != "alice" { + t.Errorf("got assoc user %q, want alice", got.User) + } +} diff --git a/pkg/client/fake/fake_user_test.go b/pkg/client/fake/fake_user_test.go new file mode 100644 index 0000000..2271758 --- /dev/null +++ b/pkg/client/fake/fake_user_test.go @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package fake + +import ( + "context" + "testing" + + apiv0042 "github.com/SlinkyProject/slurm-client/api/v0042" + apiv0043 "github.com/SlinkyProject/slurm-client/api/v0043" + apiv0044 "github.com/SlinkyProject/slurm-client/api/v0044" + apiv0045 "github.com/SlinkyProject/slurm-client/api/v0045" + "github.com/SlinkyProject/slurm-client/pkg/client" + "github.com/SlinkyProject/slurm-client/pkg/object" + "github.com/SlinkyProject/slurm-client/pkg/types" +) + +func TestFakeClient_V0042UserRoundTrip(t *testing.T) { + ctx := context.Background() + user := &types.V0042User{V0042User: apiv0042.V0042User{Name: "alice"}} + c := NewClientBuilder().WithObjects(user).Build() + + got := &types.V0042User{} + if err := c.Get(ctx, object.ObjectKey("alice"), got, &client.GetOptions{SkipCache: true}); err != nil { + t.Fatalf("Get() error = %v", err) + } + if got.Name != "alice" { + t.Errorf("got user %q, want alice", got.Name) + } + + list := &types.V0042UserList{} + if err := c.List(ctx, list); err != nil { + t.Fatalf("List() error = %v", err) + } + if len(list.Items) != 1 { + t.Errorf("List() len = %d, want 1", len(list.Items)) + } +} + +func TestFakeClient_V0043UserRoundTrip(t *testing.T) { + ctx := context.Background() + user := &types.V0043User{V0043User: apiv0043.V0043User{Name: "alice"}} + c := NewClientBuilder().WithObjects(user).Build() + + got := &types.V0043User{} + if err := c.Get(ctx, object.ObjectKey("alice"), got, &client.GetOptions{SkipCache: true}); err != nil { + t.Fatalf("Get() error = %v", err) + } + if got.Name != "alice" { + t.Errorf("got user %q, want alice", got.Name) + } + + list := &types.V0043UserList{} + if err := c.List(ctx, list); err != nil { + t.Fatalf("List() error = %v", err) + } + if len(list.Items) != 1 { + t.Errorf("List() len = %d, want 1", len(list.Items)) + } +} + +func TestFakeClient_V0044UserRoundTrip(t *testing.T) { + ctx := context.Background() + user := &types.V0044User{V0044User: apiv0044.V0044User{Name: "alice"}} + c := NewClientBuilder().WithObjects(user).Build() + + got := &types.V0044User{} + if err := c.Get(ctx, object.ObjectKey("alice"), got, &client.GetOptions{SkipCache: true}); err != nil { + t.Fatalf("Get() error = %v", err) + } + if got.Name != "alice" { + t.Errorf("got user %q, want alice", got.Name) + } + + list := &types.V0044UserList{} + if err := c.List(ctx, list); err != nil { + t.Fatalf("List() error = %v", err) + } + if len(list.Items) != 1 { + t.Errorf("List() len = %d, want 1", len(list.Items)) + } +} + +func TestFakeClient_V0045UserRoundTrip(t *testing.T) { + ctx := context.Background() + user := &types.V0045User{V0045User: apiv0045.V0045User{Name: "alice"}} + c := NewClientBuilder().WithObjects(user).Build() + + got := &types.V0045User{} + if err := c.Get(ctx, object.ObjectKey("alice"), got, &client.GetOptions{SkipCache: true}); err != nil { + t.Fatalf("Get() error = %v", err) + } + if got.Name != "alice" { + t.Errorf("got user %q, want alice", got.Name) + } + + list := &types.V0045UserList{} + if err := c.List(ctx, list); err != nil { + t.Fatalf("List() error = %v", err) + } + if len(list.Items) != 1 { + t.Errorf("List() len = %d, want 1", len(list.Items)) + } +} diff --git a/pkg/client/informer.go b/pkg/client/informer.go index 1561c0a..f4c6d72 100644 --- a/pkg/client/informer.go +++ b/pkg/client/informer.go @@ -159,6 +159,12 @@ func (i *informerCache) doListInformer() { list = &types.V0042PartitionInfoList{} case types.ObjectTypeV0042Reconfigure: panic("Reconfigure is not supported, this scenario should have been avoided.") + case types.ObjectTypeV0042Account: + list = &types.V0042AccountList{} + case types.ObjectTypeV0042User: + list = &types.V0042UserList{} + case types.ObjectTypeV0042Assoc: + list = &types.V0042AssocList{} case types.ObjectTypeV0042Stats: list = &types.V0042StatsList{} @@ -174,6 +180,12 @@ func (i *informerCache) doListInformer() { list = &types.V0043PartitionInfoList{} case types.ObjectTypeV0043Reconfigure: panic("Reconfigure is not supported, this scenario should have been avoided.") + case types.ObjectTypeV0043Account: + list = &types.V0043AccountList{} + case types.ObjectTypeV0043User: + list = &types.V0043UserList{} + case types.ObjectTypeV0043Assoc: + list = &types.V0043AssocList{} case types.ObjectTypeV0043Stats: list = &types.V0043StatsList{} @@ -191,6 +203,12 @@ func (i *informerCache) doListInformer() { panic("Reconfigure is not supported, this scenario should have been avoided.") case types.ObjectTypeV0044ReservationInfo: list = &types.V0044ReservationInfoList{} + case types.ObjectTypeV0044Account: + list = &types.V0044AccountList{} + case types.ObjectTypeV0044User: + list = &types.V0044UserList{} + case types.ObjectTypeV0044Assoc: + list = &types.V0044AssocList{} case types.ObjectTypeV0044NodeResourceLayout: panic("NodeResouceLayout is not supported, this scenario should have been avoided.") case types.ObjectTypeV0044Stats: @@ -210,6 +228,12 @@ func (i *informerCache) doListInformer() { panic("Reconfigure is not supported, this scenario should have been avoided.") case types.ObjectTypeV0045ReservationInfo: list = &types.V0045ReservationInfoList{} + case types.ObjectTypeV0045Account: + list = &types.V0045AccountList{} + case types.ObjectTypeV0045User: + list = &types.V0045UserList{} + case types.ObjectTypeV0045Assoc: + list = &types.V0045AssocList{} case types.ObjectTypeV0045NodeResourceLayout: panic("NodeResouceLayout is not supported, this scenario should have been avoided.") case types.ObjectTypeV0045Stats: @@ -292,6 +316,12 @@ func (i *informerCache) doGetInformer(key object.ObjectKey) { obj = &types.V0042PartitionInfo{} case types.ObjectTypeV0042Reconfigure: panic("Reconfigure is not supported, this scenario should have been avoided.") + case types.ObjectTypeV0042Account: + obj = &types.V0042Account{} + case types.ObjectTypeV0042User: + obj = &types.V0042User{} + case types.ObjectTypeV0042Assoc: + obj = &types.V0042Assoc{} case types.ObjectTypeV0042Stats: obj = &types.V0042Stats{} @@ -307,6 +337,12 @@ func (i *informerCache) doGetInformer(key object.ObjectKey) { obj = &types.V0043PartitionInfo{} case types.ObjectTypeV0043Reconfigure: panic("Reconfigure is not supported, this scenario should have been avoided.") + case types.ObjectTypeV0043Account: + obj = &types.V0043Account{} + case types.ObjectTypeV0043User: + obj = &types.V0043User{} + case types.ObjectTypeV0043Assoc: + obj = &types.V0043Assoc{} case types.ObjectTypeV0043Stats: obj = &types.V0043Stats{} @@ -324,6 +360,12 @@ func (i *informerCache) doGetInformer(key object.ObjectKey) { panic("Reconfigure is not supported, this scenario should have been avoided.") case types.ObjectTypeV0044ReservationInfo: obj = &types.V0044ReservationInfo{} + case types.ObjectTypeV0044Account: + obj = &types.V0044Account{} + case types.ObjectTypeV0044User: + obj = &types.V0044User{} + case types.ObjectTypeV0044Assoc: + obj = &types.V0044Assoc{} case types.ObjectTypeV0044Stats: obj = &types.V0044Stats{} @@ -341,6 +383,12 @@ func (i *informerCache) doGetInformer(key object.ObjectKey) { panic("Reconfigure is not supported, this scenario should have been avoided.") case types.ObjectTypeV0045ReservationInfo: obj = &types.V0045ReservationInfo{} + case types.ObjectTypeV0045Account: + obj = &types.V0045Account{} + case types.ObjectTypeV0045User: + obj = &types.V0045User{} + case types.ObjectTypeV0045Assoc: + obj = &types.V0045Assoc{} case types.ObjectTypeV0045Stats: obj = &types.V0045Stats{} @@ -568,6 +616,15 @@ func (i *informerCache) Get(ctx context.Context, key object.ObjectKey, obj objec case *types.V0042PartitionInfo: cache := entry.object.(*types.V0042PartitionInfo) *o = *cache + case *types.V0042Account: + cache := entry.object.(*types.V0042Account) + *o = *cache + case *types.V0042User: + cache := entry.object.(*types.V0042User) + *o = *cache + case *types.V0042Assoc: + cache := entry.object.(*types.V0042Assoc) + *o = *cache case *types.V0042Stats: cache := entry.object.(*types.V0042Stats) *o = *cache @@ -586,6 +643,15 @@ func (i *informerCache) Get(ctx context.Context, key object.ObjectKey, obj objec case *types.V0043PartitionInfo: cache := entry.object.(*types.V0043PartitionInfo) *o = *cache + case *types.V0043Account: + cache := entry.object.(*types.V0043Account) + *o = *cache + case *types.V0043User: + cache := entry.object.(*types.V0043User) + *o = *cache + case *types.V0043Assoc: + cache := entry.object.(*types.V0043Assoc) + *o = *cache case *types.V0043Stats: cache := entry.object.(*types.V0043Stats) *o = *cache @@ -607,6 +673,15 @@ func (i *informerCache) Get(ctx context.Context, key object.ObjectKey, obj objec case *types.V0044ReservationInfo: cache := entry.object.(*types.V0044ReservationInfo) *o = *cache + case *types.V0044Account: + cache := entry.object.(*types.V0044Account) + *o = *cache + case *types.V0044User: + cache := entry.object.(*types.V0044User) + *o = *cache + case *types.V0044Assoc: + cache := entry.object.(*types.V0044Assoc) + *o = *cache case *types.V0044Stats: cache := entry.object.(*types.V0044Stats) *o = *cache @@ -628,6 +703,15 @@ func (i *informerCache) Get(ctx context.Context, key object.ObjectKey, obj objec case *types.V0045ReservationInfo: cache := entry.object.(*types.V0045ReservationInfo) *o = *cache + case *types.V0045Account: + cache := entry.object.(*types.V0045Account) + *o = *cache + case *types.V0045User: + cache := entry.object.(*types.V0045User) + *o = *cache + case *types.V0045Assoc: + cache := entry.object.(*types.V0045Assoc) + *o = *cache case *types.V0045Stats: cache := entry.object.(*types.V0045Stats) *o = *cache diff --git a/pkg/types/V0042Account.go b/pkg/types/V0042Account.go new file mode 100644 index 0000000..52844b7 --- /dev/null +++ b/pkg/types/V0042Account.go @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + api "github.com/SlinkyProject/slurm-client/api/v0042" + "github.com/SlinkyProject/slurm-client/pkg/object" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +const ( + ObjectTypeV0042Account = "V0042Account" +) + +type V0042Account struct { + api.V0042Account +} + +// GetKey implements Object. +func (o *V0042Account) GetKey() object.ObjectKey { + return object.ObjectKey(o.Name) +} + +// GetType implements Object. +func (o *V0042Account) GetType() object.ObjectType { + return ObjectTypeV0042Account +} + +// DeepCopyObject implements Object. +func (o *V0042Account) DeepCopyObject() object.Object { + return o.DeepCopy() +} + +func (o *V0042Account) DeepCopy() *V0042Account { + out := new(V0042Account) + utils.RemarshalOrDie(o, out) + return out +} + +type V0042AccountList struct { + Items []V0042Account +} + +// GetType implements ObjectList. +func (o *V0042AccountList) GetType() object.ObjectType { + return ObjectTypeV0042Account +} + +// GetItems implements ObjectList. +func (o *V0042AccountList) GetItems() []object.Object { + list := make([]object.Object, len(o.Items)) + for i, item := range o.Items { + list[i] = item.DeepCopyObject() + } + return list +} + +// AppendItem implements ObjectList. +func (o *V0042AccountList) AppendItem(obj object.Object) { + out, ok := obj.(*V0042Account) + if ok { + utils.RemarshalOrDie(obj, out) + o.Items = append(o.Items, *out) + } +} + +// DeepCopyObjectList implements ObjectList. +func (o *V0042AccountList) DeepCopyObjectList() object.ObjectList { + out := new(V0042AccountList) + out.Items = make([]V0042Account, len(o.Items)) + for i, item := range o.Items { + out.Items[i] = *item.DeepCopy() + } + return out +} diff --git a/pkg/types/V0042Account_test.go b/pkg/types/V0042Account_test.go new file mode 100644 index 0000000..dd58530 --- /dev/null +++ b/pkg/types/V0042Account_test.go @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "testing" + + api "github.com/SlinkyProject/slurm-client/api/v0042" + "github.com/SlinkyProject/slurm-client/pkg/object" +) + +func TestV0042Account_GetKey(t *testing.T) { + acct := &V0042Account{V0042Account: api.V0042Account{Name: "research"}} + if got := acct.GetKey(); got != object.ObjectKey("research") { + t.Errorf("GetKey() = %q, want %q", got, "research") + } + if got := acct.GetType(); got != ObjectTypeV0042Account { + t.Errorf("GetType() = %q, want %q", got, ObjectTypeV0042Account) + } +} + +func TestV0042AccountList_GetItems(t *testing.T) { + list := &V0042AccountList{Items: []V0042Account{ + {V0042Account: api.V0042Account{Name: "a"}}, + {V0042Account: api.V0042Account{Name: "b"}}, + }} + if len(list.GetItems()) != 2 { + t.Errorf("GetItems() len = %d, want 2", len(list.GetItems())) + } +} diff --git a/pkg/types/V0042Assoc.go b/pkg/types/V0042Assoc.go new file mode 100644 index 0000000..2c6aa5e --- /dev/null +++ b/pkg/types/V0042Assoc.go @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "fmt" + + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0042" + "github.com/SlinkyProject/slurm-client/pkg/object" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +const ( + ObjectTypeV0042Assoc = "V0042Assoc" +) + +type V0042Assoc struct { + api.V0042Assoc +} + +// GetKey implements Object. Key is "cluster/account/user/partition". +func (o *V0042Assoc) GetKey() object.ObjectKey { + return object.ObjectKey(fmt.Sprintf("%s/%s/%s/%s", + ptr.Deref(o.Cluster, ""), + ptr.Deref(o.Account, ""), + o.User, + ptr.Deref(o.Partition, ""), + )) +} + +// GetType implements Object. +func (o *V0042Assoc) GetType() object.ObjectType { + return ObjectTypeV0042Assoc +} + +// DeepCopyObject implements Object. +func (o *V0042Assoc) DeepCopyObject() object.Object { + return o.DeepCopy() +} + +func (o *V0042Assoc) DeepCopy() *V0042Assoc { + out := new(V0042Assoc) + utils.RemarshalOrDie(o, out) + return out +} + +type V0042AssocList struct { + Items []V0042Assoc +} + +// GetType implements ObjectList. +func (o *V0042AssocList) GetType() object.ObjectType { + return ObjectTypeV0042Assoc +} + +// GetItems implements ObjectList. +func (o *V0042AssocList) GetItems() []object.Object { + list := make([]object.Object, len(o.Items)) + for i, item := range o.Items { + list[i] = item.DeepCopyObject() + } + return list +} + +// AppendItem implements ObjectList. +func (o *V0042AssocList) AppendItem(obj object.Object) { + out, ok := obj.(*V0042Assoc) + if ok { + utils.RemarshalOrDie(obj, out) + o.Items = append(o.Items, *out) + } +} + +// DeepCopyObjectList implements ObjectList. +func (o *V0042AssocList) DeepCopyObjectList() object.ObjectList { + out := new(V0042AssocList) + out.Items = make([]V0042Assoc, len(o.Items)) + for i, item := range o.Items { + out.Items[i] = *item.DeepCopy() + } + return out +} diff --git a/pkg/types/V0042Assoc_test.go b/pkg/types/V0042Assoc_test.go new file mode 100644 index 0000000..39e4d2f --- /dev/null +++ b/pkg/types/V0042Assoc_test.go @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "testing" + + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0042" + "github.com/SlinkyProject/slurm-client/pkg/object" +) + +func TestV0042Assoc_GetKey(t *testing.T) { + assoc := &V0042Assoc{V0042Assoc: api.V0042Assoc{ + Cluster: ptr.To("linux"), + Account: ptr.To("research"), + User: "alice", + Partition: ptr.To("debug"), + }} + want := object.ObjectKey("linux/research/alice/debug") + if got := assoc.GetKey(); got != want { + t.Errorf("GetKey() = %q, want %q", got, want) + } + if got := assoc.GetType(); got != ObjectTypeV0042Assoc { + t.Errorf("GetType() = %q, want %q", got, ObjectTypeV0042Assoc) + } +} + +func TestV0042AssocList_GetItems(t *testing.T) { + list := &V0042AssocList{Items: []V0042Assoc{ + {V0042Assoc: api.V0042Assoc{User: "alice"}}, + {V0042Assoc: api.V0042Assoc{User: "bob"}}, + }} + if len(list.GetItems()) != 2 { + t.Errorf("GetItems() len = %d, want 2", len(list.GetItems())) + } +} diff --git a/pkg/types/V0042User.go b/pkg/types/V0042User.go new file mode 100644 index 0000000..4780357 --- /dev/null +++ b/pkg/types/V0042User.go @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + api "github.com/SlinkyProject/slurm-client/api/v0042" + "github.com/SlinkyProject/slurm-client/pkg/object" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +const ( + ObjectTypeV0042User = "V0042User" +) + +type V0042User struct { + api.V0042User +} + +// GetKey implements Object. +func (o *V0042User) GetKey() object.ObjectKey { + return object.ObjectKey(o.Name) +} + +// GetType implements Object. +func (o *V0042User) GetType() object.ObjectType { + return ObjectTypeV0042User +} + +// DeepCopyObject implements Object. +func (o *V0042User) DeepCopyObject() object.Object { + return o.DeepCopy() +} + +func (o *V0042User) DeepCopy() *V0042User { + out := new(V0042User) + utils.RemarshalOrDie(o, out) + return out +} + +type V0042UserList struct { + Items []V0042User +} + +// GetType implements ObjectList. +func (o *V0042UserList) GetType() object.ObjectType { + return ObjectTypeV0042User +} + +// GetItems implements ObjectList. +func (o *V0042UserList) GetItems() []object.Object { + list := make([]object.Object, len(o.Items)) + for i, item := range o.Items { + list[i] = item.DeepCopyObject() + } + return list +} + +// AppendItem implements ObjectList. +func (o *V0042UserList) AppendItem(obj object.Object) { + out, ok := obj.(*V0042User) + if ok { + utils.RemarshalOrDie(obj, out) + o.Items = append(o.Items, *out) + } +} + +// DeepCopyObjectList implements ObjectList. +func (o *V0042UserList) DeepCopyObjectList() object.ObjectList { + out := new(V0042UserList) + out.Items = make([]V0042User, len(o.Items)) + for i, item := range o.Items { + out.Items[i] = *item.DeepCopy() + } + return out +} diff --git a/pkg/types/V0042User_test.go b/pkg/types/V0042User_test.go new file mode 100644 index 0000000..9b94cb9 --- /dev/null +++ b/pkg/types/V0042User_test.go @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "testing" + + api "github.com/SlinkyProject/slurm-client/api/v0042" + "github.com/SlinkyProject/slurm-client/pkg/object" +) + +func TestV0042User_GetKey(t *testing.T) { + user := &V0042User{V0042User: api.V0042User{Name: "alice"}} + if got := user.GetKey(); got != object.ObjectKey("alice") { + t.Errorf("GetKey() = %q, want %q", got, "alice") + } + if got := user.GetType(); got != ObjectTypeV0042User { + t.Errorf("GetType() = %q, want %q", got, ObjectTypeV0042User) + } +} + +func TestV0042UserList_GetItems(t *testing.T) { + list := &V0042UserList{Items: []V0042User{ + {V0042User: api.V0042User{Name: "alice"}}, + }} + if len(list.GetItems()) != 1 { + t.Errorf("GetItems() len = %d, want 1", len(list.GetItems())) + } +} diff --git a/pkg/types/V0043Account.go b/pkg/types/V0043Account.go new file mode 100644 index 0000000..17b5336 --- /dev/null +++ b/pkg/types/V0043Account.go @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + api "github.com/SlinkyProject/slurm-client/api/v0043" + "github.com/SlinkyProject/slurm-client/pkg/object" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +const ( + ObjectTypeV0043Account = "V0043Account" +) + +type V0043Account struct { + api.V0043Account +} + +// GetKey implements Object. +func (o *V0043Account) GetKey() object.ObjectKey { + return object.ObjectKey(o.Name) +} + +// GetType implements Object. +func (o *V0043Account) GetType() object.ObjectType { + return ObjectTypeV0043Account +} + +// DeepCopyObject implements Object. +func (o *V0043Account) DeepCopyObject() object.Object { + return o.DeepCopy() +} + +func (o *V0043Account) DeepCopy() *V0043Account { + out := new(V0043Account) + utils.RemarshalOrDie(o, out) + return out +} + +type V0043AccountList struct { + Items []V0043Account +} + +// GetType implements ObjectList. +func (o *V0043AccountList) GetType() object.ObjectType { + return ObjectTypeV0043Account +} + +// GetItems implements ObjectList. +func (o *V0043AccountList) GetItems() []object.Object { + list := make([]object.Object, len(o.Items)) + for i, item := range o.Items { + list[i] = item.DeepCopyObject() + } + return list +} + +// AppendItem implements ObjectList. +func (o *V0043AccountList) AppendItem(obj object.Object) { + out, ok := obj.(*V0043Account) + if ok { + utils.RemarshalOrDie(obj, out) + o.Items = append(o.Items, *out) + } +} + +// DeepCopyObjectList implements ObjectList. +func (o *V0043AccountList) DeepCopyObjectList() object.ObjectList { + out := new(V0043AccountList) + out.Items = make([]V0043Account, len(o.Items)) + for i, item := range o.Items { + out.Items[i] = *item.DeepCopy() + } + return out +} diff --git a/pkg/types/V0043Account_test.go b/pkg/types/V0043Account_test.go new file mode 100644 index 0000000..64782f5 --- /dev/null +++ b/pkg/types/V0043Account_test.go @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "testing" + + api "github.com/SlinkyProject/slurm-client/api/v0043" + "github.com/SlinkyProject/slurm-client/pkg/object" +) + +func TestV0043Account_GetKey(t *testing.T) { + acct := &V0043Account{V0043Account: api.V0043Account{Name: "research"}} + if got := acct.GetKey(); got != object.ObjectKey("research") { + t.Errorf("GetKey() = %q, want %q", got, "research") + } + if got := acct.GetType(); got != ObjectTypeV0043Account { + t.Errorf("GetType() = %q, want %q", got, ObjectTypeV0043Account) + } +} + +func TestV0043AccountList_GetItems(t *testing.T) { + list := &V0043AccountList{Items: []V0043Account{ + {V0043Account: api.V0043Account{Name: "a"}}, + {V0043Account: api.V0043Account{Name: "b"}}, + }} + if len(list.GetItems()) != 2 { + t.Errorf("GetItems() len = %d, want 2", len(list.GetItems())) + } +} diff --git a/pkg/types/V0043Assoc.go b/pkg/types/V0043Assoc.go new file mode 100644 index 0000000..e4f8c7a --- /dev/null +++ b/pkg/types/V0043Assoc.go @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "fmt" + + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0043" + "github.com/SlinkyProject/slurm-client/pkg/object" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +const ( + ObjectTypeV0043Assoc = "V0043Assoc" +) + +type V0043Assoc struct { + api.V0043Assoc +} + +// GetKey implements Object. Key is "cluster/account/user/partition". +func (o *V0043Assoc) GetKey() object.ObjectKey { + return object.ObjectKey(fmt.Sprintf("%s/%s/%s/%s", + ptr.Deref(o.Cluster, ""), + ptr.Deref(o.Account, ""), + o.User, + ptr.Deref(o.Partition, ""), + )) +} + +// GetType implements Object. +func (o *V0043Assoc) GetType() object.ObjectType { + return ObjectTypeV0043Assoc +} + +// DeepCopyObject implements Object. +func (o *V0043Assoc) DeepCopyObject() object.Object { + return o.DeepCopy() +} + +func (o *V0043Assoc) DeepCopy() *V0043Assoc { + out := new(V0043Assoc) + utils.RemarshalOrDie(o, out) + return out +} + +type V0043AssocList struct { + Items []V0043Assoc +} + +// GetType implements ObjectList. +func (o *V0043AssocList) GetType() object.ObjectType { + return ObjectTypeV0043Assoc +} + +// GetItems implements ObjectList. +func (o *V0043AssocList) GetItems() []object.Object { + list := make([]object.Object, len(o.Items)) + for i, item := range o.Items { + list[i] = item.DeepCopyObject() + } + return list +} + +// AppendItem implements ObjectList. +func (o *V0043AssocList) AppendItem(obj object.Object) { + out, ok := obj.(*V0043Assoc) + if ok { + utils.RemarshalOrDie(obj, out) + o.Items = append(o.Items, *out) + } +} + +// DeepCopyObjectList implements ObjectList. +func (o *V0043AssocList) DeepCopyObjectList() object.ObjectList { + out := new(V0043AssocList) + out.Items = make([]V0043Assoc, len(o.Items)) + for i, item := range o.Items { + out.Items[i] = *item.DeepCopy() + } + return out +} diff --git a/pkg/types/V0043Assoc_test.go b/pkg/types/V0043Assoc_test.go new file mode 100644 index 0000000..91aa26a --- /dev/null +++ b/pkg/types/V0043Assoc_test.go @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "testing" + + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0043" + "github.com/SlinkyProject/slurm-client/pkg/object" +) + +func TestV0043Assoc_GetKey(t *testing.T) { + assoc := &V0043Assoc{V0043Assoc: api.V0043Assoc{ + Cluster: ptr.To("linux"), + Account: ptr.To("research"), + User: "alice", + Partition: ptr.To("debug"), + }} + want := object.ObjectKey("linux/research/alice/debug") + if got := assoc.GetKey(); got != want { + t.Errorf("GetKey() = %q, want %q", got, want) + } + if got := assoc.GetType(); got != ObjectTypeV0043Assoc { + t.Errorf("GetType() = %q, want %q", got, ObjectTypeV0043Assoc) + } +} + +func TestV0043AssocList_GetItems(t *testing.T) { + list := &V0043AssocList{Items: []V0043Assoc{ + {V0043Assoc: api.V0043Assoc{User: "alice"}}, + {V0043Assoc: api.V0043Assoc{User: "bob"}}, + }} + if len(list.GetItems()) != 2 { + t.Errorf("GetItems() len = %d, want 2", len(list.GetItems())) + } +} diff --git a/pkg/types/V0043User.go b/pkg/types/V0043User.go new file mode 100644 index 0000000..7de70f7 --- /dev/null +++ b/pkg/types/V0043User.go @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + api "github.com/SlinkyProject/slurm-client/api/v0043" + "github.com/SlinkyProject/slurm-client/pkg/object" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +const ( + ObjectTypeV0043User = "V0043User" +) + +type V0043User struct { + api.V0043User +} + +// GetKey implements Object. +func (o *V0043User) GetKey() object.ObjectKey { + return object.ObjectKey(o.Name) +} + +// GetType implements Object. +func (o *V0043User) GetType() object.ObjectType { + return ObjectTypeV0043User +} + +// DeepCopyObject implements Object. +func (o *V0043User) DeepCopyObject() object.Object { + return o.DeepCopy() +} + +func (o *V0043User) DeepCopy() *V0043User { + out := new(V0043User) + utils.RemarshalOrDie(o, out) + return out +} + +type V0043UserList struct { + Items []V0043User +} + +// GetType implements ObjectList. +func (o *V0043UserList) GetType() object.ObjectType { + return ObjectTypeV0043User +} + +// GetItems implements ObjectList. +func (o *V0043UserList) GetItems() []object.Object { + list := make([]object.Object, len(o.Items)) + for i, item := range o.Items { + list[i] = item.DeepCopyObject() + } + return list +} + +// AppendItem implements ObjectList. +func (o *V0043UserList) AppendItem(obj object.Object) { + out, ok := obj.(*V0043User) + if ok { + utils.RemarshalOrDie(obj, out) + o.Items = append(o.Items, *out) + } +} + +// DeepCopyObjectList implements ObjectList. +func (o *V0043UserList) DeepCopyObjectList() object.ObjectList { + out := new(V0043UserList) + out.Items = make([]V0043User, len(o.Items)) + for i, item := range o.Items { + out.Items[i] = *item.DeepCopy() + } + return out +} diff --git a/pkg/types/V0043User_test.go b/pkg/types/V0043User_test.go new file mode 100644 index 0000000..72c2dd2 --- /dev/null +++ b/pkg/types/V0043User_test.go @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "testing" + + api "github.com/SlinkyProject/slurm-client/api/v0043" + "github.com/SlinkyProject/slurm-client/pkg/object" +) + +func TestV0043User_GetKey(t *testing.T) { + user := &V0043User{V0043User: api.V0043User{Name: "alice"}} + if got := user.GetKey(); got != object.ObjectKey("alice") { + t.Errorf("GetKey() = %q, want %q", got, "alice") + } + if got := user.GetType(); got != ObjectTypeV0043User { + t.Errorf("GetType() = %q, want %q", got, ObjectTypeV0043User) + } +} + +func TestV0043UserList_GetItems(t *testing.T) { + list := &V0043UserList{Items: []V0043User{ + {V0043User: api.V0043User{Name: "alice"}}, + }} + if len(list.GetItems()) != 1 { + t.Errorf("GetItems() len = %d, want 1", len(list.GetItems())) + } +} diff --git a/pkg/types/V0044Account.go b/pkg/types/V0044Account.go new file mode 100644 index 0000000..2140442 --- /dev/null +++ b/pkg/types/V0044Account.go @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + api "github.com/SlinkyProject/slurm-client/api/v0044" + "github.com/SlinkyProject/slurm-client/pkg/object" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +const ( + ObjectTypeV0044Account = "V0044Account" +) + +type V0044Account struct { + api.V0044Account +} + +// GetKey implements Object. +func (o *V0044Account) GetKey() object.ObjectKey { + return object.ObjectKey(o.Name) +} + +// GetType implements Object. +func (o *V0044Account) GetType() object.ObjectType { + return ObjectTypeV0044Account +} + +// DeepCopyObject implements Object. +func (o *V0044Account) DeepCopyObject() object.Object { + return o.DeepCopy() +} + +func (o *V0044Account) DeepCopy() *V0044Account { + out := new(V0044Account) + utils.RemarshalOrDie(o, out) + return out +} + +type V0044AccountList struct { + Items []V0044Account +} + +// GetType implements ObjectList. +func (o *V0044AccountList) GetType() object.ObjectType { + return ObjectTypeV0044Account +} + +// GetItems implements ObjectList. +func (o *V0044AccountList) GetItems() []object.Object { + list := make([]object.Object, len(o.Items)) + for i, item := range o.Items { + list[i] = item.DeepCopyObject() + } + return list +} + +// AppendItem implements ObjectList. +func (o *V0044AccountList) AppendItem(obj object.Object) { + out, ok := obj.(*V0044Account) + if ok { + utils.RemarshalOrDie(obj, out) + o.Items = append(o.Items, *out) + } +} + +// DeepCopyObjectList implements ObjectList. +func (o *V0044AccountList) DeepCopyObjectList() object.ObjectList { + out := new(V0044AccountList) + out.Items = make([]V0044Account, len(o.Items)) + for i, item := range o.Items { + out.Items[i] = *item.DeepCopy() + } + return out +} diff --git a/pkg/types/V0044Account_test.go b/pkg/types/V0044Account_test.go new file mode 100644 index 0000000..731aa03 --- /dev/null +++ b/pkg/types/V0044Account_test.go @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "testing" + + api "github.com/SlinkyProject/slurm-client/api/v0044" + "github.com/SlinkyProject/slurm-client/pkg/object" +) + +func TestV0044Account_GetKey(t *testing.T) { + acct := &V0044Account{V0044Account: api.V0044Account{Name: "research"}} + if got := acct.GetKey(); got != object.ObjectKey("research") { + t.Errorf("GetKey() = %q, want %q", got, "research") + } + if got := acct.GetType(); got != ObjectTypeV0044Account { + t.Errorf("GetType() = %q, want %q", got, ObjectTypeV0044Account) + } +} + +func TestV0044AccountList_GetItems(t *testing.T) { + list := &V0044AccountList{Items: []V0044Account{ + {V0044Account: api.V0044Account{Name: "a"}}, + {V0044Account: api.V0044Account{Name: "b"}}, + }} + if len(list.GetItems()) != 2 { + t.Errorf("GetItems() len = %d, want 2", len(list.GetItems())) + } +} diff --git a/pkg/types/V0044Assoc.go b/pkg/types/V0044Assoc.go new file mode 100644 index 0000000..601fb98 --- /dev/null +++ b/pkg/types/V0044Assoc.go @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "fmt" + + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0044" + "github.com/SlinkyProject/slurm-client/pkg/object" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +const ( + ObjectTypeV0044Assoc = "V0044Assoc" +) + +type V0044Assoc struct { + api.V0044Assoc +} + +// GetKey implements Object. Key is "cluster/account/user/partition". +func (o *V0044Assoc) GetKey() object.ObjectKey { + return object.ObjectKey(fmt.Sprintf("%s/%s/%s/%s", + ptr.Deref(o.Cluster, ""), + ptr.Deref(o.Account, ""), + o.User, + ptr.Deref(o.Partition, ""), + )) +} + +// GetType implements Object. +func (o *V0044Assoc) GetType() object.ObjectType { + return ObjectTypeV0044Assoc +} + +// DeepCopyObject implements Object. +func (o *V0044Assoc) DeepCopyObject() object.Object { + return o.DeepCopy() +} + +func (o *V0044Assoc) DeepCopy() *V0044Assoc { + out := new(V0044Assoc) + utils.RemarshalOrDie(o, out) + return out +} + +type V0044AssocList struct { + Items []V0044Assoc +} + +// GetType implements ObjectList. +func (o *V0044AssocList) GetType() object.ObjectType { + return ObjectTypeV0044Assoc +} + +// GetItems implements ObjectList. +func (o *V0044AssocList) GetItems() []object.Object { + list := make([]object.Object, len(o.Items)) + for i, item := range o.Items { + list[i] = item.DeepCopyObject() + } + return list +} + +// AppendItem implements ObjectList. +func (o *V0044AssocList) AppendItem(obj object.Object) { + out, ok := obj.(*V0044Assoc) + if ok { + utils.RemarshalOrDie(obj, out) + o.Items = append(o.Items, *out) + } +} + +// DeepCopyObjectList implements ObjectList. +func (o *V0044AssocList) DeepCopyObjectList() object.ObjectList { + out := new(V0044AssocList) + out.Items = make([]V0044Assoc, len(o.Items)) + for i, item := range o.Items { + out.Items[i] = *item.DeepCopy() + } + return out +} diff --git a/pkg/types/V0044Assoc_test.go b/pkg/types/V0044Assoc_test.go new file mode 100644 index 0000000..76a36ec --- /dev/null +++ b/pkg/types/V0044Assoc_test.go @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "testing" + + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0044" + "github.com/SlinkyProject/slurm-client/pkg/object" +) + +func TestV0044Assoc_GetKey(t *testing.T) { + assoc := &V0044Assoc{V0044Assoc: api.V0044Assoc{ + Cluster: ptr.To("linux"), + Account: ptr.To("research"), + User: "alice", + Partition: ptr.To("debug"), + }} + want := object.ObjectKey("linux/research/alice/debug") + if got := assoc.GetKey(); got != want { + t.Errorf("GetKey() = %q, want %q", got, want) + } + if got := assoc.GetType(); got != ObjectTypeV0044Assoc { + t.Errorf("GetType() = %q, want %q", got, ObjectTypeV0044Assoc) + } +} + +func TestV0044AssocList_GetItems(t *testing.T) { + list := &V0044AssocList{Items: []V0044Assoc{ + {V0044Assoc: api.V0044Assoc{User: "alice"}}, + {V0044Assoc: api.V0044Assoc{User: "bob"}}, + }} + if len(list.GetItems()) != 2 { + t.Errorf("GetItems() len = %d, want 2", len(list.GetItems())) + } +} diff --git a/pkg/types/V0044User.go b/pkg/types/V0044User.go new file mode 100644 index 0000000..2341ab2 --- /dev/null +++ b/pkg/types/V0044User.go @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + api "github.com/SlinkyProject/slurm-client/api/v0044" + "github.com/SlinkyProject/slurm-client/pkg/object" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +const ( + ObjectTypeV0044User = "V0044User" +) + +type V0044User struct { + api.V0044User +} + +// GetKey implements Object. +func (o *V0044User) GetKey() object.ObjectKey { + return object.ObjectKey(o.Name) +} + +// GetType implements Object. +func (o *V0044User) GetType() object.ObjectType { + return ObjectTypeV0044User +} + +// DeepCopyObject implements Object. +func (o *V0044User) DeepCopyObject() object.Object { + return o.DeepCopy() +} + +func (o *V0044User) DeepCopy() *V0044User { + out := new(V0044User) + utils.RemarshalOrDie(o, out) + return out +} + +type V0044UserList struct { + Items []V0044User +} + +// GetType implements ObjectList. +func (o *V0044UserList) GetType() object.ObjectType { + return ObjectTypeV0044User +} + +// GetItems implements ObjectList. +func (o *V0044UserList) GetItems() []object.Object { + list := make([]object.Object, len(o.Items)) + for i, item := range o.Items { + list[i] = item.DeepCopyObject() + } + return list +} + +// AppendItem implements ObjectList. +func (o *V0044UserList) AppendItem(obj object.Object) { + out, ok := obj.(*V0044User) + if ok { + utils.RemarshalOrDie(obj, out) + o.Items = append(o.Items, *out) + } +} + +// DeepCopyObjectList implements ObjectList. +func (o *V0044UserList) DeepCopyObjectList() object.ObjectList { + out := new(V0044UserList) + out.Items = make([]V0044User, len(o.Items)) + for i, item := range o.Items { + out.Items[i] = *item.DeepCopy() + } + return out +} diff --git a/pkg/types/V0044User_test.go b/pkg/types/V0044User_test.go new file mode 100644 index 0000000..681c4ee --- /dev/null +++ b/pkg/types/V0044User_test.go @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "testing" + + api "github.com/SlinkyProject/slurm-client/api/v0044" + "github.com/SlinkyProject/slurm-client/pkg/object" +) + +func TestV0044User_GetKey(t *testing.T) { + user := &V0044User{V0044User: api.V0044User{Name: "alice"}} + if got := user.GetKey(); got != object.ObjectKey("alice") { + t.Errorf("GetKey() = %q, want %q", got, "alice") + } + if got := user.GetType(); got != ObjectTypeV0044User { + t.Errorf("GetType() = %q, want %q", got, ObjectTypeV0044User) + } +} + +func TestV0044UserList_GetItems(t *testing.T) { + list := &V0044UserList{Items: []V0044User{ + {V0044User: api.V0044User{Name: "alice"}}, + }} + if len(list.GetItems()) != 1 { + t.Errorf("GetItems() len = %d, want 1", len(list.GetItems())) + } +} diff --git a/pkg/types/V0045Account.go b/pkg/types/V0045Account.go new file mode 100644 index 0000000..01c1edf --- /dev/null +++ b/pkg/types/V0045Account.go @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + api "github.com/SlinkyProject/slurm-client/api/v0045" + "github.com/SlinkyProject/slurm-client/pkg/object" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +const ( + ObjectTypeV0045Account = "V0045Account" +) + +type V0045Account struct { + api.V0045Account +} + +// GetKey implements Object. +func (o *V0045Account) GetKey() object.ObjectKey { + return object.ObjectKey(o.Name) +} + +// GetType implements Object. +func (o *V0045Account) GetType() object.ObjectType { + return ObjectTypeV0045Account +} + +// DeepCopyObject implements Object. +func (o *V0045Account) DeepCopyObject() object.Object { + return o.DeepCopy() +} + +func (o *V0045Account) DeepCopy() *V0045Account { + out := new(V0045Account) + utils.RemarshalOrDie(o, out) + return out +} + +type V0045AccountList struct { + Items []V0045Account +} + +// GetType implements ObjectList. +func (o *V0045AccountList) GetType() object.ObjectType { + return ObjectTypeV0045Account +} + +// GetItems implements ObjectList. +func (o *V0045AccountList) GetItems() []object.Object { + list := make([]object.Object, len(o.Items)) + for i, item := range o.Items { + list[i] = item.DeepCopyObject() + } + return list +} + +// AppendItem implements ObjectList. +func (o *V0045AccountList) AppendItem(obj object.Object) { + out, ok := obj.(*V0045Account) + if ok { + utils.RemarshalOrDie(obj, out) + o.Items = append(o.Items, *out) + } +} + +// DeepCopyObjectList implements ObjectList. +func (o *V0045AccountList) DeepCopyObjectList() object.ObjectList { + out := new(V0045AccountList) + out.Items = make([]V0045Account, len(o.Items)) + for i, item := range o.Items { + out.Items[i] = *item.DeepCopy() + } + return out +} diff --git a/pkg/types/V0045Account_test.go b/pkg/types/V0045Account_test.go new file mode 100644 index 0000000..6a207ae --- /dev/null +++ b/pkg/types/V0045Account_test.go @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "testing" + + api "github.com/SlinkyProject/slurm-client/api/v0045" + "github.com/SlinkyProject/slurm-client/pkg/object" +) + +func TestV0045Account_GetKey(t *testing.T) { + acct := &V0045Account{V0045Account: api.V0045Account{Name: "research"}} + if got := acct.GetKey(); got != object.ObjectKey("research") { + t.Errorf("GetKey() = %q, want %q", got, "research") + } + if got := acct.GetType(); got != ObjectTypeV0045Account { + t.Errorf("GetType() = %q, want %q", got, ObjectTypeV0045Account) + } +} + +func TestV0045AccountList_GetItems(t *testing.T) { + list := &V0045AccountList{Items: []V0045Account{ + {V0045Account: api.V0045Account{Name: "a"}}, + {V0045Account: api.V0045Account{Name: "b"}}, + }} + if len(list.GetItems()) != 2 { + t.Errorf("GetItems() len = %d, want 2", len(list.GetItems())) + } +} diff --git a/pkg/types/V0045Assoc.go b/pkg/types/V0045Assoc.go new file mode 100644 index 0000000..06f6048 --- /dev/null +++ b/pkg/types/V0045Assoc.go @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "fmt" + + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0045" + "github.com/SlinkyProject/slurm-client/pkg/object" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +const ( + ObjectTypeV0045Assoc = "V0045Assoc" +) + +type V0045Assoc struct { + api.V0045Assoc +} + +// GetKey implements Object. Key is "cluster/account/user/partition". +func (o *V0045Assoc) GetKey() object.ObjectKey { + return object.ObjectKey(fmt.Sprintf("%s/%s/%s/%s", + ptr.Deref(o.Cluster, ""), + ptr.Deref(o.Account, ""), + o.User, + ptr.Deref(o.Partition, ""), + )) +} + +// GetType implements Object. +func (o *V0045Assoc) GetType() object.ObjectType { + return ObjectTypeV0045Assoc +} + +// DeepCopyObject implements Object. +func (o *V0045Assoc) DeepCopyObject() object.Object { + return o.DeepCopy() +} + +func (o *V0045Assoc) DeepCopy() *V0045Assoc { + out := new(V0045Assoc) + utils.RemarshalOrDie(o, out) + return out +} + +type V0045AssocList struct { + Items []V0045Assoc +} + +// GetType implements ObjectList. +func (o *V0045AssocList) GetType() object.ObjectType { + return ObjectTypeV0045Assoc +} + +// GetItems implements ObjectList. +func (o *V0045AssocList) GetItems() []object.Object { + list := make([]object.Object, len(o.Items)) + for i, item := range o.Items { + list[i] = item.DeepCopyObject() + } + return list +} + +// AppendItem implements ObjectList. +func (o *V0045AssocList) AppendItem(obj object.Object) { + out, ok := obj.(*V0045Assoc) + if ok { + utils.RemarshalOrDie(obj, out) + o.Items = append(o.Items, *out) + } +} + +// DeepCopyObjectList implements ObjectList. +func (o *V0045AssocList) DeepCopyObjectList() object.ObjectList { + out := new(V0045AssocList) + out.Items = make([]V0045Assoc, len(o.Items)) + for i, item := range o.Items { + out.Items[i] = *item.DeepCopy() + } + return out +} diff --git a/pkg/types/V0045Assoc_test.go b/pkg/types/V0045Assoc_test.go new file mode 100644 index 0000000..7993352 --- /dev/null +++ b/pkg/types/V0045Assoc_test.go @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "testing" + + "k8s.io/utils/ptr" + + api "github.com/SlinkyProject/slurm-client/api/v0045" + "github.com/SlinkyProject/slurm-client/pkg/object" +) + +func TestV0045Assoc_GetKey(t *testing.T) { + assoc := &V0045Assoc{V0045Assoc: api.V0045Assoc{ + Cluster: ptr.To("linux"), + Account: ptr.To("research"), + User: "alice", + Partition: ptr.To("debug"), + }} + want := object.ObjectKey("linux/research/alice/debug") + if got := assoc.GetKey(); got != want { + t.Errorf("GetKey() = %q, want %q", got, want) + } + if got := assoc.GetType(); got != ObjectTypeV0045Assoc { + t.Errorf("GetType() = %q, want %q", got, ObjectTypeV0045Assoc) + } +} + +func TestV0045AssocList_GetItems(t *testing.T) { + list := &V0045AssocList{Items: []V0045Assoc{ + {V0045Assoc: api.V0045Assoc{User: "alice"}}, + {V0045Assoc: api.V0045Assoc{User: "bob"}}, + }} + if len(list.GetItems()) != 2 { + t.Errorf("GetItems() len = %d, want 2", len(list.GetItems())) + } +} diff --git a/pkg/types/V0045User.go b/pkg/types/V0045User.go new file mode 100644 index 0000000..03b3aeb --- /dev/null +++ b/pkg/types/V0045User.go @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + api "github.com/SlinkyProject/slurm-client/api/v0045" + "github.com/SlinkyProject/slurm-client/pkg/object" + "github.com/SlinkyProject/slurm-client/pkg/utils" +) + +const ( + ObjectTypeV0045User = "V0045User" +) + +type V0045User struct { + api.V0045User +} + +// GetKey implements Object. +func (o *V0045User) GetKey() object.ObjectKey { + return object.ObjectKey(o.Name) +} + +// GetType implements Object. +func (o *V0045User) GetType() object.ObjectType { + return ObjectTypeV0045User +} + +// DeepCopyObject implements Object. +func (o *V0045User) DeepCopyObject() object.Object { + return o.DeepCopy() +} + +func (o *V0045User) DeepCopy() *V0045User { + out := new(V0045User) + utils.RemarshalOrDie(o, out) + return out +} + +type V0045UserList struct { + Items []V0045User +} + +// GetType implements ObjectList. +func (o *V0045UserList) GetType() object.ObjectType { + return ObjectTypeV0045User +} + +// GetItems implements ObjectList. +func (o *V0045UserList) GetItems() []object.Object { + list := make([]object.Object, len(o.Items)) + for i, item := range o.Items { + list[i] = item.DeepCopyObject() + } + return list +} + +// AppendItem implements ObjectList. +func (o *V0045UserList) AppendItem(obj object.Object) { + out, ok := obj.(*V0045User) + if ok { + utils.RemarshalOrDie(obj, out) + o.Items = append(o.Items, *out) + } +} + +// DeepCopyObjectList implements ObjectList. +func (o *V0045UserList) DeepCopyObjectList() object.ObjectList { + out := new(V0045UserList) + out.Items = make([]V0045User, len(o.Items)) + for i, item := range o.Items { + out.Items[i] = *item.DeepCopy() + } + return out +} diff --git a/pkg/types/V0045User_test.go b/pkg/types/V0045User_test.go new file mode 100644 index 0000000..2141ea8 --- /dev/null +++ b/pkg/types/V0045User_test.go @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: Copyright (C) SchedMD LLC. +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "testing" + + api "github.com/SlinkyProject/slurm-client/api/v0045" + "github.com/SlinkyProject/slurm-client/pkg/object" +) + +func TestV0045User_GetKey(t *testing.T) { + user := &V0045User{V0045User: api.V0045User{Name: "alice"}} + if got := user.GetKey(); got != object.ObjectKey("alice") { + t.Errorf("GetKey() = %q, want %q", got, "alice") + } + if got := user.GetType(); got != ObjectTypeV0045User { + t.Errorf("GetType() = %q, want %q", got, ObjectTypeV0045User) + } +} + +func TestV0045UserList_GetItems(t *testing.T) { + list := &V0045UserList{Items: []V0045User{ + {V0045User: api.V0045User{Name: "alice"}}, + }} + if len(list.GetItems()) != 1 { + t.Errorf("GetItems() len = %d, want 1", len(list.GetItems())) + } +} From 54d808daa1cc3ad826242352b50fbb6c7fe91782 Mon Sep 17 00:00:00 2001 From: Giulio Calzolari Date: Tue, 23 Jun 2026 15:06:33 +0200 Subject: [PATCH 2/2] fix: correct association Create/Update identity and filter Get server-side Address MAGI review findings on the accounting association handling: - CreateAssoc now returns the composite key derived from the request so the high-level Create refresh resolves the just-created association by identity instead of the (possibly empty) input object, eliminating a spurious NotFound after a successful POST. - Add UpdateAssoc, which pins the cluster/account/user/partition identity from the target key before upserting, so an update always targets the intended association regardless of the request's identity fields (mirrors UpdateAccount). - Add GetAssoc, which decomposes the composite key into server-side filter params and selects the exact match, replacing the previous full ListAssoc + client-side linear scan on every association lookup. - DeleteAssoc omits the user filter when the user is empty rather than sending user="", matching the nil-when-unset convention of the other filter fields. Add unit coverage for the new and changed methods across v0042-v0045. Signed-off-by: Giulio Calzolari --- pkg/client/api/v0042/association.go | 89 ++++++++++- pkg/client/api/v0042/association_test.go | 183 ++++++++++++++++++++++- pkg/client/api/v0043/association.go | 89 ++++++++++- pkg/client/api/v0043/association_test.go | 183 ++++++++++++++++++++++- pkg/client/api/v0044/association.go | 89 ++++++++++- pkg/client/api/v0044/association_test.go | 183 ++++++++++++++++++++++- pkg/client/api/v0045/association.go | 89 ++++++++++- pkg/client/api/v0045/association_test.go | 183 ++++++++++++++++++++++- pkg/client/client.go | 84 +++-------- 9 files changed, 1056 insertions(+), 116 deletions(-) diff --git a/pkg/client/api/v0042/association.go b/pkg/client/api/v0042/association.go index de8accf..a335a63 100644 --- a/pkg/client/api/v0042/association.go +++ b/pkg/client/api/v0042/association.go @@ -7,6 +7,7 @@ import ( "context" "errors" "net/http" + "strings" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/utils/ptr" @@ -17,41 +18,78 @@ import ( ) type AssocInterface interface { - CreateAssoc(ctx context.Context, req any) error + CreateAssoc(ctx context.Context, req any) (string, error) + UpdateAssoc(ctx context.Context, key string, req any) error DeleteAssoc(ctx context.Context, assoc api.V0042Assoc) error + GetAssoc(ctx context.Context, key string) (*types.V0042Assoc, error) ListAssoc(ctx context.Context) (*types.V0042AssocList, error) } var _ AssocInterface = &SlurmClient{} -// CreateAssoc implements AssocInterface (POST is upsert). -func (c *SlurmClient) CreateAssoc(ctx context.Context, req any) error { +// parseAssocKey splits a "cluster/account/user/partition" association key. +func parseAssocKey(key string) (cluster, account, user, partition string) { + parts := strings.SplitN(key, "/", 4) + for len(parts) < 4 { + parts = append(parts, "") + } + return parts[0], parts[1], parts[2], parts[3] +} + +func nonEmptyPtr(s string) *string { + if s == "" { + return nil + } + return ptr.To(s) +} + +// CreateAssoc implements AssocInterface (POST is upsert). It returns the +// composite key derived from the request so callers can refresh by identity. +func (c *SlurmClient) CreateAssoc(ctx context.Context, req any) (string, error) { r, ok := req.(api.V0042Assoc) if !ok { - return errors.New("expected req to be api.V0042Assoc") + return "", errors.New("expected req to be api.V0042Assoc") } body := api.SlurmdbV0042PostAssociationsJSONRequestBody{ Associations: api.V0042AssocList{r}, } res, err := c.SlurmdbV0042PostAssociationsWithResponse(ctx, body) if err != nil { - return err + return "", err } if res.StatusCode() != 200 { errs := []error{errors.New(http.StatusText(res.StatusCode()))} if res.JSONDefault != nil { errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) } - return utilerrors.NewAggregate(errs) + return "", utilerrors.NewAggregate(errs) } - return nil + assoc := &types.V0042Assoc{V0042Assoc: r} + return string(assoc.GetKey()), nil +} + +// UpdateAssoc implements AssocInterface. POST is upsert in slurmdbd; the +// association identity is pinned from key so the update targets the intended +// association regardless of the identity fields set on req. +func (c *SlurmClient) UpdateAssoc(ctx context.Context, key string, req any) error { + r, ok := req.(api.V0042Assoc) + if !ok { + return errors.New("expected req to be api.V0042Assoc") + } + cluster, account, user, partition := parseAssocKey(key) + r.Cluster = nonEmptyPtr(cluster) + r.Account = nonEmptyPtr(account) + r.User = user + r.Partition = nonEmptyPtr(partition) + _, err := c.CreateAssoc(ctx, r) + return err } // DeleteAssoc implements AssocInterface using a filter param set. func (c *SlurmClient) DeleteAssoc(ctx context.Context, assoc api.V0042Assoc) error { params := &api.SlurmdbV0042DeleteAssociationParams{ Account: assoc.Account, - User: ptr.To(assoc.User), + User: nonEmptyPtr(assoc.User), Cluster: assoc.Cluster, Partition: assoc.Partition, } @@ -69,6 +107,41 @@ func (c *SlurmClient) DeleteAssoc(ctx context.Context, assoc api.V0042Assoc) err return nil } +// GetAssoc implements AssocInterface. There is no get-by-key endpoint, so the +// composite key is decomposed into server-side filter params to avoid listing +// every association. +func (c *SlurmClient) GetAssoc(ctx context.Context, key string) (*types.V0042Assoc, error) { + cluster, account, user, partition := parseAssocKey(key) + params := &api.SlurmdbV0042GetAssociationsParams{ + Account: nonEmptyPtr(account), + Cluster: nonEmptyPtr(cluster), + User: nonEmptyPtr(user), + Partition: nonEmptyPtr(partition), + } + res, err := c.SlurmdbV0042GetAssociationsWithResponse(ctx, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + if res.JSON200 == nil { + return nil, errors.New(http.StatusText(http.StatusNotFound)) + } + for _, item := range res.JSON200.Associations { + out := &types.V0042Assoc{} + utils.RemarshalOrDie(item, out) + if string(out.GetKey()) == key { + return out, nil + } + } + return nil, errors.New(http.StatusText(http.StatusNotFound)) +} + // ListAssoc implements AssocInterface. func (c *SlurmClient) ListAssoc(ctx context.Context) (*types.V0042AssocList, error) { params := &api.SlurmdbV0042GetAssociationsParams{} diff --git a/pkg/client/api/v0042/association_test.go b/pkg/client/api/v0042/association_test.go index de6a5b3..c075a5c 100644 --- a/pkg/client/api/v0042/association_test.go +++ b/pkg/client/api/v0042/association_test.go @@ -88,26 +88,197 @@ func TestSlurmClient_CreateAssoc(t *testing.T) { tests := []struct { name string req any + wantKey string wantErr bool }{ - {name: "default", req: api.V0042Assoc{User: "alice", Account: ptr.To("research")}, wantErr: false}, + { + name: "returns composite key from request", + req: api.V0042Assoc{Cluster: ptr.To("linux"), Account: ptr.To("research"), User: "alice"}, + wantKey: "linux/research/alice/", + wantErr: false, + }, {name: "invalid type provided", req: api.V0042Account{Name: "research"}, wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} - err := c.CreateAssoc(context.Background(), tt.req) + gotKey, err := c.CreateAssoc(context.Background(), tt.req) if (err != nil) != tt.wantErr { t.Errorf("SlurmClient.CreateAssoc() error = %v, wantErr %v", err, tt.wantErr) } + if gotKey != tt.wantKey { + t.Errorf("SlurmClient.CreateAssoc() key = %q, want %q", gotKey, tt.wantKey) + } }) } } +func TestSlurmClient_UpdateAssoc(t *testing.T) { + var gotBody api.V0042OpenapiAssocsResp + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0042PostAssociationsWithResponse: func(ctx context.Context, body api.V0042OpenapiAssocsResp, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0042PostAssociationsResponse, error) { + gotBody = body + return &api.SlurmdbV0042PostAssociationsResponse{HTTPResponse: &fake.HttpSuccess}, nil + }, + }). + Build()} + + // req identity fields deliberately differ from key; key must win. + req := api.V0042Assoc{Cluster: ptr.To("other"), Account: ptr.To("other"), User: "bob"} + if err := c.UpdateAssoc(context.Background(), "linux/research/alice/", req); err != nil { + t.Fatalf("SlurmClient.UpdateAssoc() error = %v", err) + } + if len(gotBody.Associations) != 1 { + t.Fatalf("SlurmClient.UpdateAssoc() posted %d associations, want 1", len(gotBody.Associations)) + } + got := gotBody.Associations[0] + if ptr.Deref(got.Cluster, "") != "linux" || ptr.Deref(got.Account, "") != "research" || got.User != "alice" || got.Partition != nil { + t.Errorf("SlurmClient.UpdateAssoc() pinned identity = %+v, want cluster=linux account=research user=alice partition=nil", got) + } + + // invalid type provided + if err := c.UpdateAssoc(context.Background(), "linux/research/alice/", api.V0042Account{Name: "x"}); err == nil { + t.Errorf("SlurmClient.UpdateAssoc() expected error for invalid req type") + } +} + +func TestSlurmClient_GetAssoc(t *testing.T) { + matching := func() api.ClientWithResponsesInterface { + return fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0042GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0042GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0042GetAssociationsResponse, error) { + return &api.SlurmdbV0042GetAssociationsResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0042OpenapiAssocsResp{ + Associations: api.V0042AssocList{ + {Cluster: ptr.To("linux"), Account: ptr.To("research"), User: "alice"}, + }, + }, + }, nil + }, + }). + Build() + } + tests := []struct { + name string + key string + client api.ClientWithResponsesInterface + want *types.V0042Assoc + wantErr bool + }{ + { + name: "found by key", + key: "linux/research/alice/", + client: matching(), + want: &types.V0042Assoc{V0042Assoc: api.V0042Assoc{Cluster: ptr.To("linux"), Account: ptr.To("research"), User: "alice"}}, + }, + { + name: "not found - empty result", + key: "linux/research/bob/", + client: fake.NewFakeClient(), + wantErr: true, + }, + { + name: "not found - no key match", + key: "linux/research/bob/", + client: matching(), + wantErr: true, + }, + { + name: "HTTP error", + key: "linux/research/alice/", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0042GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0042GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0042GetAssociationsResponse, error) { + return nil, errors.New(http.StatusText(http.StatusBadGateway)) + }, + }). + Build(), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.GetAssoc(context.Background(), tt.key) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.GetAssoc() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.GetAssoc() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_GetAssoc_FiltersServerSide(t *testing.T) { + var gotParams *api.SlurmdbV0042GetAssociationsParams + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0042GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0042GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0042GetAssociationsResponse, error) { + gotParams = params + return &api.SlurmdbV0042GetAssociationsResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0042OpenapiAssocsResp{ + Associations: api.V0042AssocList{ + {Cluster: ptr.To("linux"), Account: ptr.To("research"), User: "alice"}, + }, + }, + }, nil + }, + }). + Build()} + + if _, err := c.GetAssoc(context.Background(), "linux/research/alice/"); err != nil { + t.Fatalf("SlurmClient.GetAssoc() error = %v", err) + } + if gotParams == nil { + t.Fatal("SlurmClient.GetAssoc() did not call the associations endpoint") + } + if ptr.Deref(gotParams.Cluster, "") != "linux" || ptr.Deref(gotParams.Account, "") != "research" || ptr.Deref(gotParams.User, "") != "alice" { + t.Errorf("SlurmClient.GetAssoc() filter params = %+v, want cluster=linux account=research user=alice", gotParams) + } + if gotParams.Partition != nil { + t.Errorf("SlurmClient.GetAssoc() partition param = %v, want nil", gotParams.Partition) + } +} + func TestSlurmClient_DeleteAssoc(t *testing.T) { - c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} - err := c.DeleteAssoc(context.Background(), api.V0042Assoc{User: "alice", Account: ptr.To("research")}) - if err != nil { - t.Errorf("SlurmClient.DeleteAssoc() error = %v, wantErr false", err) + tests := []struct { + name string + assoc api.V0042Assoc + wantUserParam *string + }{ + { + name: "with user", + assoc: api.V0042Assoc{Account: ptr.To("research"), User: "alice"}, + wantUserParam: ptr.To("alice"), + }, + { + name: "empty user omitted", + assoc: api.V0042Assoc{Account: ptr.To("research")}, + wantUserParam: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var gotParams *api.SlurmdbV0042DeleteAssociationParams + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0042DeleteAssociationWithResponse: func(ctx context.Context, params *api.SlurmdbV0042DeleteAssociationParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0042DeleteAssociationResponse, error) { + gotParams = params + return &api.SlurmdbV0042DeleteAssociationResponse{HTTPResponse: &fake.HttpSuccess, JSON200: &api.V0042OpenapiAssocsRemovedResp{}}, nil + }, + }). + Build()} + if err := c.DeleteAssoc(context.Background(), tt.assoc); err != nil { + t.Fatalf("SlurmClient.DeleteAssoc() error = %v", err) + } + if !reflect.DeepEqual(gotParams.User, tt.wantUserParam) { + t.Errorf("SlurmClient.DeleteAssoc() User param = %v, want %v", ptr.Deref(gotParams.User, ""), ptr.Deref(tt.wantUserParam, "")) + } + }) } } diff --git a/pkg/client/api/v0043/association.go b/pkg/client/api/v0043/association.go index 8df66c1..5fa019a 100644 --- a/pkg/client/api/v0043/association.go +++ b/pkg/client/api/v0043/association.go @@ -7,6 +7,7 @@ import ( "context" "errors" "net/http" + "strings" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/utils/ptr" @@ -17,41 +18,78 @@ import ( ) type AssocInterface interface { - CreateAssoc(ctx context.Context, req any) error + CreateAssoc(ctx context.Context, req any) (string, error) + UpdateAssoc(ctx context.Context, key string, req any) error DeleteAssoc(ctx context.Context, assoc api.V0043Assoc) error + GetAssoc(ctx context.Context, key string) (*types.V0043Assoc, error) ListAssoc(ctx context.Context) (*types.V0043AssocList, error) } var _ AssocInterface = &SlurmClient{} -// CreateAssoc implements AssocInterface (POST is upsert). -func (c *SlurmClient) CreateAssoc(ctx context.Context, req any) error { +// parseAssocKey splits a "cluster/account/user/partition" association key. +func parseAssocKey(key string) (cluster, account, user, partition string) { + parts := strings.SplitN(key, "/", 4) + for len(parts) < 4 { + parts = append(parts, "") + } + return parts[0], parts[1], parts[2], parts[3] +} + +func nonEmptyPtr(s string) *string { + if s == "" { + return nil + } + return ptr.To(s) +} + +// CreateAssoc implements AssocInterface (POST is upsert). It returns the +// composite key derived from the request so callers can refresh by identity. +func (c *SlurmClient) CreateAssoc(ctx context.Context, req any) (string, error) { r, ok := req.(api.V0043Assoc) if !ok { - return errors.New("expected req to be api.V0043Assoc") + return "", errors.New("expected req to be api.V0043Assoc") } body := api.SlurmdbV0043PostAssociationsJSONRequestBody{ Associations: api.V0043AssocList{r}, } res, err := c.SlurmdbV0043PostAssociationsWithResponse(ctx, body) if err != nil { - return err + return "", err } if res.StatusCode() != 200 { errs := []error{errors.New(http.StatusText(res.StatusCode()))} if res.JSONDefault != nil { errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) } - return utilerrors.NewAggregate(errs) + return "", utilerrors.NewAggregate(errs) } - return nil + assoc := &types.V0043Assoc{V0043Assoc: r} + return string(assoc.GetKey()), nil +} + +// UpdateAssoc implements AssocInterface. POST is upsert in slurmdbd; the +// association identity is pinned from key so the update targets the intended +// association regardless of the identity fields set on req. +func (c *SlurmClient) UpdateAssoc(ctx context.Context, key string, req any) error { + r, ok := req.(api.V0043Assoc) + if !ok { + return errors.New("expected req to be api.V0043Assoc") + } + cluster, account, user, partition := parseAssocKey(key) + r.Cluster = nonEmptyPtr(cluster) + r.Account = nonEmptyPtr(account) + r.User = user + r.Partition = nonEmptyPtr(partition) + _, err := c.CreateAssoc(ctx, r) + return err } // DeleteAssoc implements AssocInterface using a filter param set. func (c *SlurmClient) DeleteAssoc(ctx context.Context, assoc api.V0043Assoc) error { params := &api.SlurmdbV0043DeleteAssociationParams{ Account: assoc.Account, - User: ptr.To(assoc.User), + User: nonEmptyPtr(assoc.User), Cluster: assoc.Cluster, Partition: assoc.Partition, } @@ -69,6 +107,41 @@ func (c *SlurmClient) DeleteAssoc(ctx context.Context, assoc api.V0043Assoc) err return nil } +// GetAssoc implements AssocInterface. There is no get-by-key endpoint, so the +// composite key is decomposed into server-side filter params to avoid listing +// every association. +func (c *SlurmClient) GetAssoc(ctx context.Context, key string) (*types.V0043Assoc, error) { + cluster, account, user, partition := parseAssocKey(key) + params := &api.SlurmdbV0043GetAssociationsParams{ + Account: nonEmptyPtr(account), + Cluster: nonEmptyPtr(cluster), + User: nonEmptyPtr(user), + Partition: nonEmptyPtr(partition), + } + res, err := c.SlurmdbV0043GetAssociationsWithResponse(ctx, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + if res.JSON200 == nil { + return nil, errors.New(http.StatusText(http.StatusNotFound)) + } + for _, item := range res.JSON200.Associations { + out := &types.V0043Assoc{} + utils.RemarshalOrDie(item, out) + if string(out.GetKey()) == key { + return out, nil + } + } + return nil, errors.New(http.StatusText(http.StatusNotFound)) +} + // ListAssoc implements AssocInterface. func (c *SlurmClient) ListAssoc(ctx context.Context) (*types.V0043AssocList, error) { params := &api.SlurmdbV0043GetAssociationsParams{} diff --git a/pkg/client/api/v0043/association_test.go b/pkg/client/api/v0043/association_test.go index 3628588..d417dac 100644 --- a/pkg/client/api/v0043/association_test.go +++ b/pkg/client/api/v0043/association_test.go @@ -88,26 +88,197 @@ func TestSlurmClient_CreateAssoc(t *testing.T) { tests := []struct { name string req any + wantKey string wantErr bool }{ - {name: "default", req: api.V0043Assoc{User: "alice", Account: ptr.To("research")}, wantErr: false}, + { + name: "returns composite key from request", + req: api.V0043Assoc{Cluster: ptr.To("linux"), Account: ptr.To("research"), User: "alice"}, + wantKey: "linux/research/alice/", + wantErr: false, + }, {name: "invalid type provided", req: api.V0043Account{Name: "research"}, wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} - err := c.CreateAssoc(context.Background(), tt.req) + gotKey, err := c.CreateAssoc(context.Background(), tt.req) if (err != nil) != tt.wantErr { t.Errorf("SlurmClient.CreateAssoc() error = %v, wantErr %v", err, tt.wantErr) } + if gotKey != tt.wantKey { + t.Errorf("SlurmClient.CreateAssoc() key = %q, want %q", gotKey, tt.wantKey) + } }) } } +func TestSlurmClient_UpdateAssoc(t *testing.T) { + var gotBody api.V0043OpenapiAssocsResp + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0043PostAssociationsWithResponse: func(ctx context.Context, body api.V0043OpenapiAssocsResp, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0043PostAssociationsResponse, error) { + gotBody = body + return &api.SlurmdbV0043PostAssociationsResponse{HTTPResponse: &fake.HttpSuccess}, nil + }, + }). + Build()} + + // req identity fields deliberately differ from key; key must win. + req := api.V0043Assoc{Cluster: ptr.To("other"), Account: ptr.To("other"), User: "bob"} + if err := c.UpdateAssoc(context.Background(), "linux/research/alice/", req); err != nil { + t.Fatalf("SlurmClient.UpdateAssoc() error = %v", err) + } + if len(gotBody.Associations) != 1 { + t.Fatalf("SlurmClient.UpdateAssoc() posted %d associations, want 1", len(gotBody.Associations)) + } + got := gotBody.Associations[0] + if ptr.Deref(got.Cluster, "") != "linux" || ptr.Deref(got.Account, "") != "research" || got.User != "alice" || got.Partition != nil { + t.Errorf("SlurmClient.UpdateAssoc() pinned identity = %+v, want cluster=linux account=research user=alice partition=nil", got) + } + + // invalid type provided + if err := c.UpdateAssoc(context.Background(), "linux/research/alice/", api.V0043Account{Name: "x"}); err == nil { + t.Errorf("SlurmClient.UpdateAssoc() expected error for invalid req type") + } +} + +func TestSlurmClient_GetAssoc(t *testing.T) { + matching := func() api.ClientWithResponsesInterface { + return fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0043GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0043GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0043GetAssociationsResponse, error) { + return &api.SlurmdbV0043GetAssociationsResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0043OpenapiAssocsResp{ + Associations: api.V0043AssocList{ + {Cluster: ptr.To("linux"), Account: ptr.To("research"), User: "alice"}, + }, + }, + }, nil + }, + }). + Build() + } + tests := []struct { + name string + key string + client api.ClientWithResponsesInterface + want *types.V0043Assoc + wantErr bool + }{ + { + name: "found by key", + key: "linux/research/alice/", + client: matching(), + want: &types.V0043Assoc{V0043Assoc: api.V0043Assoc{Cluster: ptr.To("linux"), Account: ptr.To("research"), User: "alice"}}, + }, + { + name: "not found - empty result", + key: "linux/research/bob/", + client: fake.NewFakeClient(), + wantErr: true, + }, + { + name: "not found - no key match", + key: "linux/research/bob/", + client: matching(), + wantErr: true, + }, + { + name: "HTTP error", + key: "linux/research/alice/", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0043GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0043GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0043GetAssociationsResponse, error) { + return nil, errors.New(http.StatusText(http.StatusBadGateway)) + }, + }). + Build(), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.GetAssoc(context.Background(), tt.key) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.GetAssoc() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.GetAssoc() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_GetAssoc_FiltersServerSide(t *testing.T) { + var gotParams *api.SlurmdbV0043GetAssociationsParams + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0043GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0043GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0043GetAssociationsResponse, error) { + gotParams = params + return &api.SlurmdbV0043GetAssociationsResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0043OpenapiAssocsResp{ + Associations: api.V0043AssocList{ + {Cluster: ptr.To("linux"), Account: ptr.To("research"), User: "alice"}, + }, + }, + }, nil + }, + }). + Build()} + + if _, err := c.GetAssoc(context.Background(), "linux/research/alice/"); err != nil { + t.Fatalf("SlurmClient.GetAssoc() error = %v", err) + } + if gotParams == nil { + t.Fatal("SlurmClient.GetAssoc() did not call the associations endpoint") + } + if ptr.Deref(gotParams.Cluster, "") != "linux" || ptr.Deref(gotParams.Account, "") != "research" || ptr.Deref(gotParams.User, "") != "alice" { + t.Errorf("SlurmClient.GetAssoc() filter params = %+v, want cluster=linux account=research user=alice", gotParams) + } + if gotParams.Partition != nil { + t.Errorf("SlurmClient.GetAssoc() partition param = %v, want nil", gotParams.Partition) + } +} + func TestSlurmClient_DeleteAssoc(t *testing.T) { - c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} - err := c.DeleteAssoc(context.Background(), api.V0043Assoc{User: "alice", Account: ptr.To("research")}) - if err != nil { - t.Errorf("SlurmClient.DeleteAssoc() error = %v, wantErr false", err) + tests := []struct { + name string + assoc api.V0043Assoc + wantUserParam *string + }{ + { + name: "with user", + assoc: api.V0043Assoc{Account: ptr.To("research"), User: "alice"}, + wantUserParam: ptr.To("alice"), + }, + { + name: "empty user omitted", + assoc: api.V0043Assoc{Account: ptr.To("research")}, + wantUserParam: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var gotParams *api.SlurmdbV0043DeleteAssociationParams + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0043DeleteAssociationWithResponse: func(ctx context.Context, params *api.SlurmdbV0043DeleteAssociationParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0043DeleteAssociationResponse, error) { + gotParams = params + return &api.SlurmdbV0043DeleteAssociationResponse{HTTPResponse: &fake.HttpSuccess, JSON200: &api.V0043OpenapiAssocsRemovedResp{}}, nil + }, + }). + Build()} + if err := c.DeleteAssoc(context.Background(), tt.assoc); err != nil { + t.Fatalf("SlurmClient.DeleteAssoc() error = %v", err) + } + if !reflect.DeepEqual(gotParams.User, tt.wantUserParam) { + t.Errorf("SlurmClient.DeleteAssoc() User param = %v, want %v", ptr.Deref(gotParams.User, ""), ptr.Deref(tt.wantUserParam, "")) + } + }) } } diff --git a/pkg/client/api/v0044/association.go b/pkg/client/api/v0044/association.go index 8072b00..fb6fc49 100644 --- a/pkg/client/api/v0044/association.go +++ b/pkg/client/api/v0044/association.go @@ -7,6 +7,7 @@ import ( "context" "errors" "net/http" + "strings" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/utils/ptr" @@ -17,41 +18,78 @@ import ( ) type AssocInterface interface { - CreateAssoc(ctx context.Context, req any) error + CreateAssoc(ctx context.Context, req any) (string, error) + UpdateAssoc(ctx context.Context, key string, req any) error DeleteAssoc(ctx context.Context, assoc api.V0044Assoc) error + GetAssoc(ctx context.Context, key string) (*types.V0044Assoc, error) ListAssoc(ctx context.Context) (*types.V0044AssocList, error) } var _ AssocInterface = &SlurmClient{} -// CreateAssoc implements AssocInterface (POST is upsert). -func (c *SlurmClient) CreateAssoc(ctx context.Context, req any) error { +// parseAssocKey splits a "cluster/account/user/partition" association key. +func parseAssocKey(key string) (cluster, account, user, partition string) { + parts := strings.SplitN(key, "/", 4) + for len(parts) < 4 { + parts = append(parts, "") + } + return parts[0], parts[1], parts[2], parts[3] +} + +func nonEmptyPtr(s string) *string { + if s == "" { + return nil + } + return ptr.To(s) +} + +// CreateAssoc implements AssocInterface (POST is upsert). It returns the +// composite key derived from the request so callers can refresh by identity. +func (c *SlurmClient) CreateAssoc(ctx context.Context, req any) (string, error) { r, ok := req.(api.V0044Assoc) if !ok { - return errors.New("expected req to be api.V0044Assoc") + return "", errors.New("expected req to be api.V0044Assoc") } body := api.SlurmdbV0044PostAssociationsJSONRequestBody{ Associations: api.V0044AssocList{r}, } res, err := c.SlurmdbV0044PostAssociationsWithResponse(ctx, body) if err != nil { - return err + return "", err } if res.StatusCode() != 200 { errs := []error{errors.New(http.StatusText(res.StatusCode()))} if res.JSONDefault != nil { errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) } - return utilerrors.NewAggregate(errs) + return "", utilerrors.NewAggregate(errs) } - return nil + assoc := &types.V0044Assoc{V0044Assoc: r} + return string(assoc.GetKey()), nil +} + +// UpdateAssoc implements AssocInterface. POST is upsert in slurmdbd; the +// association identity is pinned from key so the update targets the intended +// association regardless of the identity fields set on req. +func (c *SlurmClient) UpdateAssoc(ctx context.Context, key string, req any) error { + r, ok := req.(api.V0044Assoc) + if !ok { + return errors.New("expected req to be api.V0044Assoc") + } + cluster, account, user, partition := parseAssocKey(key) + r.Cluster = nonEmptyPtr(cluster) + r.Account = nonEmptyPtr(account) + r.User = user + r.Partition = nonEmptyPtr(partition) + _, err := c.CreateAssoc(ctx, r) + return err } // DeleteAssoc implements AssocInterface using a filter param set. func (c *SlurmClient) DeleteAssoc(ctx context.Context, assoc api.V0044Assoc) error { params := &api.SlurmdbV0044DeleteAssociationParams{ Account: assoc.Account, - User: ptr.To(assoc.User), + User: nonEmptyPtr(assoc.User), Cluster: assoc.Cluster, Partition: assoc.Partition, } @@ -69,6 +107,41 @@ func (c *SlurmClient) DeleteAssoc(ctx context.Context, assoc api.V0044Assoc) err return nil } +// GetAssoc implements AssocInterface. There is no get-by-key endpoint, so the +// composite key is decomposed into server-side filter params to avoid listing +// every association. +func (c *SlurmClient) GetAssoc(ctx context.Context, key string) (*types.V0044Assoc, error) { + cluster, account, user, partition := parseAssocKey(key) + params := &api.SlurmdbV0044GetAssociationsParams{ + Account: nonEmptyPtr(account), + Cluster: nonEmptyPtr(cluster), + User: nonEmptyPtr(user), + Partition: nonEmptyPtr(partition), + } + res, err := c.SlurmdbV0044GetAssociationsWithResponse(ctx, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + if res.JSON200 == nil { + return nil, errors.New(http.StatusText(http.StatusNotFound)) + } + for _, item := range res.JSON200.Associations { + out := &types.V0044Assoc{} + utils.RemarshalOrDie(item, out) + if string(out.GetKey()) == key { + return out, nil + } + } + return nil, errors.New(http.StatusText(http.StatusNotFound)) +} + // ListAssoc implements AssocInterface. func (c *SlurmClient) ListAssoc(ctx context.Context) (*types.V0044AssocList, error) { params := &api.SlurmdbV0044GetAssociationsParams{} diff --git a/pkg/client/api/v0044/association_test.go b/pkg/client/api/v0044/association_test.go index 04f6636..9f3d060 100644 --- a/pkg/client/api/v0044/association_test.go +++ b/pkg/client/api/v0044/association_test.go @@ -88,26 +88,197 @@ func TestSlurmClient_CreateAssoc(t *testing.T) { tests := []struct { name string req any + wantKey string wantErr bool }{ - {name: "default", req: api.V0044Assoc{User: "alice", Account: ptr.To("research")}, wantErr: false}, + { + name: "returns composite key from request", + req: api.V0044Assoc{Cluster: ptr.To("linux"), Account: ptr.To("research"), User: "alice"}, + wantKey: "linux/research/alice/", + wantErr: false, + }, {name: "invalid type provided", req: api.V0044Account{Name: "research"}, wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} - err := c.CreateAssoc(context.Background(), tt.req) + gotKey, err := c.CreateAssoc(context.Background(), tt.req) if (err != nil) != tt.wantErr { t.Errorf("SlurmClient.CreateAssoc() error = %v, wantErr %v", err, tt.wantErr) } + if gotKey != tt.wantKey { + t.Errorf("SlurmClient.CreateAssoc() key = %q, want %q", gotKey, tt.wantKey) + } }) } } +func TestSlurmClient_UpdateAssoc(t *testing.T) { + var gotBody api.V0044OpenapiAssocsResp + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0044PostAssociationsWithResponse: func(ctx context.Context, body api.V0044OpenapiAssocsResp, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0044PostAssociationsResponse, error) { + gotBody = body + return &api.SlurmdbV0044PostAssociationsResponse{HTTPResponse: &fake.HttpSuccess}, nil + }, + }). + Build()} + + // req identity fields deliberately differ from key; key must win. + req := api.V0044Assoc{Cluster: ptr.To("other"), Account: ptr.To("other"), User: "bob"} + if err := c.UpdateAssoc(context.Background(), "linux/research/alice/", req); err != nil { + t.Fatalf("SlurmClient.UpdateAssoc() error = %v", err) + } + if len(gotBody.Associations) != 1 { + t.Fatalf("SlurmClient.UpdateAssoc() posted %d associations, want 1", len(gotBody.Associations)) + } + got := gotBody.Associations[0] + if ptr.Deref(got.Cluster, "") != "linux" || ptr.Deref(got.Account, "") != "research" || got.User != "alice" || got.Partition != nil { + t.Errorf("SlurmClient.UpdateAssoc() pinned identity = %+v, want cluster=linux account=research user=alice partition=nil", got) + } + + // invalid type provided + if err := c.UpdateAssoc(context.Background(), "linux/research/alice/", api.V0044Account{Name: "x"}); err == nil { + t.Errorf("SlurmClient.UpdateAssoc() expected error for invalid req type") + } +} + +func TestSlurmClient_GetAssoc(t *testing.T) { + matching := func() api.ClientWithResponsesInterface { + return fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0044GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0044GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0044GetAssociationsResponse, error) { + return &api.SlurmdbV0044GetAssociationsResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0044OpenapiAssocsResp{ + Associations: api.V0044AssocList{ + {Cluster: ptr.To("linux"), Account: ptr.To("research"), User: "alice"}, + }, + }, + }, nil + }, + }). + Build() + } + tests := []struct { + name string + key string + client api.ClientWithResponsesInterface + want *types.V0044Assoc + wantErr bool + }{ + { + name: "found by key", + key: "linux/research/alice/", + client: matching(), + want: &types.V0044Assoc{V0044Assoc: api.V0044Assoc{Cluster: ptr.To("linux"), Account: ptr.To("research"), User: "alice"}}, + }, + { + name: "not found - empty result", + key: "linux/research/bob/", + client: fake.NewFakeClient(), + wantErr: true, + }, + { + name: "not found - no key match", + key: "linux/research/bob/", + client: matching(), + wantErr: true, + }, + { + name: "HTTP error", + key: "linux/research/alice/", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0044GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0044GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0044GetAssociationsResponse, error) { + return nil, errors.New(http.StatusText(http.StatusBadGateway)) + }, + }). + Build(), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.GetAssoc(context.Background(), tt.key) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.GetAssoc() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.GetAssoc() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_GetAssoc_FiltersServerSide(t *testing.T) { + var gotParams *api.SlurmdbV0044GetAssociationsParams + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0044GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0044GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0044GetAssociationsResponse, error) { + gotParams = params + return &api.SlurmdbV0044GetAssociationsResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0044OpenapiAssocsResp{ + Associations: api.V0044AssocList{ + {Cluster: ptr.To("linux"), Account: ptr.To("research"), User: "alice"}, + }, + }, + }, nil + }, + }). + Build()} + + if _, err := c.GetAssoc(context.Background(), "linux/research/alice/"); err != nil { + t.Fatalf("SlurmClient.GetAssoc() error = %v", err) + } + if gotParams == nil { + t.Fatal("SlurmClient.GetAssoc() did not call the associations endpoint") + } + if ptr.Deref(gotParams.Cluster, "") != "linux" || ptr.Deref(gotParams.Account, "") != "research" || ptr.Deref(gotParams.User, "") != "alice" { + t.Errorf("SlurmClient.GetAssoc() filter params = %+v, want cluster=linux account=research user=alice", gotParams) + } + if gotParams.Partition != nil { + t.Errorf("SlurmClient.GetAssoc() partition param = %v, want nil", gotParams.Partition) + } +} + func TestSlurmClient_DeleteAssoc(t *testing.T) { - c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} - err := c.DeleteAssoc(context.Background(), api.V0044Assoc{User: "alice", Account: ptr.To("research")}) - if err != nil { - t.Errorf("SlurmClient.DeleteAssoc() error = %v, wantErr false", err) + tests := []struct { + name string + assoc api.V0044Assoc + wantUserParam *string + }{ + { + name: "with user", + assoc: api.V0044Assoc{Account: ptr.To("research"), User: "alice"}, + wantUserParam: ptr.To("alice"), + }, + { + name: "empty user omitted", + assoc: api.V0044Assoc{Account: ptr.To("research")}, + wantUserParam: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var gotParams *api.SlurmdbV0044DeleteAssociationParams + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0044DeleteAssociationWithResponse: func(ctx context.Context, params *api.SlurmdbV0044DeleteAssociationParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0044DeleteAssociationResponse, error) { + gotParams = params + return &api.SlurmdbV0044DeleteAssociationResponse{HTTPResponse: &fake.HttpSuccess, JSON200: &api.V0044OpenapiAssocsRemovedResp{}}, nil + }, + }). + Build()} + if err := c.DeleteAssoc(context.Background(), tt.assoc); err != nil { + t.Fatalf("SlurmClient.DeleteAssoc() error = %v", err) + } + if !reflect.DeepEqual(gotParams.User, tt.wantUserParam) { + t.Errorf("SlurmClient.DeleteAssoc() User param = %v, want %v", ptr.Deref(gotParams.User, ""), ptr.Deref(tt.wantUserParam, "")) + } + }) } } diff --git a/pkg/client/api/v0045/association.go b/pkg/client/api/v0045/association.go index 7ae94d2..9ab6033 100644 --- a/pkg/client/api/v0045/association.go +++ b/pkg/client/api/v0045/association.go @@ -7,6 +7,7 @@ import ( "context" "errors" "net/http" + "strings" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/utils/ptr" @@ -17,41 +18,78 @@ import ( ) type AssocInterface interface { - CreateAssoc(ctx context.Context, req any) error + CreateAssoc(ctx context.Context, req any) (string, error) + UpdateAssoc(ctx context.Context, key string, req any) error DeleteAssoc(ctx context.Context, assoc api.V0045Assoc) error + GetAssoc(ctx context.Context, key string) (*types.V0045Assoc, error) ListAssoc(ctx context.Context) (*types.V0045AssocList, error) } var _ AssocInterface = &SlurmClient{} -// CreateAssoc implements AssocInterface (POST is upsert). -func (c *SlurmClient) CreateAssoc(ctx context.Context, req any) error { +// parseAssocKey splits a "cluster/account/user/partition" association key. +func parseAssocKey(key string) (cluster, account, user, partition string) { + parts := strings.SplitN(key, "/", 4) + for len(parts) < 4 { + parts = append(parts, "") + } + return parts[0], parts[1], parts[2], parts[3] +} + +func nonEmptyPtr(s string) *string { + if s == "" { + return nil + } + return ptr.To(s) +} + +// CreateAssoc implements AssocInterface (POST is upsert). It returns the +// composite key derived from the request so callers can refresh by identity. +func (c *SlurmClient) CreateAssoc(ctx context.Context, req any) (string, error) { r, ok := req.(api.V0045Assoc) if !ok { - return errors.New("expected req to be api.V0045Assoc") + return "", errors.New("expected req to be api.V0045Assoc") } body := api.SlurmdbV0045PostAssociationsJSONRequestBody{ Associations: api.V0045AssocList{r}, } res, err := c.SlurmdbV0045PostAssociationsWithResponse(ctx, body) if err != nil { - return err + return "", err } if res.StatusCode() != 200 { errs := []error{errors.New(http.StatusText(res.StatusCode()))} if res.JSONDefault != nil { errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) } - return utilerrors.NewAggregate(errs) + return "", utilerrors.NewAggregate(errs) } - return nil + assoc := &types.V0045Assoc{V0045Assoc: r} + return string(assoc.GetKey()), nil +} + +// UpdateAssoc implements AssocInterface. POST is upsert in slurmdbd; the +// association identity is pinned from key so the update targets the intended +// association regardless of the identity fields set on req. +func (c *SlurmClient) UpdateAssoc(ctx context.Context, key string, req any) error { + r, ok := req.(api.V0045Assoc) + if !ok { + return errors.New("expected req to be api.V0045Assoc") + } + cluster, account, user, partition := parseAssocKey(key) + r.Cluster = nonEmptyPtr(cluster) + r.Account = nonEmptyPtr(account) + r.User = user + r.Partition = nonEmptyPtr(partition) + _, err := c.CreateAssoc(ctx, r) + return err } // DeleteAssoc implements AssocInterface using a filter param set. func (c *SlurmClient) DeleteAssoc(ctx context.Context, assoc api.V0045Assoc) error { params := &api.SlurmdbV0045DeleteAssociationParams{ Account: assoc.Account, - User: ptr.To(assoc.User), + User: nonEmptyPtr(assoc.User), Cluster: assoc.Cluster, Partition: assoc.Partition, } @@ -69,6 +107,41 @@ func (c *SlurmClient) DeleteAssoc(ctx context.Context, assoc api.V0045Assoc) err return nil } +// GetAssoc implements AssocInterface. There is no get-by-key endpoint, so the +// composite key is decomposed into server-side filter params to avoid listing +// every association. +func (c *SlurmClient) GetAssoc(ctx context.Context, key string) (*types.V0045Assoc, error) { + cluster, account, user, partition := parseAssocKey(key) + params := &api.SlurmdbV0045GetAssociationsParams{ + Account: nonEmptyPtr(account), + Cluster: nonEmptyPtr(cluster), + User: nonEmptyPtr(user), + Partition: nonEmptyPtr(partition), + } + res, err := c.SlurmdbV0045GetAssociationsWithResponse(ctx, params) + if err != nil { + return nil, err + } + if res.StatusCode() != 200 { + errs := []error{errors.New(http.StatusText(res.StatusCode()))} + if res.JSONDefault != nil { + errs = append(errs, getOpenapiErrors(res.JSONDefault.Errors)...) + } + return nil, utilerrors.NewAggregate(errs) + } + if res.JSON200 == nil { + return nil, errors.New(http.StatusText(http.StatusNotFound)) + } + for _, item := range res.JSON200.Associations { + out := &types.V0045Assoc{} + utils.RemarshalOrDie(item, out) + if string(out.GetKey()) == key { + return out, nil + } + } + return nil, errors.New(http.StatusText(http.StatusNotFound)) +} + // ListAssoc implements AssocInterface. func (c *SlurmClient) ListAssoc(ctx context.Context) (*types.V0045AssocList, error) { params := &api.SlurmdbV0045GetAssociationsParams{} diff --git a/pkg/client/api/v0045/association_test.go b/pkg/client/api/v0045/association_test.go index ddbd657..1a8617b 100644 --- a/pkg/client/api/v0045/association_test.go +++ b/pkg/client/api/v0045/association_test.go @@ -88,26 +88,197 @@ func TestSlurmClient_CreateAssoc(t *testing.T) { tests := []struct { name string req any + wantKey string wantErr bool }{ - {name: "default", req: api.V0045Assoc{User: "alice", Account: ptr.To("research")}, wantErr: false}, + { + name: "returns composite key from request", + req: api.V0045Assoc{Cluster: ptr.To("linux"), Account: ptr.To("research"), User: "alice"}, + wantKey: "linux/research/alice/", + wantErr: false, + }, {name: "invalid type provided", req: api.V0045Account{Name: "research"}, wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} - err := c.CreateAssoc(context.Background(), tt.req) + gotKey, err := c.CreateAssoc(context.Background(), tt.req) if (err != nil) != tt.wantErr { t.Errorf("SlurmClient.CreateAssoc() error = %v, wantErr %v", err, tt.wantErr) } + if gotKey != tt.wantKey { + t.Errorf("SlurmClient.CreateAssoc() key = %q, want %q", gotKey, tt.wantKey) + } }) } } +func TestSlurmClient_UpdateAssoc(t *testing.T) { + var gotBody api.V0045OpenapiAssocsResp + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0045PostAssociationsWithResponse: func(ctx context.Context, body api.V0045OpenapiAssocsResp, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0045PostAssociationsResponse, error) { + gotBody = body + return &api.SlurmdbV0045PostAssociationsResponse{HTTPResponse: &fake.HttpSuccess}, nil + }, + }). + Build()} + + // req identity fields deliberately differ from key; key must win. + req := api.V0045Assoc{Cluster: ptr.To("other"), Account: ptr.To("other"), User: "bob"} + if err := c.UpdateAssoc(context.Background(), "linux/research/alice/", req); err != nil { + t.Fatalf("SlurmClient.UpdateAssoc() error = %v", err) + } + if len(gotBody.Associations) != 1 { + t.Fatalf("SlurmClient.UpdateAssoc() posted %d associations, want 1", len(gotBody.Associations)) + } + got := gotBody.Associations[0] + if ptr.Deref(got.Cluster, "") != "linux" || ptr.Deref(got.Account, "") != "research" || got.User != "alice" || got.Partition != nil { + t.Errorf("SlurmClient.UpdateAssoc() pinned identity = %+v, want cluster=linux account=research user=alice partition=nil", got) + } + + // invalid type provided + if err := c.UpdateAssoc(context.Background(), "linux/research/alice/", api.V0045Account{Name: "x"}); err == nil { + t.Errorf("SlurmClient.UpdateAssoc() expected error for invalid req type") + } +} + +func TestSlurmClient_GetAssoc(t *testing.T) { + matching := func() api.ClientWithResponsesInterface { + return fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0045GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0045GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0045GetAssociationsResponse, error) { + return &api.SlurmdbV0045GetAssociationsResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0045OpenapiAssocsResp{ + Associations: api.V0045AssocList{ + {Cluster: ptr.To("linux"), Account: ptr.To("research"), User: "alice"}, + }, + }, + }, nil + }, + }). + Build() + } + tests := []struct { + name string + key string + client api.ClientWithResponsesInterface + want *types.V0045Assoc + wantErr bool + }{ + { + name: "found by key", + key: "linux/research/alice/", + client: matching(), + want: &types.V0045Assoc{V0045Assoc: api.V0045Assoc{Cluster: ptr.To("linux"), Account: ptr.To("research"), User: "alice"}}, + }, + { + name: "not found - empty result", + key: "linux/research/bob/", + client: fake.NewFakeClient(), + wantErr: true, + }, + { + name: "not found - no key match", + key: "linux/research/bob/", + client: matching(), + wantErr: true, + }, + { + name: "HTTP error", + key: "linux/research/alice/", + client: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0045GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0045GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0045GetAssociationsResponse, error) { + return nil, errors.New(http.StatusText(http.StatusBadGateway)) + }, + }). + Build(), + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &SlurmClient{ClientWithResponsesInterface: tt.client} + got, err := c.GetAssoc(context.Background(), tt.key) + if (err != nil) != tt.wantErr { + t.Errorf("SlurmClient.GetAssoc() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && !reflect.DeepEqual(got, tt.want) { + t.Errorf("SlurmClient.GetAssoc() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestSlurmClient_GetAssoc_FiltersServerSide(t *testing.T) { + var gotParams *api.SlurmdbV0045GetAssociationsParams + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0045GetAssociationsWithResponse: func(ctx context.Context, params *api.SlurmdbV0045GetAssociationsParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0045GetAssociationsResponse, error) { + gotParams = params + return &api.SlurmdbV0045GetAssociationsResponse{ + HTTPResponse: &fake.HttpSuccess, + JSON200: &api.V0045OpenapiAssocsResp{ + Associations: api.V0045AssocList{ + {Cluster: ptr.To("linux"), Account: ptr.To("research"), User: "alice"}, + }, + }, + }, nil + }, + }). + Build()} + + if _, err := c.GetAssoc(context.Background(), "linux/research/alice/"); err != nil { + t.Fatalf("SlurmClient.GetAssoc() error = %v", err) + } + if gotParams == nil { + t.Fatal("SlurmClient.GetAssoc() did not call the associations endpoint") + } + if ptr.Deref(gotParams.Cluster, "") != "linux" || ptr.Deref(gotParams.Account, "") != "research" || ptr.Deref(gotParams.User, "") != "alice" { + t.Errorf("SlurmClient.GetAssoc() filter params = %+v, want cluster=linux account=research user=alice", gotParams) + } + if gotParams.Partition != nil { + t.Errorf("SlurmClient.GetAssoc() partition param = %v, want nil", gotParams.Partition) + } +} + func TestSlurmClient_DeleteAssoc(t *testing.T) { - c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClient()} - err := c.DeleteAssoc(context.Background(), api.V0045Assoc{User: "alice", Account: ptr.To("research")}) - if err != nil { - t.Errorf("SlurmClient.DeleteAssoc() error = %v, wantErr false", err) + tests := []struct { + name string + assoc api.V0045Assoc + wantUserParam *string + }{ + { + name: "with user", + assoc: api.V0045Assoc{Account: ptr.To("research"), User: "alice"}, + wantUserParam: ptr.To("alice"), + }, + { + name: "empty user omitted", + assoc: api.V0045Assoc{Account: ptr.To("research")}, + wantUserParam: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var gotParams *api.SlurmdbV0045DeleteAssociationParams + c := &SlurmClient{ClientWithResponsesInterface: fake.NewFakeClientBuilder(). + WithInterceptorFuncs(interceptor.Funcs{ + SlurmdbV0045DeleteAssociationWithResponse: func(ctx context.Context, params *api.SlurmdbV0045DeleteAssociationParams, reqEditors ...api.RequestEditorFn) (*api.SlurmdbV0045DeleteAssociationResponse, error) { + gotParams = params + return &api.SlurmdbV0045DeleteAssociationResponse{HTTPResponse: &fake.HttpSuccess, JSON200: &api.V0045OpenapiAssocsRemovedResp{}}, nil + }, + }). + Build()} + if err := c.DeleteAssoc(context.Background(), tt.assoc); err != nil { + t.Fatalf("SlurmClient.DeleteAssoc() error = %v", err) + } + if !reflect.DeepEqual(gotParams.User, tt.wantUserParam) { + t.Errorf("SlurmClient.DeleteAssoc() User param = %v, want %v", ptr.Deref(gotParams.User, ""), ptr.Deref(tt.wantUserParam, "")) + } + }) } } diff --git a/pkg/client/client.go b/pkg/client/client.go index f56e606..cfe120b 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -174,8 +174,9 @@ func (c *client) Create( key = object.ObjectKey(name) case *types.V0042Assoc: - err = c.v0042Client.CreateAssoc(ctx, req) - key = obj.GetKey() + var assocKey string + assocKey, err = c.v0042Client.CreateAssoc(ctx, req) + key = object.ObjectKey(assocKey) ///////////////////////////////////////////////////////////////////////////////// @@ -195,8 +196,9 @@ func (c *client) Create( key = object.ObjectKey(name) case *types.V0043Assoc: - err = c.v0043Client.CreateAssoc(ctx, req) - key = obj.GetKey() + var assocKey string + assocKey, err = c.v0043Client.CreateAssoc(ctx, req) + key = object.ObjectKey(assocKey) ///////////////////////////////////////////////////////////////////////////////// @@ -226,8 +228,9 @@ func (c *client) Create( key = object.ObjectKey(name) case *types.V0044Assoc: - err = c.v0044Client.CreateAssoc(ctx, req) - key = obj.GetKey() + var assocKey string + assocKey, err = c.v0044Client.CreateAssoc(ctx, req) + key = object.ObjectKey(assocKey) ///////////////////////////////////////////////////////////////////////////////// @@ -257,8 +260,9 @@ func (c *client) Create( key = object.ObjectKey(name) case *types.V0045Assoc: - err = c.v0045Client.CreateAssoc(ctx, req) - key = obj.GetKey() + var assocKey string + assocKey, err = c.v0045Client.CreateAssoc(ctx, req) + key = object.ObjectKey(assocKey) ///////////////////////////////////////////////////////////////////////////////// @@ -390,7 +394,7 @@ func (c *client) Update( case *types.V0042User: err = c.v0042Client.UpdateUser(ctx, key, req) case *types.V0042Assoc: - err = c.v0042Client.CreateAssoc(ctx, req) + err = c.v0042Client.UpdateAssoc(ctx, key, req) ///////////////////////////////////////////////////////////////////////////////// @@ -403,7 +407,7 @@ func (c *client) Update( case *types.V0043User: err = c.v0043Client.UpdateUser(ctx, key, req) case *types.V0043Assoc: - err = c.v0043Client.CreateAssoc(ctx, req) + err = c.v0043Client.UpdateAssoc(ctx, key, req) ///////////////////////////////////////////////////////////////////////////////// @@ -418,7 +422,7 @@ func (c *client) Update( case *types.V0044User: err = c.v0044Client.UpdateUser(ctx, key, req) case *types.V0044Assoc: - err = c.v0044Client.CreateAssoc(ctx, req) + err = c.v0044Client.UpdateAssoc(ctx, key, req) ///////////////////////////////////////////////////////////////////////////////// @@ -433,7 +437,7 @@ func (c *client) Update( case *types.V0045User: err = c.v0045Client.UpdateUser(ctx, key, req) case *types.V0045Assoc: - err = c.v0045Client.CreateAssoc(ctx, req) + err = c.v0045Client.UpdateAssoc(ctx, key, req) ///////////////////////////////////////////////////////////////////////////////// @@ -514,21 +518,11 @@ func (c *client) Get( } *o = *out case *types.V0042Assoc: - list, err := c.v0042Client.ListAssoc(ctx) + out, err := c.v0042Client.GetAssoc(ctx, string(key)) if err != nil { return err } - found := false - for i := range list.Items { - if list.Items[i].GetKey() == key { - *o = list.Items[i] - found = true - break - } - } - if !found { - return errors.New(http.StatusText(http.StatusNotFound)) - } + *o = *out case *types.V0042Stats: out, err := c.v0042Client.GetStats(ctx) if err != nil { @@ -581,21 +575,11 @@ func (c *client) Get( } *o = *out case *types.V0043Assoc: - list, err := c.v0043Client.ListAssoc(ctx) + out, err := c.v0043Client.GetAssoc(ctx, string(key)) if err != nil { return err } - found := false - for i := range list.Items { - if list.Items[i].GetKey() == key { - *o = list.Items[i] - found = true - break - } - } - if !found { - return errors.New(http.StatusText(http.StatusNotFound)) - } + *o = *out case *types.V0043Stats: out, err := c.v0043Client.GetStats(ctx) if err != nil { @@ -660,21 +644,11 @@ func (c *client) Get( } *o = *out case *types.V0044Assoc: - list, err := c.v0044Client.ListAssoc(ctx) + out, err := c.v0044Client.GetAssoc(ctx, string(key)) if err != nil { return err } - found := false - for i := range list.Items { - if list.Items[i].GetKey() == key { - *o = list.Items[i] - found = true - break - } - } - if !found { - return errors.New(http.StatusText(http.StatusNotFound)) - } + *o = *out case *types.V0044Stats: out, err := c.v0044Client.GetStats(ctx) if err != nil { @@ -739,21 +713,11 @@ func (c *client) Get( } *o = *out case *types.V0045Assoc: - list, err := c.v0045Client.ListAssoc(ctx) + out, err := c.v0045Client.GetAssoc(ctx, string(key)) if err != nil { return err } - found := false - for i := range list.Items { - if list.Items[i].GetKey() == key { - *o = list.Items[i] - found = true - break - } - } - if !found { - return errors.New(http.StatusText(http.StatusNotFound)) - } + *o = *out case *types.V0045Stats: out, err := c.v0045Client.GetStats(ctx) if err != nil {