diff --git a/internal/clioptions/clioptions.go b/internal/clioptions/clioptions.go index 2b592eb1..03fdb271 100644 --- a/internal/clioptions/clioptions.go +++ b/internal/clioptions/clioptions.go @@ -61,6 +61,8 @@ type CLIOptions struct { UserEmails []string UserIDs []string + Page int + ServiceAccountID string BasicClientID string @@ -79,6 +81,9 @@ type CLIOptions struct { MarketplaceItemObjectID string MarketplaceFetchPublicItems bool + ItemTypeDefinitionName string + ItemTypeDefinitionFilePath string + FromCronJob string FollowLogs bool @@ -250,6 +255,12 @@ func (o *CLIOptions) AddPublicFlag(flags *pflag.FlagSet) (flagName string) { return } +func (o *CLIOptions) AddPageFlag(flags *pflag.FlagSet) (flagName string) { + flagName = "page" + flags.IntVar(&o.Page, flagName, 1, "specify the page to fetch") + return +} + func (o *CLIOptions) AddMarketplaceItemObjectIDFlag(flags *pflag.FlagSet) (flagName string) { flagName = "object-id" flags.StringVar(&o.MarketplaceItemObjectID, flagName, "", "The _id of the Marketplace item") @@ -262,6 +273,21 @@ func (o *CLIOptions) AddMarketplaceVersionFlag(flags *pflag.FlagSet) (flagName s return } +func (o *CLIOptions) AddItemTypeDefinitionNameFlag(flags *pflag.FlagSet) (flagName string) { + flagName = "name" + flags.StringVarP(&o.ItemTypeDefinitionName, flagName, "i", "", "The name of the Item Type Definition") + return +} + +func (o *CLIOptions) AddItemTypeDefinitionFileFlag(cmd *cobra.Command) { + cmd.Flags().StringVarP(&o.ItemTypeDefinitionFilePath, "file", "f", "", "paths to JSON/YAML file containing an item type definition") + err := cmd.MarkFlagRequired("file") + if err != nil { + // the error is only due to a programming error (missing command), hence panic + panic(err) + } +} + func (o *CLIOptions) AddCreateJobFlags(flags *pflag.FlagSet) { flags.StringVar(&o.FromCronJob, "from", "", "name of the cronjob to create a Job from") } diff --git a/internal/cmd/catalog/get.go b/internal/cmd/catalog/get.go index 4d1a3ad6..775e7ebf 100644 --- a/internal/cmd/catalog/get.go +++ b/internal/cmd/catalog/get.go @@ -36,7 +36,7 @@ const ( This command works with Mia-Platform Console v14.0.0 or later. - You need to specify the companyId, itemId and version, via the respective flags. The company-id flag can be omitted if it is already set in the context. + You need to specify the itemId, via the respective flag. The company-id flag can be omitted if it is already set in the context. ` cmdGetUse = "get { --item-id item-id --version version }" ) diff --git a/internal/cmd/catalog/list.go b/internal/cmd/catalog/list.go index c3eaf0bf..201d4862 100644 --- a/internal/cmd/catalog/list.go +++ b/internal/cmd/catalog/list.go @@ -30,8 +30,11 @@ const ( This command lists the Catalog items of a company. It works with Mia-Platform Console v14.0.0 or later. + Results are paginated. By default, only the first page is shown. + you can also specify the following flags: - --public - if this flag is set, the command fetches not only the items from the requested company, but also the public Catalog items from other companies. + - -- page - specify the page to fetch, default is 1 ` listCmdUse = "list --company-id company-id" ) @@ -46,6 +49,7 @@ func ListCmd(options *clioptions.CLIOptions) *cobra.Command { } options.AddPublicFlag(cmd.Flags()) + options.AddPageFlag(cmd.Flags()) return cmd } @@ -65,6 +69,7 @@ func runListCmd(options *clioptions.CLIOptions) func(cmd *cobra.Command, args [] marketplaceItemsOptions := commonMarketplace.GetMarketplaceItemsOptions{ CompanyID: restConfig.CompanyID, Public: options.MarketplaceFetchPublicItems, + Page: options.Page, } err = commonMarketplace.PrintMarketplaceItems(cmd.Context(), apiClient, marketplaceItemsOptions, options.Printer(), listMarketplaceEndpoint) diff --git a/internal/cmd/catalog/list_test.go b/internal/cmd/catalog/list_test.go index 2c738fd4..0b21fa67 100644 --- a/internal/cmd/catalog/list_test.go +++ b/internal/cmd/catalog/list_test.go @@ -157,6 +157,7 @@ func privateAndPublicMarketplaceHandler(t *testing.T) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if strings.EqualFold(r.URL.Path, "/api/marketplace/") && r.Method == http.MethodGet && + r.URL.Query().Get("page") == "1" && r.URL.Query().Get("includeTenantId") == "my-company" { _, err := w.Write([]byte(marketplaceItemsBodyContent(t))) require.NoError(t, err) @@ -172,6 +173,7 @@ func privateCompanyMarketplaceHandler(t *testing.T) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if strings.EqualFold(r.URL.Path, "/api/marketplace/") && r.Method == http.MethodGet && + r.URL.Query().Get("page") == "1" && r.URL.Query().Get("tenantId") == "my-company" { _, err := w.Write([]byte(marketplacePrivateCompanyBodyContent(t))) require.NoError(t, err) diff --git a/internal/cmd/common/marketplace/list.go b/internal/cmd/common/marketplace/list.go index 93b933fc..3d50bf7f 100644 --- a/internal/cmd/common/marketplace/list.go +++ b/internal/cmd/common/marketplace/list.go @@ -18,6 +18,7 @@ package marketplace import ( "context" "fmt" + "strconv" "github.com/mia-platform/miactl/internal/client" "github.com/mia-platform/miactl/internal/printer" @@ -28,6 +29,7 @@ import ( type GetMarketplaceItemsOptions struct { CompanyID string Public bool + Page int } func PrintMarketplaceItems(context context.Context, client *client.APIClient, options GetMarketplaceItemsOptions, p printer.IPrinter, endpoint string) error { @@ -89,6 +91,15 @@ func buildRequest(client *client.APIClient, options GetMarketplaceItemsOptions, request.SetParam("tenantId", options.CompanyID) } + // marketplace command API does not support pagination + if endpoint == "/api/marketplace/" { + if options.Page <= 0 { + request.SetParam("page", "1") + } else { + request.SetParam("page", strconv.Itoa(options.Page)) + } + } + return request } diff --git a/internal/cmd/item-type-definition/delete.go b/internal/cmd/item-type-definition/delete.go new file mode 100644 index 00000000..60107b2f --- /dev/null +++ b/internal/cmd/item-type-definition/delete.go @@ -0,0 +1,116 @@ +// Copyright Mia srl +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package itd + +import ( + "context" + "errors" + "fmt" + "net/http" + + "github.com/mia-platform/miactl/internal/client" + "github.com/mia-platform/miactl/internal/clioptions" + itd "github.com/mia-platform/miactl/internal/resources/item-type-definition" + "github.com/mia-platform/miactl/internal/util" + "github.com/spf13/cobra" +) + +var ( + ErrServerDeleteItem = errors.New("server error while deleting item type definition") + ErrUnexpectedDeleteItem = errors.New("unexpected response while deleting item") +) + +const ( + deleteItdEndpoint = "/api/tenants/%s/marketplace/item-type-definitions/%s/" + + cmdDeleteLongDescription = `Delete an Item Type Definition. It works with Mia-Platform Console v14.1.0 or later. + + You need to specify the companyId and the item type definition name via the respective flags (recommended). The company-id flag can be omitted if it is already set in the context. + ` + deleteCmdUse = "delete { --name name --version version }" +) + +func DeleteCmd(options *clioptions.CLIOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: deleteCmdUse, + Short: "Delete an Item Type Definition", + Long: cmdDeleteLongDescription, + SuggestFor: []string{"rm"}, + RunE: func(cmd *cobra.Command, _ []string) error { + restConfig, err := options.ToRESTConfig() + cobra.CheckErr(err) + client, err := client.APIClientForConfig(restConfig) + cobra.CheckErr(err) + + canUseNewAPI, versionError := util.VersionCheck(cmd.Context(), client, 14, 1) + if !canUseNewAPI || versionError != nil { + return itd.ErrUnsupportedCompanyVersion + } + + companyID := restConfig.CompanyID + if len(companyID) == 0 { + return itd.ErrMissingCompanyID + } + + if options.MarketplaceItemVersion != "" && options.MarketplaceItemID != "" { + err = deleteITD( + cmd.Context(), + client, + companyID, + options.ItemTypeDefinitionName, + ) + cobra.CheckErr(err) + return nil + } + + return errors.New("invalid input parameters") + }, + } + + ITDFlagName := options.AddItemTypeDefinitionNameFlag(cmd.Flags()) + + err := cmd.MarkFlagRequired(ITDFlagName) + if err != nil { + // the error is only due to a programming error (missing command flag), hence panic + panic(err) + } + + return cmd +} + +func deleteITD(ctx context.Context, client *client.APIClient, companyID, name string) error { + resp, err := client. + Delete(). + APIPath(fmt.Sprintf(deleteItdEndpoint, companyID, name)). + Do(ctx) + + if err != nil { + return fmt.Errorf("error executing request: %w", err) + } + + switch resp.StatusCode() { + case http.StatusNoContent: + fmt.Println("item deleted successfully") + return nil + case http.StatusNotFound: + return itd.ErrItemNotFound + default: + if resp.StatusCode() >= http.StatusInternalServerError { + return ErrServerDeleteItem + } + return fmt.Errorf("%w: %d", ErrUnexpectedDeleteItem, resp.StatusCode()) + } +} diff --git a/internal/cmd/item-type-definition/delete_test.go b/internal/cmd/item-type-definition/delete_test.go new file mode 100644 index 00000000..500a7c6d --- /dev/null +++ b/internal/cmd/item-type-definition/delete_test.go @@ -0,0 +1,164 @@ +// Copyright Mia srl +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package itd + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/mia-platform/miactl/internal/client" + "github.com/mia-platform/miactl/internal/clioptions" + itd "github.com/mia-platform/miactl/internal/resources/item-type-definition" + "github.com/stretchr/testify/require" +) + +const ( + mockDeleteCompanyID = "company-id" +) + +func TestDeleteResourceCmd(t *testing.T) { + t.Run("test command creation", func(t *testing.T) { + opts := clioptions.NewCLIOptions() + cmd := DeleteCmd(opts) + require.NotNil(t, cmd) + }) + + t.Run("should not run command when Console version is lower than 14.1.0", func(t *testing.T) { + server := httptest.NewServer(unexecutedCmdMockServer(t)) + defer server.Close() + + opts := clioptions.NewCLIOptions() + opts.CompanyID = mockDeleteCompanyID + opts.Endpoint = server.URL + + cmd := DeleteCmd(opts) + cmd.SetArgs([]string{"delete", "--name", "some-item-id"}) + + err := cmd.Execute() + require.ErrorIs(t, err, itd.ErrUnsupportedCompanyVersion) + }) +} + +func deleteByItemNameMockServer(t *testing.T, + statusCode int, + mockName string, + callsCount *int, +) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, + fmt.Sprintf(deleteItdEndpoint, mockDeleteCompanyID, mockName), + r.RequestURI, + ) + require.Equal(t, http.MethodDelete, r.Method) + w.WriteHeader(statusCode) + if statusCode != http.StatusNoContent { + w.Write([]byte(` + { + "message": "some error message" + } + `)) + } + *callsCount++ + })) +} + +func TestDeleteItemByItemIDAndVersion(t *testing.T) { + mockClientConfig := &client.Config{ + Transport: http.DefaultTransport, + } + testCases := []struct { + testName string + + statusCode int + + name string + + expectedErr error + expectedCalls int + }{ + { + testName: "should not return error if deletion is successful", + statusCode: http.StatusNoContent, + name: "plugin", + + expectedErr: nil, + expectedCalls: 1, + }, + { + testName: "should return not found error in case the item is not found", + name: "plugin", + + statusCode: http.StatusNotFound, + + expectedErr: itd.ErrItemNotFound, + expectedCalls: 1, + }, + { + testName: "should return generic error in case the server responds 500", + name: "plugin", + + statusCode: http.StatusInternalServerError, + + expectedErr: ErrServerDeleteItem, + expectedCalls: 1, + }, + { + testName: "should return unexpected response error in case of bad request response", + name: "plugin", + + statusCode: http.StatusBadRequest, + + expectedErr: ErrUnexpectedDeleteItem, + expectedCalls: 1, + }, + } + + for _, tt := range testCases { + t.Run(tt.testName, func(t *testing.T) { + callsCount := new(int) + *callsCount = 0 + testServer := deleteByItemNameMockServer( + t, + tt.statusCode, + tt.name, + callsCount, + ) + defer testServer.Close() + + mockClientConfig.Host = testServer.URL + client, err := client.APIClientForConfig(mockClientConfig) + require.NoError(t, err) + + err = deleteITD( + t.Context(), + client, + mockDeleteCompanyID, + tt.name, + ) + + require.Equal(t, tt.expectedCalls, *callsCount, "did not match number of calls") + + if tt.expectedErr != nil { + require.ErrorIs(t, err, tt.expectedErr) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/internal/cmd/item-type-definition/get.go b/internal/cmd/item-type-definition/get.go new file mode 100644 index 00000000..6fb4ce27 --- /dev/null +++ b/internal/cmd/item-type-definition/get.go @@ -0,0 +1,121 @@ +// Copyright Mia srl +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package itd + +import ( + "context" + "fmt" + + "github.com/mia-platform/miactl/internal/client" + "github.com/mia-platform/miactl/internal/clioptions" + itd "github.com/mia-platform/miactl/internal/resources/item-type-definition" + "github.com/mia-platform/miactl/internal/util" + "github.com/spf13/cobra" +) + +const ( + getItdEndpoint = "/api/tenants/%s/marketplace/item-type-definitions/%s/" + getCmdLong = `Get an Item Type Definition + + This command get an Item Type Definitions based on its name and tenant namespace. It works with Mia-Platform Console v14.1.0 or later. + + You need to specify the name and its tenant namespace, via the respective flags. The company-id flag can be omitted if it is already set in the context. + ` + getCmdUse = "get --tenantId tenantId --name name" +) + +func GetCmd(options *clioptions.CLIOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: getCmdUse, + Short: "Get item type definition", + Long: getCmdLong, + RunE: func(cmd *cobra.Command, _ []string) error { + restConfig, err := options.ToRESTConfig() + cobra.CheckErr(err) + client, err := client.APIClientForConfig(restConfig) + cobra.CheckErr(err) + + canUseNewAPI, versionError := util.VersionCheck(cmd.Context(), client, 14, 1) + if !canUseNewAPI || versionError != nil { + return itd.ErrUnsupportedCompanyVersion + } + + serializedItem, err := getItemEncodedWithFormat( + cmd.Context(), + client, + restConfig.CompanyID, + options.ItemTypeDefinitionName, + options.OutputFormat, + ) + cobra.CheckErr(err) + + fmt.Println(serializedItem) + return nil + }, + } + + nameFlagName := options.AddItemTypeDefinitionNameFlag(cmd.Flags()) + + err := cmd.MarkFlagRequired(nameFlagName) + if err != nil { + // the error is only due to a programming error (missing command flag), hence panic + panic(err) + } + + return cmd +} + +func getItemEncodedWithFormat(ctx context.Context, client *client.APIClient, companyID, name, outputFormat string) (string, error) { + if companyID == "" { + return "", itd.ErrMissingCompanyID + } + endpoint := fmt.Sprintf(getItdEndpoint, companyID, name) + item, err := performGetITDRequest(ctx, client, endpoint) + + if err != nil { + return "", err + } + + data, err := item.Marshal(outputFormat) + if err != nil { + return "", err + } + + return string(data), nil +} + +func performGetITDRequest(ctx context.Context, client *client.APIClient, endpoint string) (*itd.GenericItemTypeDefinition, error) { + resp, err := client.Get().APIPath(endpoint).Do(ctx) + + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + + if err := resp.Error(); err != nil { + return nil, err + } + + var itd *itd.GenericItemTypeDefinition + if err := resp.ParseResponse(&itd); err != nil { + return nil, fmt.Errorf("error parsing response body: %w", err) + } + + if itd == nil { + return nil, fmt.Errorf("no item type definition returned in the response") + } + + return itd, nil +} diff --git a/internal/cmd/item-type-definition/get_test.go b/internal/cmd/item-type-definition/get_test.go new file mode 100644 index 00000000..576aa1f6 --- /dev/null +++ b/internal/cmd/item-type-definition/get_test.go @@ -0,0 +1,248 @@ +// Copyright Mia srl +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package itd + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/mia-platform/miactl/internal/client" + "github.com/mia-platform/miactl/internal/clioptions" + "github.com/mia-platform/miactl/internal/encoding" + itd "github.com/mia-platform/miactl/internal/resources/item-type-definition" + "github.com/stretchr/testify/require" + "sigs.k8s.io/kustomize/kyaml/yaml" +) + +const ( + mockCompanyID = "some-company-id" + mockName = "plugin" + validBodyJSONString = `{ + "apiVersion": "software-catalog.mia-platform.eu/v1", + "kind": "item-type-definition", + "metadata": { + "namespace": { + "scope": "tenant", + "id": "some-company-id" + }, + "name": "plugin", + "visibility": { + "scope": "tenant", + "ids": [ + "some-company-id" + ] + }, + "displayName": "Plugin", + "tags": [ + "prova" + ], + "maintainers": [ + { + "name": "ok" + } + ], + "publisher": { + "name": "publisher-name" + } + }, + "spec": { + "type": "plugin", + "scope": "tenant", + "validation": { + "mechanism": "json-schema", + "schema": {} + } + }, + "__v": 2 + }` +) + +func TestGetResourceCmd(t *testing.T) { + t.Run("test command creation", func(t *testing.T) { + opts := clioptions.NewCLIOptions() + cmd := GetCmd(opts) + require.NotNil(t, cmd) + }) + + t.Run("should not run command when Console version is lower than 14.1.0", func(t *testing.T) { + server := httptest.NewServer(unexecutedCmdMockServer(t)) + defer server.Close() + + opts := clioptions.NewCLIOptions() + opts.CompanyID = mockCompanyID + opts.Endpoint = server.URL + + cmd := GetCmd(opts) + cmd.SetArgs([]string{"get", "--name", mockName}) + + err := cmd.Execute() + require.ErrorIs(t, err, itd.ErrUnsupportedCompanyVersion) + }) +} + +func getItemByTupleMockServer( + t *testing.T, + validResponse bool, + statusCode int, + calledCount *int, +) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + *calledCount++ + require.Equal(t, + fmt.Sprintf( + getItdEndpoint, mockCompanyID, mockName), + r.RequestURI, + ) + require.Equal(t, http.MethodGet, r.Method) + + w.WriteHeader(statusCode) + if statusCode == http.StatusNotFound || statusCode == http.StatusInternalServerError { + return + } + if validResponse { + w.Write([]byte(validBodyJSONString)) + return + } + w.Write([]byte("invalid response")) + })) +} + +func TestGetItemEncodedByTuple(t *testing.T) { + clientConfig := &client.Config{ + Transport: http.DefaultTransport, + } + + testCases := map[string]struct { + invalidResponse bool + statusCode int + + outputFormat string + companyID string + name string + + expectError bool + expectedCalledCount int + }{ + "valid get response - json": { + outputFormat: encoding.JSON, + statusCode: http.StatusOK, + expectedCalledCount: 1, + + companyID: mockCompanyID, + name: mockName, + }, + "valid get response - yaml": { + statusCode: http.StatusOK, + outputFormat: encoding.YAML, + expectedCalledCount: 1, + + companyID: mockCompanyID, + name: mockName, + }, + "invalid body response": { + statusCode: http.StatusOK, + expectError: true, + invalidResponse: true, + outputFormat: encoding.JSON, + expectedCalledCount: 1, + + companyID: mockCompanyID, + name: mockName, + }, + "resource not found": { + statusCode: http.StatusNotFound, + expectError: true, + outputFormat: encoding.JSON, + expectedCalledCount: 1, + + companyID: mockCompanyID, + name: mockName, + }, + "internal server error": { + statusCode: http.StatusInternalServerError, + outputFormat: encoding.JSON, + expectError: true, + expectedCalledCount: 1, + + companyID: mockCompanyID, + name: mockName, + }, + "should throw error and not call endpoint with missing company id": { + statusCode: http.StatusOK, + outputFormat: encoding.JSON, + + expectError: true, + expectedCalledCount: 0, + + companyID: "", + name: mockName, + }, + } + + for testName, testCase := range testCases { + t.Run(testName, func(t *testing.T) { + calledCount := new(int) + *calledCount = 0 + server := getItemByTupleMockServer( + t, + !testCase.invalidResponse, + testCase.statusCode, + calledCount, + ) + defer server.Close() + clientConfig.Host = server.URL + client, err := client.APIClientForConfig(clientConfig) + require.NoError(t, err) + found, err := getItemEncodedWithFormat( + t.Context(), + client, + testCase.companyID, + testCase.name, + testCase.outputFormat, + ) + + require.Equal( + t, + testCase.expectedCalledCount, + *calledCount, + "unexpected number of calls to endpoint", + ) + + if testCase.expectError { + require.Zero(t, found) + require.Error(t, err) + } else { + if testCase.outputFormat == encoding.JSON { + require.JSONEq(t, validBodyJSONString, found) + } else { + foundMap := map[string]interface{}{} + err := yaml.Unmarshal([]byte(found), &foundMap) + require.NoError(t, err) + + expectedMap := map[string]interface{}{} + err = yaml.Unmarshal([]byte(found), &expectedMap) + require.NoError(t, err) + + require.Equal(t, expectedMap, foundMap) + } + require.NoError(t, err) + } + }) + } +} diff --git a/internal/cmd/item-type-definition/list.go b/internal/cmd/item-type-definition/list.go new file mode 100644 index 00000000..40332d59 --- /dev/null +++ b/internal/cmd/item-type-definition/list.go @@ -0,0 +1,183 @@ +// Copyright Mia srl +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package itd + +import ( + "context" + "fmt" + "strconv" + + "github.com/mia-platform/miactl/internal/client" + "github.com/mia-platform/miactl/internal/clioptions" + "github.com/mia-platform/miactl/internal/printer" + itd "github.com/mia-platform/miactl/internal/resources/item-type-definition" + "github.com/mia-platform/miactl/internal/util" + "github.com/spf13/cobra" +) + +type GetItdsOptions struct { + CompanyID string + Public bool + Page int +} + +const ( + listItdEndpoint = "/api/marketplace/item-type-definitions/" + listCmdLong = `List Item Type Definitions + + This command lists the Item Type Definitions of a company. It works with Mia-Platform Console v14.1.0 or later. + + Results are paginated. By default, only the first page is shown. + + you can also specify the following flags: + - --public - if this flag is set, the command fetches not only the items from the requested company, but also the public Catalog items from other companies. + - --page - specify the page to fetch, default is 1 + ` + listCmdUse = "list --company-id company-id" +) + +func ListCmd(options *clioptions.CLIOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: listCmdUse, + Short: "List item type definitions", + Long: listCmdLong, + RunE: runListCmd(options), + } + + options.AddPublicFlag(cmd.Flags()) + options.AddPageFlag(cmd.Flags()) + + return cmd +} + +func runListCmd(options *clioptions.CLIOptions) func(cmd *cobra.Command, args []string) error { + return func(cmd *cobra.Command, _ []string) error { + restConfig, err := options.ToRESTConfig() + cobra.CheckErr(err) + apiClient, err := client.APIClientForConfig(restConfig) + cobra.CheckErr(err) + + canUseNewAPI, versionError := util.VersionCheck(cmd.Context(), apiClient, 14, 1) + if !canUseNewAPI || versionError != nil { + return itd.ErrUnsupportedCompanyVersion + } + + listItemsOptions := GetItdsOptions{ + CompanyID: restConfig.CompanyID, + Public: options.MarketplaceFetchPublicItems, + Page: options.Page, + } + + err = PrintItds(cmd.Context(), apiClient, listItemsOptions, options.Printer(), listItdEndpoint) + cobra.CheckErr(err) + + return nil + } +} + +func PrintItds(context context.Context, client *client.APIClient, options GetItdsOptions, p printer.IPrinter, endpoint string) error { + itds, err := fetchItds(context, client, options, endpoint) + if err != nil { + return err + } + + p.Keys("Name", "Display Name", "Visibility", "Publisher", "Versioning Supported") + for _, itd := range itds { + publisher := itd.Metadata.Publisher.Name + if publisher == "" { + publisher = "-" + } + + p.Record( + itd.Metadata.Name, + itd.Metadata.DisplayName, + itd.Metadata.Visibility.Scope, + publisher, + strconv.FormatBool(itd.Spec.IsVersioningSupported), + ) + } + p.Print() + return nil +} + +func fetchItds(ctx context.Context, client *client.APIClient, options GetItdsOptions, endpoint string) ([]*itd.ItemTypeDefinition, error) { + err := validateOptions(options) + if err != nil { + return nil, err + } + + request := buildRequest(client, options, endpoint) + resp, err := executeRequest(ctx, request) + if err != nil { + return nil, err + } + + listItems, err := parseResponse(resp) + if err != nil { + return nil, err + } + + return listItems, nil +} + +func validateOptions(options GetItdsOptions) error { + requestedSpecificCompany := len(options.CompanyID) > 0 + + if !requestedSpecificCompany { + return itd.ErrMissingCompanyID + } + + return nil +} + +func buildRequest(client *client.APIClient, options GetItdsOptions, endpoint string) *client.Request { + request := client.Get().APIPath(endpoint) + switch { + case options.Public: + request.SetParam("visibility", "console"+","+options.CompanyID) + case !options.Public: + request.SetParam("visibility", options.CompanyID) + } + + if options.Page <= 0 { + request.SetParam("page", "1") + } else { + request.SetParam("page", strconv.Itoa(options.Page)) + } + + return request +} + +func executeRequest(ctx context.Context, request *client.Request) (*client.Response, error) { + resp, err := request.Do(ctx) + if err != nil { + return nil, fmt.Errorf("error executing request: %w", err) + } + if err := resp.Error(); err != nil { + return nil, err + } + + return resp, nil +} + +func parseResponse(resp *client.Response) ([]*itd.ItemTypeDefinition, error) { + listItems := make([]*itd.ItemTypeDefinition, 0) + if err := resp.ParseResponse(&listItems); err != nil { + return nil, fmt.Errorf("error parsing response body: %w", err) + } + + return listItems, nil +} diff --git a/internal/cmd/item-type-definition/list_test.go b/internal/cmd/item-type-definition/list_test.go new file mode 100644 index 00000000..678d6d35 --- /dev/null +++ b/internal/cmd/item-type-definition/list_test.go @@ -0,0 +1,309 @@ +// Copyright Mia srl +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package itd + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/mia-platform/miactl/internal/client" + "github.com/mia-platform/miactl/internal/clioptions" + "github.com/mia-platform/miactl/internal/printer" + itd "github.com/mia-platform/miactl/internal/resources/item-type-definition" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetCmd(t *testing.T) { + t.Run("test command creation", func(t *testing.T) { + opts := clioptions.NewCLIOptions() + cmd := ListCmd(opts) + require.NotNil(t, cmd) + }) + + t.Run("should not run command when Console version is lower than 14.1.0", func(t *testing.T) { + server := httptest.NewServer(unexecutedCmdMockServer(t)) + defer server.Close() + + opts := clioptions.NewCLIOptions() + opts.CompanyID = "my-company" + opts.Endpoint = server.URL + + cmd := ListCmd(opts) + cmd.SetArgs([]string{"list"}) + + err := cmd.Execute() + require.ErrorIs(t, err, itd.ErrUnsupportedCompanyVersion) + }) +} + +func TestBuildMarketplaceItemsList(t *testing.T) { + type testCase struct { + name string + options GetItdsOptions + responseHandler http.HandlerFunc + clientConfig *client.Config + expectError bool + expectedContains []string + } + + testCases := []testCase{ + { + name: "private item type definitions", + options: GetItdsOptions{ + CompanyID: "my-company", + Public: false, + }, + responseHandler: privateItdsHandler(t), + clientConfig: &client.Config{Transport: http.DefaultTransport}, + expectError: false, + expectedContains: []string{ + "NAME", "DISPLAY NAME", "VISIBILITY", "PUBLISHER", "VERSIONING SUPPORTED", + "plugin", "Plugin", "tenant", "Test Publisher", "false", + }, + }, + { + name: "public and private item type definitions, page 2", + options: GetItdsOptions{ + CompanyID: "my-company", + Public: true, + Page: 2, + }, + responseHandler: publicAndPrivateItdsHandler(t), + clientConfig: &client.Config{Transport: http.DefaultTransport}, + expectError: false, + expectedContains: []string{ + "NAME", "DISPLAY NAME", "VISIBILITY", "PUBLISHER", "VERSIONING SUPPORTED", + "plugin", "Plugin", "tenant", "Test Publisher", "false", + "custom-resource", "Infrastructure resource", "console", "Mia-Platform", "true", + "itd-no-publisher", "ITD no publisher", "console", "-", "false", + }, + }, + } + + runTestCase := func(t *testing.T, tc testCase) { + t.Helper() + server := httptest.NewServer(tc.responseHandler) + defer server.Close() + + tc.clientConfig.Host = server.URL + client, err := client.APIClientForConfig(tc.clientConfig) + require.NoError(t, err) + + strBuilder := &strings.Builder{} + mockPrinter := printer.NewTablePrinter(printer.TablePrinterOptions{}, strBuilder) + err = PrintItds(t.Context(), client, tc.options, mockPrinter, listItdEndpoint) + found := strBuilder.String() + if tc.expectError { + assert.Error(t, err) + assert.Zero(t, found) + } else { + assert.NoError(t, err) + assert.NotZero(t, found) + for _, expected := range tc.expectedContains { + assert.Contains(t, found, expected) + } + } + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + runTestCase(t, tc) + }) + } +} + +func unexecutedCmdMockServer(t *testing.T) http.HandlerFunc { + t.Helper() + return func(w http.ResponseWriter, r *http.Request) { + if strings.EqualFold(r.URL.Path, "/api/version") && r.Method == http.MethodGet { + _, err := w.Write([]byte(`{"major": "14", "minor":"0"}`)) + require.NoError(t, err) + } else { + w.WriteHeader(http.StatusNotFound) + assert.Fail(t, fmt.Sprintf("unexpected request: %s", r.URL.Path)) + } + } +} + +func privateItdsHandler(t *testing.T) http.HandlerFunc { + t.Helper() + return func(w http.ResponseWriter, r *http.Request) { + if strings.EqualFold(r.URL.Path, listItdEndpoint) && + r.Method == http.MethodGet && + r.URL.Query().Get("page") == "1" && + r.URL.Query().Get("visibility") == "my-company" { + _, err := w.Write([]byte(privateItdsBodyContent(t))) + require.NoError(t, err) + } else { + w.WriteHeader(http.StatusNotFound) + assert.Fail(t, fmt.Sprintf("unexpected request: %s", r.URL.Path)) + } + } +} + +func publicAndPrivateItdsHandler(t *testing.T) http.HandlerFunc { + t.Helper() + return func(w http.ResponseWriter, r *http.Request) { + if strings.EqualFold(r.URL.Path, listItdEndpoint) && + r.Method == http.MethodGet && + r.URL.Query().Get("page") == "2" && + r.URL.Query().Get("visibility") == "console,my-company" { + _, err := w.Write([]byte(publicItdsBodyContent(t))) + require.NoError(t, err) + } else { + w.WriteHeader(http.StatusNotFound) + assert.Fail(t, fmt.Sprintf("unexpected request: %s", r.URL.Path)) + } + } +} + +func privateItdsBodyContent(t *testing.T) string { + t.Helper() + + return `[ + { + "apiVersion": "software-catalog.mia-platform.eu/v1", + "kind": "item-type-definition", + "metadata": { + "namespace": { + "scope": "tenant", + "id": "99350849-653a-418c-8a66-545b4b34b619" + }, + "name": "plugin", + "visibility": { + "scope": "tenant", + "ids": ["99350849-653a-418c-8a66-545b4b34b619"] + }, + "displayName": "Plugin", + "publisher": { + "name": "Test Publisher" + } + }, + "spec": { + "type": "plugin", + "scope": "tenant", + "validation": { + "mechanism": "json-schema", + "schema": {} + } + }, + "__v": 9 + } + ]` +} + +func publicItdsBodyContent(t *testing.T) string { + t.Helper() + + return `[ + { + "apiVersion": "software-catalog.mia-platform.eu/v1", + "kind": "item-type-definition", + "metadata": { + "namespace": { + "scope": "tenant", + "id": "99350849-653a-418c-8a66-545b4b34b619" + }, + "name": "plugin", + "visibility": { + "scope": "tenant", + "ids": ["99350849-653a-418c-8a66-545b4b34b619"] + }, + "displayName": "Plugin", + "publisher": { + "name": "Test Publisher" + } + }, + "spec": { + "type": "plugin", + "scope": "tenant", + "validation": { + "mechanism": "json-schema", + "schema": {} + } + }, + "__v": 9 + }, + { + "apiVersion": "software-catalog.mia-platform.eu/v1", + "kind": "item-type-definition", + "metadata": { + "namespace": { + "scope": "tenant", + "id": "mia-platform" + }, + "name": "custom-resource", + "visibility": { + "scope": "console" + }, + "displayName": "Infrastructure resource", + "description": "Defines custom objects beyond the standard Console-supported resources.", + "documentation": { + "type": "external", + "url": "https://docs.mia-platform.eu/docs/software-catalog/items-manifest/infrastructure-resource" + }, + "maintainers": [ + { + "name": "Mia-Platform", + "email": "support@mia-platform.eu" + } + ], + "publisher": { + "name": "Mia-Platform", + "url": "https://mia-platform.eu/" + } + }, + "spec": { + "type": "custom-resource", + "scope": "tenant", + "validation": { + "mechanism": "json-schema", + "schema": {} + }, + "isVersioningSupported": true + }, + "__v": 0 + }, + { + "apiVersion": "software-catalog.mia-platform.eu/v1", + "kind": "item-type-definition", + "metadata": { + "namespace": { + "scope": "tenant", + "id": "99350849-653a-418c-8a66-545b4b34b619" + }, + "name": "itd-no-publisher", + "visibility": { + "scope": "console" + }, + "displayName": "ITD no publisher" + }, + "spec": { + "type": "plugin", + "scope": "tenant", + "validation": { + "mechanism": "json-schema", + "schema": {} + } + }, + "__v": 1 + } + ]` +} diff --git a/internal/cmd/item-type-definition/put.go b/internal/cmd/item-type-definition/put.go new file mode 100644 index 00000000..3ee69748 --- /dev/null +++ b/internal/cmd/item-type-definition/put.go @@ -0,0 +1,183 @@ +// Copyright Mia srl +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package itd + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io/fs" + "path/filepath" + + "github.com/mia-platform/miactl/internal/client" + "github.com/mia-platform/miactl/internal/clioptions" + "github.com/mia-platform/miactl/internal/encoding" + "github.com/mia-platform/miactl/internal/files" + itd "github.com/mia-platform/miactl/internal/resources/item-type-definition" + "github.com/mia-platform/miactl/internal/util" + "github.com/spf13/cobra" +) + +var ( + ErrInvalidFilePath = errors.New("invalid file path") + ErrPathIsFolder = errors.New("path must be a file path, not a folder") + ErrFileFormatNotSupported = errors.New("file format not supported, supported formats are: .json, .yaml, .yml") + ErrResWithoutName = errors.New("name field is required") + ErrResNameNotAString = errors.New("name field must be string") + ErrPuttingResources = errors.New("cannot save the item type definition") +) + +const ( + putItdEndpoint = "/api/tenants/%s/marketplace/item-type-definitions/" + + cmdPutLongDescription = ` Create or update an Item Type Definition. It works with Mia-Platform Console v14.1.0 or later. + + You need to specify the flag --file or -f that accepts a file and companyId. + + Supported formats are JSON (.json files) and YAML (.yaml or .yml files). The company-id flag can be omitted if it is already set in the context.` + + putExample = ` + # Create the item type definition in file myFantasticGoTemplate.json located in the current directory + miactl catalog put --file myFantasticGoTemplate.json + + # Create the item type definition in file myFantasticGoTemplate.json, with relative path + miactl catalog put --file ./path/to/myFantasticGoTemplate.json` + + putCmdUse = "put { --file file-path }" +) + +func PutCmd(options *clioptions.CLIOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: putCmdUse, + Short: "Create or update an Item Type Definition", + Long: cmdPutLongDescription, + Example: putExample, + RunE: func(cmd *cobra.Command, _ []string) error { + restConfig, err := options.ToRESTConfig() + cobra.CheckErr(err) + client, err := client.APIClientForConfig(restConfig) + cobra.CheckErr(err) + + canUseNewAPI, versionError := util.VersionCheck(cmd.Context(), client, 14, 1) + if !canUseNewAPI || versionError != nil { + return itd.ErrUnsupportedCompanyVersion + } + + companyID := restConfig.CompanyID + if len(companyID) == 0 { + return itd.ErrMissingCompanyID + } + + outcome, err := putItemFromPath( + cmd.Context(), + client, + companyID, + options.ItemTypeDefinitionFilePath, + options.OutputFormat, + ) + cobra.CheckErr(err) + + fmt.Println(outcome) + + return nil + }, + } + + options.AddItemTypeDefinitionFileFlag(cmd) + + return cmd +} + +func putItemFromPath(ctx context.Context, client *client.APIClient, companyID string, filePath string, outputFormat string) (string, error) { + _, err := checkFilePath(filePath) + if err != nil { + return "", fmt.Errorf("%w: %s", err, err) + } + + outcome, err := putItemTypeDefinition(ctx, client, companyID, filePath) + if err != nil { + return "", fmt.Errorf("%w: %s", ErrPuttingResources, err) + } + + data, err := outcome.Marshal(outputFormat) + if err != nil { + return "", err + } + + return string(data), nil +} + +func checkFilePath(rootPath string) (string, error) { + err := filepath.Walk(rootPath, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return ErrPathIsFolder + } + extension := filepath.Ext(path) + if extension == encoding.YmlExtension || extension == encoding.YamlExtension || + extension == encoding.JSONExtension { + return nil + } + return ErrFileFormatNotSupported + }) + if err != nil { + return "", err + } + return rootPath, nil +} + +func putItemTypeDefinition( + ctx context.Context, + client *client.APIClient, + companyID string, + filePath string, +) (*itd.GenericItemTypeDefinition, error) { + if companyID == "" { + return nil, itd.ErrMissingCompanyID + } + + itemTypeDefinition := &itd.GenericItemTypeDefinition{} + if err := files.ReadFile(filePath, itemTypeDefinition); err != nil { + return nil, err + } + + bodyBytes, err := json.Marshal(itemTypeDefinition) + if err != nil { + return nil, err + } + + resp, err := client.Put(). + APIPath(fmt.Sprintf(putItdEndpoint, companyID)). + Body(bodyBytes). + Do(ctx) + if err != nil { + return nil, err + } + if err := resp.Error(); err != nil { + return nil, err + } + + putResponse := &itd.GenericItemTypeDefinition{} + err = resp.ParseResponse(putResponse) + if err != nil { + return nil, err + } + + return putResponse, nil +} diff --git a/internal/cmd/item-type-definition/put_test.go b/internal/cmd/item-type-definition/put_test.go new file mode 100644 index 00000000..1ec609a0 --- /dev/null +++ b/internal/cmd/item-type-definition/put_test.go @@ -0,0 +1,161 @@ +// Copyright Mia srl +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package itd + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/mia-platform/miactl/internal/client" + "github.com/mia-platform/miactl/internal/clioptions" + itd "github.com/mia-platform/miactl/internal/resources/item-type-definition" + "github.com/stretchr/testify/require" +) + +func TestPutCommand(t *testing.T) { + t.Run("test post run - shows deprecated command message", func(t *testing.T) { + server := httptest.NewServer(unexecutedCmdMockServer(t)) + defer server.Close() + + opts := clioptions.NewCLIOptions() + opts.CompanyID = "company-id" + opts.Endpoint = server.URL + + cmd := PutCmd(opts) + cmd.SetArgs([]string{"put", "--file", "testdata/validItem1.json"}) + + err := cmd.Execute() + require.ErrorIs(t, err, itd.ErrUnsupportedCompanyVersion) + }) +} + +var mockTenantID = "mock-tenant-id" +var mockURI = "/api/tenants/" + mockTenantID + "/marketplace/item-type-definitions/" +var mockFilePath = "./testdata/validItd.json" + +func TestApplyApplyResourceCmd(t *testing.T) { + // testdata/validItd.json with __v + 1 + mockResponseJSON := `{ + "apiVersion": "software-catalog.mia-platform.eu/v1", + "kind": "item-type-definition", + "metadata": { + "namespace": { + "scope": "tenant", + "id": "some-company-id" + }, + "name": "plugin", + "visibility": { + "scope": "tenant", + "ids": [ + "some-company-id" + ] + }, + "displayName": "Plugin", + "tags": [ + "prova" + ], + "maintainers": [ + { + "name": "ok" + } + ], + "publisher": { + "name": "publisher-name" + } + }, + "spec": { + "type": "plugin", + "scope": "tenant", + "validation": { + "mechanism": "json-schema", + "schema": {} + } + }, + "__v": 3 + }` + + var mockResponse itd.GenericItemTypeDefinition + err := json.Unmarshal([]byte(mockResponseJSON), &mockResponse) + require.NoError(t, err) + + t.Run("should return response when is 200 OK", func(t *testing.T) { + server := putMockServer(t, http.StatusOK, mockResponse) + defer server.Close() + clientConfig := &client.Config{ + Transport: http.DefaultTransport, + } + clientConfig.Host = server.URL + client, err := client.APIClientForConfig(clientConfig) + require.NoError(t, err) + + found, err := putItemTypeDefinition( + t.Context(), + client, + mockTenantID, + mockFilePath, + ) + + require.NoError(t, err) + require.Equal(t, &mockResponse, found) + }) + + t.Run("should return err if response is a http error", func(t *testing.T) { + mockErrorResponse := map[string]interface{}{ + "message": "You are not allowed to perform the request!", + "statusCode": http.StatusForbidden, + "error": "Forbidden", + } + server := putMockServer(t, http.StatusForbidden, mockErrorResponse) + defer server.Close() + clientConfig := &client.Config{ + Transport: http.DefaultTransport, + } + clientConfig.Host = server.URL + client, err := client.APIClientForConfig(clientConfig) + require.NoError(t, err) + + found, err := putItemTypeDefinition( + t.Context(), + client, + mockTenantID, + mockFilePath, + ) + + require.EqualError(t, err, "You are not allowed to perform the request!") + require.Nil(t, found) + }) +} + +func putMockServer(t *testing.T, statusCode int, mockResponse interface{}) *httptest.Server { + t.Helper() + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + putRequestHandler(t, w, r, statusCode, mockResponse) + })) +} + +func putRequestHandler(t *testing.T, w http.ResponseWriter, r *http.Request, statusCode int, mockResponse interface{}) { + t.Helper() + + require.Equal(t, mockURI, r.RequestURI) + require.Equal(t, http.MethodPut, r.Method) + + w.WriteHeader(statusCode) + resBytes, err := json.Marshal(mockResponse) + require.NoError(t, err) + w.Write(resBytes) +} diff --git a/internal/cmd/item-type-definition/testdata/validItd.json b/internal/cmd/item-type-definition/testdata/validItd.json new file mode 100644 index 00000000..17cb9ce3 --- /dev/null +++ b/internal/cmd/item-type-definition/testdata/validItd.json @@ -0,0 +1,38 @@ +{ + "apiVersion": "software-catalog.mia-platform.eu/v1", + "kind": "item-type-definition", + "metadata": { + "namespace": { + "scope": "tenant", + "id": "some-company-id" + }, + "name": "plugin", + "visibility": { + "scope": "tenant", + "ids": [ + "some-company-id" + ] + }, + "displayName": "Plugin", + "tags": [ + "prova" + ], + "maintainers": [ + { + "name": "ok" + } + ], + "publisher": { + "name": "publisher-name" + } + }, + "spec": { + "type": "plugin", + "scope": "tenant", + "validation": { + "mechanism": "json-schema", + "schema": {} + } + }, + "__v": 2 + } \ No newline at end of file diff --git a/internal/cmd/item_type_definition.go b/internal/cmd/item_type_definition.go new file mode 100644 index 00000000..72b8771c --- /dev/null +++ b/internal/cmd/item_type_definition.go @@ -0,0 +1,43 @@ +// Copyright Mia srl +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/mia-platform/miactl/internal/clioptions" + itd "github.com/mia-platform/miactl/internal/cmd/item-type-definition" + "github.com/spf13/cobra" +) + +func ItemTypeDefinitionCmd(options *clioptions.CLIOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "itd", + Short: "View and manage Item Type Definitions. This command is available from Mia-Platform Console v14.1.0.", + } + + // add cmd flags + flags := cmd.PersistentFlags() + options.AddConnectionFlags(flags) + options.AddCompanyFlags(flags) + options.AddContextFlags(flags) + + // add sub commands + cmd.AddCommand(itd.ListCmd(options)) + cmd.AddCommand(itd.GetCmd(options)) + cmd.AddCommand(itd.DeleteCmd(options)) + cmd.AddCommand(itd.PutCmd(options)) + + return cmd +} diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 01022616..c9496197 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -52,6 +52,7 @@ func NewRootCommand() *cobra.Command { ContextCmd(options), ProjectCmd(options), MarketplaceCmd(options), + ItemTypeDefinitionCmd(options), CatalogCmd(options), RuntimeCmd(options), VersionCmd(options), diff --git a/internal/resources/item-type-definition/item-type-definition.go b/internal/resources/item-type-definition/item-type-definition.go new file mode 100644 index 00000000..62b89970 --- /dev/null +++ b/internal/resources/item-type-definition/item-type-definition.go @@ -0,0 +1,80 @@ +// Copyright Mia srl +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package itd + +import ( + "errors" + + "github.com/mia-platform/miactl/internal/encoding" +) + +var ( + ErrUnsupportedCompanyVersion = errors.New("you need Mia-Platform Console v14.1.0 or later to use this command") + ErrMissingCompanyID = errors.New("missing company id, please set one with the flag company-id or in the context") + ErrItemNotFound = errors.New("item type definition not found") +) + +// Item is a Marketplace item +// we use a map[string]interface{} to represent the item +// this allows to avoid changes in the code in case of a change in the resource structure +type GenericItemTypeDefinition map[string]interface{} + +type ItemTypeDefinitionMetadataVisibility struct { + Scope string `json:"scope"` + IDs []string `json:"ids"` //nolint: tagliatelle +} + +type ItemTypeDefinitionMetadataNamespace struct { + Scope string `json:"scope"` + ID string `json:"id"` +} + +type ItemTypeDefinitionMetadataPublisher struct { + Name string `json:"name"` +} + +type ItemTypeDefinitionMetadata struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + Namespace ItemTypeDefinitionMetadataNamespace `json:"namespace"` + Visibility ItemTypeDefinitionMetadataVisibility `json:"visibility"` + Publisher ItemTypeDefinitionMetadataPublisher `json:"publisher"` +} + +type ItemTypeDefinitionSpec struct { + IsVersioningSupported bool `json:"isVersioningSupported"` +} + +type ItemTypeDefinition struct { + Metadata ItemTypeDefinitionMetadata `json:"metadata"` + Spec ItemTypeDefinitionSpec `json:"spec"` +} + +func (i *GenericItemTypeDefinition) Marshal(encodingFormat string) ([]byte, error) { + return encoding.MarshalData(i, encodingFormat, encoding.MarshalOptions{Indent: true}) +} + +func (i *GenericItemTypeDefinition) Del(key string) { + delete(*i, key) +} + +func (i *GenericItemTypeDefinition) Set(key string, val interface{}) { + (*i)[key] = val +} + +func (i *GenericItemTypeDefinition) Get(key string) interface{} { + return (*i)[key] +}