diff --git a/apiv2/pkg/clients/immutable/immutable.go b/apiv2/pkg/clients/immutable/immutable.go new file mode 100644 index 00000000..9d6a689b --- /dev/null +++ b/apiv2/pkg/clients/immutable/immutable.go @@ -0,0 +1,131 @@ +package immutable + +import ( + "context" + immutableapi "github.com/mittwald/goharbor-client/v5/apiv2/internal/api/client/immutable" + "github.com/mittwald/goharbor-client/v5/apiv2/pkg/config" + + v2client "github.com/mittwald/goharbor-client/v5/apiv2/internal/api/client" + "github.com/mittwald/goharbor-client/v5/apiv2/model" + + "github.com/go-openapi/runtime" +) + +type RESTClient struct { + // Options contains optional configuration when making API calls. + Options *config.Options + + // The new client of the harbor v2 API + V2Client *v2client.Harbor + + // AuthInfo contains the auth information that is provided on API calls. + AuthInfo runtime.ClientAuthInfoWriter +} + +func NewClient(v2Client *v2client.Harbor, opts *config.Options, authInfo runtime.ClientAuthInfoWriter) *RESTClient { + return &RESTClient{ + Options: opts, + V2Client: v2Client, + AuthInfo: authInfo, + } +} + +type Client interface { + CreateImmuRule(ctx context.Context, projectNameOrID string, immutableRule *model.ImmutableRule) error + UpdateImmuRule(ctx context.Context, projectNameOrID string, immutableRule *model.ImmutableRule) error + DeleteImmuRule(ctx context.Context, projectNameOrID string, immutableRuleID int64) error + ListImmuRules(ctx context.Context, projectNameOrID string) ([]*model.ImmutableRule, error) +} + +func (c *RESTClient) CreateImmuRule(ctx context.Context, projectNameOrID string, immutableRule *model.ImmutableRule) error { + params := &immutableapi.CreateImmuRuleParams{ + ProjectNameOrID: projectNameOrID, + ImmutableRule: immutableRule, + Context: ctx, + } + + params.WithTimeout(c.Options.Timeout) + + _, err := c.V2Client.Immutable.CreateImmuRule(params, c.AuthInfo) + if err != nil { + return handleSwaggerImmutableRuleErrors(err) + } + + return nil +} + +func (c *RESTClient) UpdateImmuRule(ctx context.Context, projectNameOrID string, immutableRule *model.ImmutableRule, immutableRuleID int64) error { + params := &immutableapi.UpdateImmuRuleParams{ + ProjectNameOrID: projectNameOrID, + ImmutableRule: immutableRule, + ImmutableRuleID: immutableRuleID, + Context: ctx, + } + + params.WithTimeout(c.Options.Timeout) + + params.ImmutableRule.ID = immutableRuleID + + _, err := c.V2Client.Immutable.UpdateImmuRule(params, c.AuthInfo) + if err != nil { + return handleSwaggerImmutableRuleErrors(err) + } + + return nil +} + +func (c *RESTClient) DeleteImmuRule(ctx context.Context, projectNameOrID string, immutableRuleID int64) error { + params := &immutableapi.DeleteImmuRuleParams{ + ProjectNameOrID: projectNameOrID, + ImmutableRuleID: immutableRuleID, + Context: ctx, + } + + params.WithTimeout(c.Options.Timeout) + + _, err := c.V2Client.Immutable.DeleteImmuRule(params, c.AuthInfo) + if err != nil { + return handleSwaggerImmutableRuleErrors(err) + } + + return nil +} + +func (c *RESTClient) ListImmuRules(ctx context.Context, projectNameOrID string) ([]*model.ImmutableRule, error) { + var immutableRules []*model.ImmutableRule + page := c.Options.Page + + params := &immutableapi.ListImmuRulesParams{ + Page: &page, + PageSize: &c.Options.PageSize, + ProjectNameOrID: projectNameOrID, + Q: &c.Options.Query, + Sort: &c.Options.Sort, + Context: ctx, + } + + params.WithTimeout(c.Options.Timeout) + + for { + resp, err := c.V2Client.Immutable.ListImmuRules(params, c.AuthInfo) + if err != nil { + return nil, handleSwaggerImmutableRuleErrors(err) + } + + if len(resp.Payload) == 0 { + break + } + + totalCount := resp.XTotalCount + + immutableRules = append(immutableRules, resp.Payload...) + + if int64(len(immutableRules)) >= totalCount { + break + } + + page++ + } + + return immutableRules, nil +} diff --git a/apiv2/pkg/clients/immutable/immutable_errors.go b/apiv2/pkg/clients/immutable/immutable_errors.go new file mode 100644 index 00000000..a98212cd --- /dev/null +++ b/apiv2/pkg/clients/immutable/immutable_errors.go @@ -0,0 +1,42 @@ +package immutable + +import ( + "net/http" + + "github.com/go-openapi/runtime" + "github.com/mittwald/goharbor-client/v5/apiv2/internal/api/client/immutable" + "github.com/mittwald/goharbor-client/v5/apiv2/pkg/errors" +) + +func handleSwaggerImmutableRuleErrors(in error) error { + t, ok := in.(*runtime.APIError) + if ok { + switch t.Code { + case http.StatusOK: + return nil + case http.StatusCreated: + return nil + case http.StatusBadRequest: + return &errors.ErrBadRequest{} + case http.StatusUnauthorized: + return &errors.ErrUnauthorized{} + case http.StatusForbidden: + return &errors.ErrForbidden{} + case http.StatusNotFound: + return &errors.ErrNotFound{} + case http.StatusInternalServerError: + return &errors.ErrInternalErrors{} + } + } + + switch in.(type) { + case *immutable.UpdateImmuRuleBadRequest: + return &errors.ErrBadRequest{} + case *immutable.CreateImmuRuleBadRequest: + return &errors.ErrBadRequest{} + case *immutable.DeleteImmuRuleBadRequest: + return &errors.ErrBadRequest{} + default: + return in + } +} \ No newline at end of file diff --git a/apiv2/pkg/clients/immutable/immutable_integration_test.go b/apiv2/pkg/clients/immutable/immutable_integration_test.go new file mode 100644 index 00000000..9449b001 --- /dev/null +++ b/apiv2/pkg/clients/immutable/immutable_integration_test.go @@ -0,0 +1,116 @@ +//go:build integration + +package immutable + +import ( + "context" + "testing" + "github.com/mittwald/goharbor-client/v5/apiv2/model" + "github.com/mittwald/goharbor-client/v5/apiv2/pkg/clients/project" + clienttesting "github.com/mittwald/goharbor-client/v5/apiv2/pkg/testing" + "github.com/stretchr/testify/require" +) + +var projectName = "test-project" + +func TestAPIImmutableListImmutableRules(t *testing.T) { + ctx := context.Background() + + c := NewClient(clienttesting.V2SwaggerClient, clienttesting.DefaultOpts, clienttesting.AuthInfo) + pc := project.NewClient(clienttesting.V2SwaggerClient, clienttesting.DefaultOpts, clienttesting.AuthInfo) + + err := pc.NewProject(ctx, &model.ProjectReq{ + ProjectName: projectName, + }) + require.NoError(t, err) + + defer pc.DeleteProject(ctx, projectName) + + immutableRule := model.ImmutableRule{ + ScopeSelectors: map[string][]model.ImmutableSelector{}, + TagSelectors: []*model.ImmutableSelector{{ + Decoration: "matches", + Kind: "doublestar", + Pattern: "**", + }}, + } + + err = c.CreateImmuRule(ctx, projectName, &immutableRule) + require.NoError(t, err) + + listedImmutableRules, err := c.ListImmuRules(ctx, projectName) + + require.NoError(t, err) + + listedImmutableRuleTag := listedImmutableRules[0].TagSelectors + + require.Equal(t, listedImmutableRuleTag[0], immutableRule.TagSelectors[0]) + + immuRuleID := listedImmutableRules[0].ID + + c.DeleteImmuRule(ctx, projectName, immuRuleID) + + checkDeletedImmuRules, err := c.ListImmuRules(ctx, projectName) + + require.Empty(t, checkDeletedImmuRules) +} + +func TestAPIImmutableUpdateImmutableRules(t *testing.T) { + ctx := context.Background() + + c := NewClient(clienttesting.V2SwaggerClient, clienttesting.DefaultOpts, clienttesting.AuthInfo) + pc := project.NewClient(clienttesting.V2SwaggerClient, clienttesting.DefaultOpts, clienttesting.AuthInfo) + + err := pc.NewProject(ctx, &model.ProjectReq{ + ProjectName: projectName, + }) + require.NoError(t, err) + + defer pc.DeleteProject(ctx, projectName) + + createImmutableRule := model.ImmutableRule{ + ScopeSelectors: map[string][]model.ImmutableSelector{}, + TagSelectors: []*model.ImmutableSelector{{ + Decoration: "matches", + Kind: "doublestar", + Pattern: "1.0.0", + }}, + } + + updateImmutableRule := model.ImmutableRule{ + ScopeSelectors: map[string][]model.ImmutableSelector{}, + TagSelectors: []*model.ImmutableSelector{{ + Decoration: "matches", + Kind: "doublestar", + Pattern: "2.0.0", + }}, + } + + err = c.CreateImmuRule(ctx, projectName, &createImmutableRule) + require.NoError(t, err) + + listedImmutableRules, err := c.ListImmuRules(ctx, projectName) + + require.NoError(t, err) + + immuRuleID := listedImmutableRules[0].ID + + err = c.UpdateImmuRule(ctx, projectName, &updateImmutableRule, immuRuleID) + + require.NoError(t, err) + + listedImmutableRules, err = c.ListImmuRules(ctx, projectName) + + require.NoError(t, err) + + listedImmutableRuleTag := listedImmutableRules[0].TagSelectors + + require.Equal(t, listedImmutableRuleTag[0], updateImmutableRule.TagSelectors[0]) + + + c.DeleteImmuRule(ctx, projectName, immuRuleID) + + checkDeletedImmuRules, err := c.ListImmuRules(ctx, projectName) + + require.Empty(t, checkDeletedImmuRules) +} \ No newline at end of file diff --git a/apiv2/pkg/clients/immutable/immutable_test.go b/apiv2/pkg/clients/immutable/immutable_test.go new file mode 100644 index 00000000..22d18c55 --- /dev/null +++ b/apiv2/pkg/clients/immutable/immutable_test.go @@ -0,0 +1,292 @@ +//go:build !integration + +package immutable + +import ( + "context" + "strconv" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/stretchr/testify/mock" + + "github.com/mittwald/goharbor-client/v5/apiv2/internal/api/client/immutable" + "github.com/mittwald/goharbor-client/v5/apiv2/mocks" + "github.com/mittwald/goharbor-client/v5/apiv2/model" + clienttesting "github.com/mittwald/goharbor-client/v5/apiv2/pkg/testing" + "github.com/mittwald/goharbor-client/v5/apiv2/pkg/errors" +) + +var ( + ctx = context.Background() + projectID = 1 + immutableRuleID = 100 +) + +func APIandMockClientsForTests() (*RESTClient, *clienttesting.MockClients) { + desiredMockClients := &clienttesting.MockClients{ + Immutable: mocks.MockImmutableClientService{}, + } + + v2Client := clienttesting.BuildV2ClientWithMocks(desiredMockClients) + + cl := NewClient(v2Client, clienttesting.DefaultOpts, clienttesting.AuthInfo) + + return cl, desiredMockClients +} + +func TestRESTClient_CreateImmuRule(t *testing.T) { + apiClient, mockClient := APIandMockClientsForTests() + + params := &immutable.CreateImmuRuleParams{ + ProjectNameOrID: strconv.Itoa(projectID), + ImmutableRule: &model.ImmutableRule{ + TagSelectors: []*model.ImmutableSelector{{ + Decoration: "matches", + Kind: "doublestar", + Pattern: "1.0.0", + }}, + ScopeSelectors: map[string][]model.ImmutableSelector{ + "repository": {{ + Decoration: "repoMatches", + Kind: "doublestar", + Pattern: "**", + }}, + }, + }, + Context: ctx, + } + + params.WithTimeout(apiClient.Options.Timeout) + + mockClient.Immutable.On("CreateImmuRule", params, mock.AnythingOfType("runtime.ClientAuthInfoWriterFunc")). + Return(&immutable.CreateImmuRuleCreated{}, nil) + + err := apiClient.CreateImmuRule(ctx, strconv.Itoa(projectID), params.ImmutableRule) + + require.NoError(t, err) + + mockClient.Immutable.AssertExpectations(t) +} + +func TestRESTClient_CreateImmuRuleError(t *testing.T) { + apiClient, mockClient := APIandMockClientsForTests() + + params := &immutable.CreateImmuRuleParams{ + ProjectNameOrID: strconv.Itoa(projectID), + ImmutableRule: &model.ImmutableRule{ + TagSelectors: []*model.ImmutableSelector{{ + Decoration: "matches", + Kind: "doublestar", + Pattern: "1.0.0", + }}, + ScopeSelectors: map[string][]model.ImmutableSelector{ + "repository": {{ + Decoration: "repoMatches", + Kind: "doublestar", + Pattern: "**", + }}, + }, + }, + Context: ctx, + } + + params.WithTimeout(apiClient.Options.Timeout) + + mockClient.Immutable.On("CreateImmuRule", params, mock.AnythingOfType("runtime.ClientAuthInfoWriterFunc")). + Return(nil, &immutable.CreateImmuRuleBadRequest{}) + + err := apiClient.CreateImmuRule(ctx, strconv.Itoa(projectID), params.ImmutableRule) + + require.Error(t, err) + + mockClient.Immutable.AssertExpectations(t) +} + +func TestRESTClient_UpdateImmuRule(t *testing.T) { + apiClient, mockClient := APIandMockClientsForTests() + + params := &immutable.UpdateImmuRuleParams{ + ImmutableRuleID: int64(immutableRuleID), + ProjectNameOrID: strconv.Itoa(projectID), + ImmutableRule: &model.ImmutableRule{ + ID: int64(immutableRuleID), + TagSelectors: []*model.ImmutableSelector{{ + Decoration: "matches", + Kind: "doublestar", + Pattern: "1.0.0", + }}, + ScopeSelectors: map[string][]model.ImmutableSelector{ + "repository": {{ + Decoration: "repoMatches", + Kind: "doublestar", + Pattern: "**", + }}, + }, + }, + Context: ctx, + } + + params.WithTimeout(apiClient.Options.Timeout) + + mockClient.Immutable.On("UpdateImmuRule", params, mock.AnythingOfType("runtime.ClientAuthInfoWriterFunc")). + Return(&immutable.UpdateImmuRuleOK{}, nil) + + err := apiClient.UpdateImmuRule(ctx, strconv.Itoa(projectID), params.ImmutableRule, int64(immutableRuleID)) + + require.NoError(t, err) + + mockClient.Immutable.AssertExpectations(t) +} + +func TestRESTClient_UpdateImmuRuleNotFound(t *testing.T) { + apiClient, mockClient := APIandMockClientsForTests() + + params := &immutable.UpdateImmuRuleParams{ + ImmutableRuleID: int64(immutableRuleID), + ProjectNameOrID: strconv.Itoa(projectID), + ImmutableRule: &model.ImmutableRule{ + TagSelectors: []*model.ImmutableSelector{{ + Decoration: "matches", + Kind: "doublestar", + Pattern: "1.0.0", + }}, + ScopeSelectors: map[string][]model.ImmutableSelector{ + "repository": {{ + Decoration: "repoMatches", + Kind: "doublestar", + Pattern: "**", + }}, + }, + }, + Context: ctx, + } + + params.WithTimeout(apiClient.Options.Timeout) + + mockClient.Immutable.On("UpdateImmuRule", params, mock.AnythingOfType("runtime.ClientAuthInfoWriterFunc")). + Return(nil, &errors.ErrNotFound{}) + + err := apiClient.UpdateImmuRule(ctx, strconv.Itoa(projectID), params.ImmutableRule, int64(immutableRuleID)) + + require.Error(t, err) + + mockClient.Immutable.AssertExpectations(t) +} + +func TestRESTClient_UpdateImmuRuleBadRequest(t *testing.T) { + apiClient, mockClient := APIandMockClientsForTests() + + params := &immutable.UpdateImmuRuleParams{ + ImmutableRuleID: int64(immutableRuleID), + ProjectNameOrID: strconv.Itoa(projectID), + ImmutableRule: &model.ImmutableRule{ + TagSelectors: []*model.ImmutableSelector{{ + Decoration: "matches", + Kind: "doublestar", + Pattern: "1.0.0", + }}, + ScopeSelectors: map[string][]model.ImmutableSelector{ + "repository": {{ + Decoration: "repoMatches", + Kind: "doublestar", + Pattern: "**", + }}, + }, + }, + Context: ctx, + } + + params.WithTimeout(apiClient.Options.Timeout) + + mockClient.Immutable.On("UpdateImmuRule", params, mock.AnythingOfType("runtime.ClientAuthInfoWriterFunc")). + Return(nil, &errors.ErrBadRequest{}) + + err := apiClient.UpdateImmuRule(ctx, strconv.Itoa(projectID), params.ImmutableRule, int64(immutableRuleID)) + + require.Error(t, err) + + mockClient.Immutable.AssertExpectations(t) +} + +func TestRESTClient_UpdateImmuRuleUnauthorized(t *testing.T) { + apiClient, mockClient := APIandMockClientsForTests() + + params := &immutable.UpdateImmuRuleParams{ + ImmutableRuleID: int64(immutableRuleID), + ProjectNameOrID: strconv.Itoa(projectID), + ImmutableRule: &model.ImmutableRule{}, + Context: ctx, + } + + params.WithTimeout(apiClient.Options.Timeout) + + mockClient.Immutable.On("UpdateImmuRule", params, mock.AnythingOfType("runtime.ClientAuthInfoWriterFunc")). + Return(nil, &errors.ErrUnauthorized{}) + + err := apiClient.UpdateImmuRule(ctx, strconv.Itoa(projectID), params.ImmutableRule, int64(immutableRuleID)) + + require.Error(t, err) + + mockClient.Immutable.AssertExpectations(t) +} + +func TestRESTClient_DeleteImmuRule(t *testing.T) { + apiClient, mockClient := APIandMockClientsForTests() + + params := &immutable.DeleteImmuRuleParams{ + ImmutableRuleID: int64(immutableRuleID), + ProjectNameOrID: strconv.Itoa(projectID), + Context: ctx, + } + + params.WithTimeout(apiClient.Options.Timeout) + + mockClient.Immutable.On("DeleteImmuRule", params, mock.AnythingOfType("runtime.ClientAuthInfoWriterFunc")). + Return(&immutable.DeleteImmuRuleOK{}, nil) + + err := apiClient.DeleteImmuRule(ctx, strconv.Itoa(projectID), int64(immutableRuleID)) + + require.NoError(t, err) + + mockClient.Immutable.AssertExpectations(t) +} + +func TestRESTClient_ListImmuRules(t *testing.T) { + apiClient, mockClient := APIandMockClientsForTests() + + expectedListImmutableRule := model.ImmutableRule{ + Action: "immutable", + ID: 1, + ScopeSelectors: map[string][]model.ImmutableSelector{}, + TagSelectors: []*model.ImmutableSelector{{ + Decoration: "matches", + Kind: "doublestar", + Pattern: "**", + }}, + Template: "immutable_template", + } + + params := &immutable.ListImmuRulesParams{ + Page: &apiClient.Options.Page, + PageSize: &apiClient.Options.PageSize, + ProjectNameOrID: strconv.Itoa(projectID), + Q: &apiClient.Options.Query, + Sort: &apiClient.Options.Sort, + Context: ctx, + } + + params.WithTimeout(apiClient.Options.Timeout) + + mockClient.Immutable.On("ListImmuRules", params, mock.AnythingOfType("runtime.ClientAuthInfoWriterFunc")). + Return(&immutable.ListImmuRulesOK{Payload: []*model.ImmutableRule{&expectedListImmutableRule}}, nil) + + immutableRules, err := apiClient.ListImmuRules(ctx, strconv.Itoa(projectID)) + + require.NoError(t, err) + + require.Equal(t, expectedListImmutableRule, *immutableRules[0]) + + mockClient.Immutable.AssertExpectations(t) +}