diff --git a/docs/ccli_manual.md b/docs/ccli_manual.md index 13ca767..925a6b3 100644 --- a/docs/ccli_manual.md +++ b/docs/ccli_manual.md @@ -33,6 +33,10 @@ required yml file. For example: ``` $ ccli update openssl-1.1.1n.v4.yml ``` +- **set** - sets the fields of a part record including empty values. +``` +$ ccli set openssl-1.1.1n.v4.yml +``` - **upload** - uploads the specified source archive. A a new part record will be created if it does not correspond part record exists otherwise it will be associated with an existing part if it already exists. ``` diff --git a/main.go b/main.go index 15d2030..5b5ae23 100644 --- a/main.go +++ b/main.go @@ -37,14 +37,12 @@ func init() { viper.AddConfigPath(".") // read the config file if err := viper.ReadInConfig(); err != nil { - if err != nil { - if err != errors.New("open ccli_config.yml: no such file or directory") { - fmt.Println("User configuration file not found. Please create ccli_config.yml and copy the contents of ccli_config.DEFAULT.yml.") - } else { - fmt.Println("Error reading in config file. Error:", err) - } - os.Exit(1) + if err != errors.New("open ccli_config.yml: no such file or directory") { + fmt.Println("User configuration file not found. Please create ccli_config.yml and copy the contents of ccli_config.DEFAULT.yml.") + } else { + fmt.Println("Error reading in config file. Error:", err) } + os.Exit(1) } // unmarshal the config file parameters to a struct if err := viper.Unmarshal(&configFile); err != nil { @@ -110,6 +108,7 @@ func main() { rootCmd.AddCommand(cmd.Ping(&configFile)) rootCmd.AddCommand(cmd.Upload(&configFile)) rootCmd.AddCommand(cmd.Update(&configFile, client, indent)) + rootCmd.AddCommand(cmd.Set(&configFile, client, indent)) rootCmd.AddCommand(cmd.Query(&configFile, client, indent)) rootCmd.AddCommand(cmd.Find(&configFile, client, indent)) rootCmd.AddCommand(cmd.Export(&configFile, client, indent)) diff --git a/main_test.go b/main_test.go index 24fe9b2..66f53a9 100644 --- a/main_test.go +++ b/main_test.go @@ -141,6 +141,24 @@ func TestUpdate(tester *testing.T) { } } +// TestSet sets a part's information based on the yml file present in the given path using the +// command line and checks if the command line output is as expected +func TestSet(tester *testing.T) { + // ccli set testdir/yml/openid-client-4.9.1.yml + cmd := exec.Command("ccli", "set", "testdir/yml/openid-client-4.9.1.yml") + // capturing command line output + output, err := cmd.Output() + if err != nil { + tester.Error("failed to capture command line output", err) + } + // splitting and extracting the output message to be checked + result := strings.Split(string(output), "\n")[0] + expected := "Part fields successfully set" + if result != expected { + tester.Errorf("Expected %s but got %s", expected, result) + } +} + // TestAddLicenseProfile adds a part's licensing profile based on the yml file present in the given path using the // command line and checks if the command line output is as expected func TestAddLicenseProfile(tester *testing.T) { diff --git a/packages/cmd/example.go b/packages/cmd/example.go index b79e1fa..6d03f87 100644 --- a/packages/cmd/example.go +++ b/packages/cmd/example.go @@ -34,6 +34,7 @@ func Example() *cobra.Command { $ ccli export part id sdl3ga-naTs42g5-rbow2A -o file.yml $ ccli export template security -o file.yml $ ccli update openssl-1.1.1n.v4.yml + $ ccli set openssl-1.1.1n.v4.yml $ ccli upload openssl-1.1.1n.tar.gz $ ccli find part busybox $ ccli find sha256 2493347f59c03... diff --git a/packages/cmd/set.go b/packages/cmd/set.go new file mode 100644 index 0000000..b747d96 --- /dev/null +++ b/packages/cmd/set.go @@ -0,0 +1,90 @@ +// Copyright (c) 2020 Wind River Systems, Inc. +// +// 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. +package cmd + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "os" + "wrs/catalog/ccli/packages/config" + "wrs/catalog/ccli/packages/graphql" + "wrs/catalog/ccli/packages/yaml" + + graph "github.com/hasura/go-graphql-client" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +// Set() is a sub command responsible for setting part information including zero values +// based on a given yml file. +func Set(configFile *config.ConfigData, client *graph.Client, indent string) *cobra.Command { + // cobra command for set part + setCmd := &cobra.Command{ + Use: "set [path]", + Short: "Set the fields of a part in the Software Parts Catalog including empty values", + // function to be run as setup for the command execution + PreRunE: func(cmd *cobra.Command, args []string) error { + // check if exactly 1 argument is present + if len(args) < 1 { + return errors.New("No path provided.") + } + return nil + }, + // function to be run during command execution + RunE: func(cmd *cobra.Command, args []string) error { + argImportPath := args[0] + if argImportPath == "" { + return errors.New("error setting part fields, set subcommand usage: ./ccli set ") + } + // check if the file is of yaml/yml format + if argImportPath != "" { + if argImportPath[len(argImportPath)-5:] != ".yaml" && argImportPath[len(argImportPath)-4:] != ".yml" { + return errors.New("error importing part, import path not a yaml file") + } + // open the file + f, err := os.Open(argImportPath) + if err != nil { + return errors.Wrapf(err, "error opening file") + } + defer f.Close() + // read all the data from the file + data, err := io.ReadAll(f) + if err != nil { + return errors.Wrapf(err, "error reading file") + } + // unmarshal the data of the file into a struct + var partData yaml.Part + if err = yaml.Unmarshal(data, &partData); err != nil { + return errors.Wrapf(err, "error decoding file contents") + } + slog.Debug("setting part fields") + // set the part fields with the given part data + returnPart, err := graphql.SetPart(context.Background(), client, &partData) + if err != nil { + return errors.Wrapf(err, "error setting part fields") + } + // marshal the struct into a json + prettyJson, err := json.MarshalIndent(&returnPart, "", indent) + if err != nil { + return errors.Wrapf(err, "error prettifying json") + } + fmt.Printf("Part fields successfully set\n%s\n", string(prettyJson)) + } + return nil + }, + } + return setCmd + +} diff --git a/packages/graphql/controller.go b/packages/graphql/controller.go index 483c87b..51195fb 100644 --- a/packages/graphql/controller.go +++ b/packages/graphql/controller.go @@ -394,6 +394,72 @@ func UpdatePart(ctx context.Context, client *graphql.Client, partData *yaml.Part return &mutation.Part, nil } +// Sets the fields of a part record including emptry values from the catalog using yaml template +func SetPart(ctx context.Context, client *graphql.Client, partData *yaml.Part) (*Part, error) { + + var partInput PartInput + if partData.CatalogID == "" { + if partData.FVC == "" && partData.Sha256 == "" { + return nil, errors.New("error updating part, no part identifier provided") + } + if partData.FVC != "" { + catalogID, err := GetPartIDByFVC(ctx, client, partData.FVC) + if err != nil { + return nil, err + } + if catalogID != nil { + partInputID := UUID(catalogID.String()) + partInput.ID = &partInputID + } + } else if partData.Sha256 != "" { + catalogID, err := GetPartIDBySha256(ctx, client, partData.Sha256) + if err != nil { + return nil, err + } + if catalogID != nil { + partInputID := UUID(catalogID.String()) + partInput.ID = &partInputID + } + } + } + + if partData.ComprisedOf != "" { + comprisedID := UUID(partData.ComprisedOf) + partInput.Comprised = &comprisedID + } + + var mutation struct { + Part `graphql:"setPart(partInput: $partInput)"` + } + + variables := map[string]interface{}{ + "partInput": partInput, + } + + if err := client.Mutate(ctx, &mutation, variables); err != nil { + return nil, err + } + + if partData.Aliases != nil { + var aliasMutation struct { + UUID `graphql:"createAlias(id: $id, alias: $alias)"` + } + + for _, v := range partData.Aliases { + aliasVariables := map[string]interface{}{ + "id": *partInput.ID, + "alias": v, + } + + if err := client.Mutate(ctx, &aliasMutation, aliasVariables); err != nil { + return nil, err + } + } + } + + return &mutation.Part, nil +} + // Used to convert a part data structure into the structure expected by yaml i/o func UnmarshalPart(part *Part, yamlPart *yaml.Part) error { yamlPart.Format = 1.0