From 5136a9cff8b1ff0c284ab35ad99920caa2b373f8 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Thu, 11 Jan 2024 20:37:30 -0800 Subject: [PATCH 1/8] feat(update): add update attributes handler and Cobra CLI cmd with improved flag rule value validation to prevent grpc response error, and also add notes in TODOs and comments about a missing attribute ID source for the Update call and questions about implementing attribute group CRUD and how we can possibly clean up Update so there are not a dozen flags with some containing lists as values --- cmd/attributes.go | 132 +++++++++++++++++++++++++++++++- pkg/handlers/createAttribute.go | 59 ++++++++++++++ 2 files changed, 187 insertions(+), 4 deletions(-) diff --git a/cmd/attributes.go b/cmd/attributes.go index a556f08d..8fb1d079 100644 --- a/cmd/attributes.go +++ b/cmd/attributes.go @@ -24,7 +24,11 @@ This application is a tool to generate the needed files to quickly create a Cobra application.`, } -var attrValues []string +var ( + attrValues []string + groupBy []string + resourceDependencies []string +) // List attributes var attributesListCmd = &cobra.Command{ @@ -98,8 +102,8 @@ var attributesCreateCmd = &cobra.Command{ } rule := cmd.Flag("rule").Value.String() - if rule == "" { - fmt.Println("Rule is required") + if rule == "" || handlers.GetAttributeRuleFromReadableString(rule) == 0 { + fmt.Printf("Flag 'rule' is required and must be one of: %v", handlers.GetAttributeRuleOptions()) return } @@ -129,7 +133,111 @@ var attributesCreateCmd = &cobra.Command{ }, } -// TODO: Update an attribute +// TODO: think about how to improve this. Passing a 12 flags/args in a CLI is very challenging... +// Update one attribute +var attributeUpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update an attribute", + Run: func(cmd *cobra.Command, args []string) { + if err := grpc.Connect(cmd.Flag("host").Value.String()); err != nil { + fmt.Println(err) + return + } + defer grpc.Conn.Close() + + Id, e := cmd.Flags().GetInt32("id") + if e != nil { + fmt.Println("Flag 'id' is required") + return + } + + name := cmd.Flag("name").Value.String() + if name == "" { + fmt.Println("Flag 'name' is required") + return + } + + rule := cmd.Flag("rule").Value.String() + if rule == "" || handlers.GetAttributeRuleFromReadableString(rule) == 0 { + fmt.Printf("Flag 'rule' is required and must be one of: %v", handlers.GetAttributeRuleOptions()) + return + } + + // Would this memory leak since the var is in scope to create & update both? + // TODO: check the same for groupBy, dependencies + if len(attrValues) == 0 { + fmt.Println("Flag 'values' is required") + return + } + + if len(groupBy) == 0 { + fmt.Println("Flag 'group-by' is required") + return + } + + if len(resourceDependencies) == 0 { + fmt.Println("Flag 'resource-dependencies' is required") + return + } + + // TODO: are all of these required, or can some be defaulted / looked up? + resourceId, e := cmd.Flags().GetInt32("resource-id") + if e != nil { + fmt.Println("Flag 'resource-id' is required") + return + } + + resourceVersion, e := cmd.Flags().GetInt32("res-version") + if e != nil { + fmt.Println("Flag 'resource-version' is required") + return + } + + resourceName := cmd.Flag("resource-name").Value.String() + if resourceName == "" { + fmt.Println("Flag 'resource-name' is required") + return + } + + resourceNamespace := cmd.Flag("resource-namespace").Value.String() + if resourceNamespace == "" { + fmt.Println("Flag 'resource-namespace' is required") + return + } + + resourceFqn := cmd.Flag("resource-fqn").Value.String() + if resourceFqn == "" { + fmt.Println("Flag 'resource-fqn' is required") + return + } + + resourceDescription := cmd.Flag("resource-description").Value.String() + if resourceDescription == "" { + fmt.Println("Flag 'resource-description' is required") + return + } + + if resp, err := handlers.UpdateAttribute( + Id, + name, + rule, + attrValues, + groupBy, + resourceId, + resourceVersion, + resourceName, + resourceNamespace, + resourceFqn, + resourceDescription, + resourceDependencies, + ); err != nil { + fmt.Println(err) + return + } else { + fmt.Println(resp) + } + }, +} // TODO: Delete an attribute @@ -144,4 +252,20 @@ func init() { attributesCreateCmd.Flags().StringSliceVarP(&attrValues, "values", "v", []string{}, "Values of the attribute") attributesCreateCmd.Flags().StringP("namespace", "s", "", "Namespace of the attribute") attributesCreateCmd.Flags().StringP("description", "d", "", "Description of the attribute") + + attributesCmd.AddCommand(attributeUpdateCmd) + // NOTE: I can't find the ID of created/listed attributes anywhere in grpc responses? Where is this located? + attributeUpdateCmd.Flags().Int32P("id", "i", 0, "Id of the attribute") + attributeUpdateCmd.Flags().StringP("name", "n", "", "Name of the attribute") + attributeUpdateCmd.Flags().StringP("rule", "r", "", "Rule of the attribute") + attributeUpdateCmd.Flags().StringSliceVarP(&attrValues, "values", "v", []string{}, "Values of the attribute") + attributeUpdateCmd.Flags().StringSliceVarP(&groupBy, "group-by", "g", []string{}, "GroupBy of the attribute") + // TODO: again, can any of these be defaulted/inferred via lookup? + attributeUpdateCmd.Flags().StringSliceVarP(&resourceDependencies, "resource-dependencies", "d", []string{}, "ResourceDependencies of the attribute definition descriptor") + attributeUpdateCmd.Flags().Int32P("resource-id", "I", 0, "ResourceId of the attribute definition descriptor") + attributeUpdateCmd.Flags().Int32P("resource-version", "V", 0, "ResourceVersion of the attribute definition descriptor") + attributeUpdateCmd.Flags().StringP("resource-name", "N", "", "ResourceName of the attribute definition descriptor") + attributeUpdateCmd.Flags().StringP("resource-namespace", "S", "", "ResourceNamespace of the attribute definition descriptor") + attributeUpdateCmd.Flags().StringP("resource-fqn", "F", "", "ResourceFqn of the attribute") + attributeUpdateCmd.Flags().StringP("resource-description", "D", "", "ResourceDescription of the attribute definition descriptor") } diff --git a/pkg/handlers/createAttribute.go b/pkg/handlers/createAttribute.go index e8883f5c..228643a1 100644 --- a/pkg/handlers/createAttribute.go +++ b/pkg/handlers/createAttribute.go @@ -44,6 +44,65 @@ func CreateAttribute(name string, rule string, values []string, namespace string }) } +func UpdateAttribute( + Id int32, + name string, + rule string, + values []string, + groupBy []string, + resourceId int32, + resourceVersion int32, + resourceName string, + resourceNamespace string, + resourceFqn string, + resourceDescription string, + resourceDependencies []string, +) (*attributesv1.UpdateAttributeResponse, error) { + var attrValues []*attributesv1.AttributeDefinitionValue + for _, v := range values { + if v != "" { + attrValues = append(attrValues, &attributesv1.AttributeDefinitionValue{Value: v}) + } + } + + var attrGroupBy []*attributesv1.AttributeDefinitionValue + for _, v := range groupBy { + if v != "" { + attrGroupBy = append(attrGroupBy, &attributesv1.AttributeDefinitionValue{Value: v}) + } + } + + var dependencies []*commonv1.ResourceDependency + for _, v := range resourceDependencies { + if v != "" { + dependencies = append(dependencies, &commonv1.ResourceDependency{Namespace: v}) + } + } + + client := attributesv1.NewAttributesServiceClient(grpc.Conn) + return client.UpdateAttribute(grpc.Context, &attributesv1.UpdateAttributeRequest{ + Id: Id, + Definition: &attributesv1.AttributeDefinition{ + Name: name, + Rule: GetAttributeRuleFromReadableString(rule), + Values: attrValues, + GroupBy: attrGroupBy, + Descriptor_: &commonv1.ResourceDescriptor{ + Type: commonv1.PolicyResourceType_POLICY_RESOURCE_TYPE_ATTRIBUTE_DEFINITION, + Id: resourceId, + Version: resourceVersion, + Name: resourceName, + Namespace: resourceNamespace, + Fqn: resourceFqn, + Description: resourceDescription, + Dependencies: dependencies, + }, + }, + }) +} + +// TODO: do we implement all methods for attribute groups as well, or attributes alone? + func GetAttributeRuleOptions() []string { return []string{ AttributeRuleAllOf, From c654b0b6715fea387c447e286f0fa915690af440 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Tue, 16 Jan 2024 12:41:10 -0800 Subject: [PATCH 2/8] rename createattributes.go to attributes.go --- pkg/handlers/{createAttribute.go => attributes.go} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename pkg/handlers/{createAttribute.go => attributes.go} (98%) diff --git a/pkg/handlers/createAttribute.go b/pkg/handlers/attributes.go similarity index 98% rename from pkg/handlers/createAttribute.go rename to pkg/handlers/attributes.go index 228643a1..599d4f0b 100644 --- a/pkg/handlers/createAttribute.go +++ b/pkg/handlers/attributes.go @@ -101,7 +101,7 @@ func UpdateAttribute( }) } -// TODO: do we implement all methods for attribute groups as well, or attributes alone? +// TODO: attribute groups CRUD func GetAttributeRuleOptions() []string { return []string{ From 49b8907066ebb8ff406ff61d06a25773c45a64b2 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Tue, 16 Jan 2024 12:41:34 -0800 Subject: [PATCH 3/8] anticipate attribute Id back and render in table when Listing --- cmd/attributes.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/attributes.go b/cmd/attributes.go index 8fb1d079..3cbba13b 100644 --- a/cmd/attributes.go +++ b/cmd/attributes.go @@ -5,6 +5,7 @@ package cmd import ( "fmt" + "strconv" "github.com/charmbracelet/bubbles/table" "github.com/opentdf/tructl/pkg/grpc" @@ -48,6 +49,7 @@ var attributesListCmd = &cobra.Command{ } columns := []table.Column{ + {Title: "Id", Width: 10}, {Title: "Namespace", Width: 20}, {Title: "Name", Width: 20}, {Title: "Rule", Width: 20}, @@ -65,6 +67,7 @@ var attributesListCmd = &cobra.Command{ } rows = append(rows, table.Row{ + strconv.Itoa(int(attr.Descriptor_.Id)), attr.Descriptor_.Namespace, attr.Name, handlers.GetAttributeRuleFromAttributeType(attr.Rule), From 007df32b4457a1b93b6bc543d397df83118527b7 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Tue, 16 Jan 2024 13:18:38 -0800 Subject: [PATCH 4/8] remove extraneous comments --- cmd/attributes.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/cmd/attributes.go b/cmd/attributes.go index 8e5a0706..b73c832c 100644 --- a/cmd/attributes.go +++ b/cmd/attributes.go @@ -292,13 +292,11 @@ func init() { attributesCreateCmd.Flags().StringP("description", "d", "", "Description of the attribute") attributesCmd.AddCommand(attributeUpdateCmd) - // NOTE: I can't find the ID of created/listed attributes anywhere in grpc responses? Where is this located? attributeUpdateCmd.Flags().Int32P("id", "i", 0, "Id of the attribute") attributeUpdateCmd.Flags().StringP("name", "n", "", "Name of the attribute") attributeUpdateCmd.Flags().StringP("rule", "r", "", "Rule of the attribute") attributeUpdateCmd.Flags().StringSliceVarP(&attrValues, "values", "v", []string{}, "Values of the attribute") attributeUpdateCmd.Flags().StringSliceVarP(&groupBy, "group-by", "g", []string{}, "GroupBy of the attribute") - // TODO: again, can any of these be defaulted/inferred via lookup? attributeUpdateCmd.Flags().StringSliceVarP(&resourceDependencies, "resource-dependencies", "d", []string{}, "ResourceDependencies of the attribute definition descriptor") attributeUpdateCmd.Flags().Int32P("resource-id", "I", 0, "ResourceId of the attribute definition descriptor") attributeUpdateCmd.Flags().Int32P("resource-version", "V", 0, "ResourceVersion of the attribute definition descriptor") From 754740c5a22fd9dc6b84a89c5d0ccdf3d610f3d0 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Tue, 16 Jan 2024 15:25:52 -0800 Subject: [PATCH 5/8] Use new cli package functions. Print table on new line to move terminal cursor to new line after rendering. Make string slice flag function more reusable by changing naming from 'required' to just get the slice with a possible 0 minimum in options argument passed. Render Id in List and Get tables rendered on success. --- cmd/attributes.go | 112 ++++++++++++---------------------------------- 1 file changed, 29 insertions(+), 83 deletions(-) diff --git a/cmd/attributes.go b/cmd/attributes.go index b73c832c..fdf128be 100644 --- a/cmd/attributes.go +++ b/cmd/attributes.go @@ -9,7 +9,6 @@ import ( "strconv" "github.com/opentdf/tructl/pkg/cli" - "github.com/opentdf/tructl/pkg/grpc" "github.com/opentdf/tructl/pkg/handlers" "github.com/spf13/cobra" ) @@ -56,6 +55,7 @@ var attributeGetCmd = &cobra.Command{ fmt.Println( cli.NewTabular(). Rows([][]string{ + {"Id", strconv.Itoa(int(attr.Id))}, {"Name", attr.Name}, {"Rule", attr.Rule}, {"Values", cli.CommaSeparated(attr.Values)}, @@ -83,14 +83,14 @@ var attributesListCmd = &cobra.Command{ t.Headers("Id", "Namespace", "Name", "Rule", "Values") for _, attr := range attrs { t.Row( - attr.Id, + strconv.Itoa(int(attr.Id)), attr.Namespace, attr.Name, attr.Rule, cli.CommaSeparated(attr.Values), ) } - fmt.Print(t.Render()) + fmt.Println(t.Render()) }, } @@ -105,7 +105,7 @@ var attributesCreateCmd = &cobra.Command{ flagHelper := cli.NewFlagHelper(cmd) name := flagHelper.GetRequiredString("name") rule := flagHelper.GetRequiredString("rule") - values := flagHelper.GetRequiredStringSlice("values", attrValues, cli.FlagHelperStringSliceOptions{ + values := flagHelper.GetStringSlice("values", attrValues, cli.FlagHelperStringSliceOptions{ Min: 1, }) namespace := flagHelper.GetRequiredString("namespace") @@ -171,108 +171,55 @@ var attributesDeleteCmd = &cobra.Command{ }, } -// TODO: think about how to improve this. Passing a 12 flags/args in a CLI is very challenging... // Update one attribute var attributeUpdateCmd = &cobra.Command{ Use: "update", Short: "Update an attribute", Run: func(cmd *cobra.Command, args []string) { - if err := grpc.Connect(cmd.Flag("host").Value.String()); err != nil { - fmt.Println(err) - return - } - defer grpc.Conn.Close() - - Id, e := cmd.Flags().GetInt32("id") - if e != nil { - fmt.Println("Flag 'id' is required") - return - } - - name := cmd.Flag("name").Value.String() - if name == "" { - fmt.Println("Flag 'name' is required") - return - } - - rule := cmd.Flag("rule").Value.String() - if rule == "" || handlers.GetAttributeRuleFromReadableString(rule) == 0 { - fmt.Printf("Flag 'rule' is required and must be one of: %v", handlers.GetAttributeRuleOptions()) - return - } - - // Would this memory leak since the var is in scope to create & update both? - // TODO: check the same for groupBy, dependencies - if len(attrValues) == 0 { - fmt.Println("Flag 'values' is required") - return - } - - if len(groupBy) == 0 { - fmt.Println("Flag 'group-by' is required") - return - } - - if len(resourceDependencies) == 0 { - fmt.Println("Flag 'resource-dependencies' is required") - return - } + close := cli.GrpcConnect(cmd) + defer close() - // TODO: are all of these required, or can some be defaulted / looked up? - resourceId, e := cmd.Flags().GetInt32("resource-id") - if e != nil { - fmt.Println("Flag 'resource-id' is required") - return - } + flagHelper := cli.NewFlagHelper(cmd) - resourceVersion, e := cmd.Flags().GetInt32("res-version") - if e != nil { - fmt.Println("Flag 'resource-version' is required") - return - } + id := flagHelper.GetRequiredInt32("id") + name := flagHelper.GetRequiredString("name") + rule := flagHelper.GetRequiredString("rule") - resourceName := cmd.Flag("resource-name").Value.String() - if resourceName == "" { - fmt.Println("Flag 'resource-name' is required") - return - } + values := flagHelper.GetStringSlice("values", attrValues, cli.FlagHelperStringSliceOptions{ + Min: 1, + }) - resourceNamespace := cmd.Flag("resource-namespace").Value.String() - if resourceNamespace == "" { - fmt.Println("Flag 'resource-namespace' is required") - return - } + groupBy := flagHelper.GetStringSlice("group-by", groupBy, cli.FlagHelperStringSliceOptions{ + Min: 0, + }) - resourceFqn := cmd.Flag("resource-fqn").Value.String() - if resourceFqn == "" { - fmt.Println("Flag 'resource-fqn' is required") - return - } + resourceDependencies := flagHelper.GetStringSlice("resource-dependencies", resourceDependencies, cli.FlagHelperStringSliceOptions{ + Min: 1, + }) - resourceDescription := cmd.Flag("resource-description").Value.String() - if resourceDescription == "" { - fmt.Println("Flag 'resource-description' is required") - return - } + resourceId := flagHelper.GetRequiredInt32("resource-id") + resourceVersion := flagHelper.GetRequiredInt32("resource-version") + resourceName := flagHelper.GetRequiredString("resource-name") + resourceNamespace := flagHelper.GetRequiredString("resource-namespace") + resourceDescription := flagHelper.GetRequiredString("resource-description") - if resp, err := handlers.UpdateAttribute( - Id, + if _, err := handlers.UpdateAttribute( + id, name, rule, - attrValues, + values, groupBy, resourceId, resourceVersion, resourceName, resourceNamespace, - resourceFqn, resourceDescription, resourceDependencies, ); err != nil { - fmt.Println(err) + cli.ExitWithError("Could not update attribute", err) return } else { - fmt.Println(resp) + fmt.Println(cli.SuccessMessage(fmt.Sprintf("Attribute id: %d updated.", id))) } }, } @@ -302,7 +249,6 @@ func init() { attributeUpdateCmd.Flags().Int32P("resource-version", "V", 0, "ResourceVersion of the attribute definition descriptor") attributeUpdateCmd.Flags().StringP("resource-name", "N", "", "ResourceName of the attribute definition descriptor") attributeUpdateCmd.Flags().StringP("resource-namespace", "S", "", "ResourceNamespace of the attribute definition descriptor") - attributeUpdateCmd.Flags().StringP("resource-fqn", "F", "", "ResourceFqn of the attribute") attributeUpdateCmd.Flags().StringP("resource-description", "D", "", "ResourceDescription of the attribute definition descriptor") attributesCmd.AddCommand(attributesDeleteCmd) From c4df4aed2afbe2e7d8936378224904a6b2a746eb Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Tue, 16 Jan 2024 15:26:15 -0800 Subject: [PATCH 6/8] add get required int32 flag helper method and optional string helper method --- pkg/cli/flagValues.go | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pkg/cli/flagValues.go b/pkg/cli/flagValues.go index 3426177d..3a1bd2ac 100644 --- a/pkg/cli/flagValues.go +++ b/pkg/cli/flagValues.go @@ -29,7 +29,11 @@ func (f FlagHelper) GetRequiredString(flag string) string { return v } -func (f FlagHelper) GetRequiredStringSlice(flag string, v []string, opts FlagHelperStringSliceOptions) []string { +func (f FlagHelper) GetOptionalString(flag string) string { + return f.cmd.Flag(flag).Value.String() +} + +func (f FlagHelper) GetStringSlice(flag string, v []string, opts FlagHelperStringSliceOptions) []string { if len(v) < opts.Min { fmt.Println(ErrorMessage(fmt.Sprintf("Flag %s must have at least %d non-empty values", flag, opts.Min), nil)) os.Exit(1) @@ -40,3 +44,16 @@ func (f FlagHelper) GetRequiredStringSlice(flag string, v []string, opts FlagHel } return v } + +func (f FlagHelper) GetRequiredInt32(flag string) int32 { + v, e := f.cmd.Flags().GetInt32(flag) + if e != nil { + fmt.Println(ErrorMessage("Flag "+flag+" is required", nil)) + os.Exit(1) + } + // if v == 0 { + // fmt.Println(ErrorMessage("Flag "+flag+" must be greater than 0", nil)) + // os.Exit(1) + // } + return v +} From fe6e5bc11751558529c307061e40b70b79e34055 Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Tue, 16 Jan 2024 15:26:44 -0800 Subject: [PATCH 7/8] generate the fqn and make it reusable --- pkg/handlers/attribute.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pkg/handlers/attribute.go b/pkg/handlers/attribute.go index b309a73c..b218a9a4 100644 --- a/pkg/handlers/attribute.go +++ b/pkg/handlers/attribute.go @@ -18,6 +18,7 @@ const ( ) type Attribute struct { + Id int32 Name string Rule string Values []string @@ -41,12 +42,13 @@ func GetAttribute(id int) (Attribute, error) { } return Attribute{ + Id: resp.Definition.Descriptor_.Id, Name: resp.Definition.Name, Rule: GetAttributeRuleFromAttributeType(resp.Definition.Rule), Values: values, Namespace: resp.Definition.Descriptor_.Namespace, Description: resp.Definition.Descriptor_.Description, - Fqn: GetAttributeFqn(resp.Definition), + Fqn: GetAttributeFqn(resp.Definition.Descriptor_.Namespace, resp.Definition.Name), }, nil } @@ -64,6 +66,7 @@ func ListAttributes() ([]Attribute, error) { values = append(values, v.Value) } attrs = append(attrs, Attribute{ + Id: attr.Descriptor_.Id, Name: attr.Name, Rule: GetAttributeRuleFromAttributeType(attr.Rule), Values: values, @@ -111,7 +114,7 @@ func CreateAttribute(name string, rule string, values []string, namespace string } func UpdateAttribute( - Id int32, + id int32, name string, rule string, values []string, @@ -120,7 +123,6 @@ func UpdateAttribute( resourceVersion int32, resourceName string, resourceNamespace string, - resourceFqn string, resourceDescription string, resourceDependencies []string, ) (*attributesv1.UpdateAttributeResponse, error) { @@ -147,7 +149,7 @@ func UpdateAttribute( client := attributesv1.NewAttributesServiceClient(grpc.Conn) return client.UpdateAttribute(grpc.Context, &attributesv1.UpdateAttributeRequest{ - Id: Id, + Id: id, Definition: &attributesv1.AttributeDefinition{ Name: name, Rule: GetAttributeRuleFromReadableString(rule), @@ -159,7 +161,7 @@ func UpdateAttribute( Version: resourceVersion, Name: resourceName, Namespace: resourceNamespace, - Fqn: resourceFqn, + Fqn: GetAttributeFqn(resourceNamespace, resourceName), Description: resourceDescription, Dependencies: dependencies, }, @@ -177,8 +179,8 @@ func DeleteAttribute(id int) error { return err } -func GetAttributeFqn(resp *attributesv1.AttributeDefinition) string { - return fmt.Sprintf("https://%s/attr/%s", resp.Descriptor_.Namespace, resp.Name) +func GetAttributeFqn(namespace string, name string) string { + return fmt.Sprintf("https://%s/attr/%s", namespace, name) } func GetAttributeRuleOptions() []string { From 20b5fffe7be7c59cd658c22d9a540846f195bb9d Mon Sep 17 00:00:00 2001 From: jakedoublev Date: Tue, 16 Jan 2024 15:41:00 -0800 Subject: [PATCH 8/8] allow 0 resource-dependencies to be passed in update call --- cmd/attributes.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/attributes.go b/cmd/attributes.go index fdf128be..accc4ec1 100644 --- a/cmd/attributes.go +++ b/cmd/attributes.go @@ -194,7 +194,7 @@ var attributeUpdateCmd = &cobra.Command{ }) resourceDependencies := flagHelper.GetStringSlice("resource-dependencies", resourceDependencies, cli.FlagHelperStringSliceOptions{ - Min: 1, + Min: 0, }) resourceId := flagHelper.GetRequiredInt32("resource-id")