From 0cdfa5b3dc96236e258ebd429bd36dbeee354992 Mon Sep 17 00:00:00 2001 From: "yufei.li" Date: Thu, 15 Sep 2022 21:04:20 +0800 Subject: [PATCH 1/3] feat: add remove configmap command (cherry picked from commit 0d7c56dcf8d0920f8cc86ac879dfc1da37162831) fix: add logging when configmap not exists (cherry picked from commit 0235f10b09a69e7d440c06146693a84f22be4883) fix: correct lint issues (cherry picked from commit 8ca1f3813bed849c9ebcfaebc60f31ab9aafce03) fix: Resolve conversation (cherry picked from commit 927804dfe58fff240e26e384a612947401089d04) --- kustomize/commands/edit/remove/all.go | 4 + .../commands/edit/remove/removeconfigmap.go | 92 +++++++++++++++++++ .../edit/remove/removeconfigmap_test.go | 84 +++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 kustomize/commands/edit/remove/removeconfigmap.go create mode 100644 kustomize/commands/edit/remove/removeconfigmap_test.go diff --git a/kustomize/commands/edit/remove/all.go b/kustomize/commands/edit/remove/all.go index b86f2541b..016ace963 100644 --- a/kustomize/commands/edit/remove/all.go +++ b/kustomize/commands/edit/remove/all.go @@ -22,6 +22,9 @@ func NewCmdRemove( kustomize edit remove resource {filepath} {filepath} kustomize edit remove resource {pattern} + # Removes one or more configmap from the kustomization file + kustomize edit remove configmap {name1},{name2} + # Removes one or more patches from the kustomization file kustomize edit remove patch --path {filepath} --group {target group name} --version {target version} @@ -37,6 +40,7 @@ func NewCmdRemove( Args: cobra.MinimumNArgs(1), } c.AddCommand( + newCmdRemoveConfigMap(fSys), newCmdRemoveResource(fSys), newCmdRemoveLabel(fSys, v.MakeLabelNameValidator()), newCmdRemoveAnnotation(fSys, v.MakeAnnotationNameValidator()), diff --git a/kustomize/commands/edit/remove/removeconfigmap.go b/kustomize/commands/edit/remove/removeconfigmap.go new file mode 100644 index 000000000..e39bf3ba7 --- /dev/null +++ b/kustomize/commands/edit/remove/removeconfigmap.go @@ -0,0 +1,92 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package remove + +import ( + "errors" + "fmt" + "log" + "strings" + + "github.com/spf13/cobra" + "sigs.k8s.io/kustomize/api/konfig" + "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kustomize/v4/commands/internal/kustfile" + "sigs.k8s.io/kustomize/kyaml/filesys" +) + +type removeConfigMapOptions struct { + configMapNamesToRemove []string +} + +// newCmdRemoveResource remove the name of a file containing a resource to the kustomization file. +func newCmdRemoveConfigMap(fSys filesys.FileSystem) *cobra.Command { + var o removeConfigMapOptions + + cmd := &cobra.Command{ + Use: "configmap", + Short: "Removes specified configmap" + + konfig.DefaultKustomizationFileName(), + Example: ` + remove configmap my-configmap + `, + RunE: func(cmd *cobra.Command, args []string) error { + err := o.Validate(args) + if err != nil { + return err + } + return o.RunRemoveConfigMap(fSys) + }, + } + return cmd +} + +// Validate validates removeConfigMap command. +func (o *removeConfigMapOptions) Validate(args []string) error { + if len(args) == 0 { + return errors.New("must specify a ConfigMap name") + } + if len(args) > 1 { + return fmt.Errorf("too many arguments: %s; to provide multiple ConfigMaps to remove, please separate ConfigMap names by commas", args) + } + o.configMapNamesToRemove = strings.Split(args[0], ",") + return nil +} + +// RunRemoveConfigMap runs ConfigMap command (do real work). +func (o *removeConfigMapOptions) RunRemoveConfigMap(fSys filesys.FileSystem) error { + mf, err := kustfile.NewKustomizationFile(fSys) + if err != nil { + return fmt.Errorf("could not read kustomization file: %w", err) + } + + m, err := mf.Read() + if err != nil { + return fmt.Errorf("could not read kustomization file: %w", err) + } + + foundConfigMaps := make(map[string]struct{}) + + newConfigMaps := make([]types.ConfigMapArgs, 0, len(m.ConfigMapGenerator)) + for _, currentConfigMap := range m.ConfigMapGenerator { + if kustfile.StringInSlice(currentConfigMap.Name, o.configMapNamesToRemove) { + foundConfigMaps[currentConfigMap.Name] = struct{}{} + continue + } + newConfigMaps = append(newConfigMaps, currentConfigMap) + } + + for _, name := range o.configMapNamesToRemove { + if _, found := foundConfigMaps[name]; !found { + log.Printf("configmap %s doesn't exist in kustomization file", name) + } + } + + m.ConfigMapGenerator = newConfigMaps + err = mf.Write(m) + if err != nil { + return fmt.Errorf("configmap cannot write back to file, got %w", err) + } + return nil +} diff --git a/kustomize/commands/edit/remove/removeconfigmap_test.go b/kustomize/commands/edit/remove/removeconfigmap_test.go new file mode 100644 index 000000000..d44829f75 --- /dev/null +++ b/kustomize/commands/edit/remove/removeconfigmap_test.go @@ -0,0 +1,84 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package remove //nolint:testpackage + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + testutils_test "sigs.k8s.io/kustomize/kustomize/v4/commands/internal/testutils" + "sigs.k8s.io/kustomize/kyaml/filesys" +) + +func TestRemoveConfigMap(t *testing.T) { + const configMapName01 = "example-configmap-01" + const configMapName02 = "example-configmap-02" + + tests := map[string]struct { + input string + args []string + expectedErr string + }{ + "happy path": { + input: fmt.Sprintf(` +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +configMapGenerator: +- name: %s + files: + - application.properties +`, configMapName01), + args: []string{configMapName01}, + }, + "multiple": { + input: fmt.Sprintf(` +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +configMapGenerator: +- name: %s + files: + - application.properties +- name: %s + files: + - application.properties +`, configMapName01, configMapName02), + args: []string{ + fmt.Sprintf("%s,%s", configMapName01, configMapName02), + }, + }, + "miss": { + input: fmt.Sprintf(` +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +configMapGenerator: +- name: %s + files: + - application.properties +`, configMapName01), + args: []string{"foo"}, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + fSys := filesys.MakeFsInMemory() + testutils_test.WriteTestKustomizationWith(fSys, []byte(tc.input)) + cmd := newCmdRemoveConfigMap(fSys) + err := cmd.RunE(cmd, tc.args) + if tc.expectedErr != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tc.expectedErr) + } else { + assert.NoError(t, err) + content, err := testutils_test.ReadTestKustomization(fSys) + assert.NoError(t, err) + for _, opt := range strings.Split(tc.args[0], ",") { + assert.NotContains(t, string(content), opt) + } + } + }) + } +} From 0571a2f15d860c52e46e0ab88d5f39d4ca5a4551 Mon Sep 17 00:00:00 2001 From: Mauren Berti Date: Tue, 12 Sep 2023 20:18:45 -0400 Subject: [PATCH 2/3] Incorporate feedback from original PR * Incorporate feedback left on original PR. * Add more test cases. * Minor refactoring and update to copyright notice. --- go.work.sum | 9 +++ .../commands/edit/remove/removeconfigmap.go | 27 +++++--- .../edit/remove/removeconfigmap_test.go | 64 ++++++++++++++----- kustomize/go.mod | 2 +- kustomize/go.sum | 4 +- 5 files changed, 78 insertions(+), 28 deletions(-) diff --git a/go.work.sum b/go.work.sum index 93fe3800d..8adfe90e5 100644 --- a/go.work.sum +++ b/go.work.sum @@ -31,19 +31,28 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.starlark.net v0.0.0-20190528202925-30ae18b8564f/go.mod h1:c1/X6cHgvdXj6pUlmWKMkuqRnW4K8x2vwt6JAaaircg= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= diff --git a/kustomize/commands/edit/remove/removeconfigmap.go b/kustomize/commands/edit/remove/removeconfigmap.go index e39bf3ba7..9ff632f3d 100644 --- a/kustomize/commands/edit/remove/removeconfigmap.go +++ b/kustomize/commands/edit/remove/removeconfigmap.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Kubernetes Authors. +// Copyright 2022 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 package remove @@ -12,7 +12,7 @@ import ( "github.com/spf13/cobra" "sigs.k8s.io/kustomize/api/konfig" "sigs.k8s.io/kustomize/api/types" - "sigs.k8s.io/kustomize/kustomize/v4/commands/internal/kustfile" + "sigs.k8s.io/kustomize/kustomize/v5/commands/internal/kustfile" "sigs.k8s.io/kustomize/kyaml/filesys" ) @@ -26,8 +26,9 @@ func newCmdRemoveConfigMap(fSys filesys.FileSystem) *cobra.Command { cmd := &cobra.Command{ Use: "configmap", - Short: "Removes specified configmap" + + Short: "Removes the specified configmap(s) from " + konfig.DefaultKustomizationFileName(), + Long: "", Example: ` remove configmap my-configmap `, @@ -44,12 +45,13 @@ func newCmdRemoveConfigMap(fSys filesys.FileSystem) *cobra.Command { // Validate validates removeConfigMap command. func (o *removeConfigMapOptions) Validate(args []string) error { - if len(args) == 0 { - return errors.New("must specify a ConfigMap name") - } - if len(args) > 1 { - return fmt.Errorf("too many arguments: %s; to provide multiple ConfigMaps to remove, please separate ConfigMap names by commas", args) + switch { + case len(args) == 0: + return errors.New("at least one configmap name must be specified") + case len(args) > 1: + return fmt.Errorf("too many arguments: %s; to provide multiple configmaps to remove, please separate configmap names by commas", args) } + o.configMapNamesToRemove = strings.Split(args[0], ",") return nil } @@ -63,7 +65,7 @@ func (o *removeConfigMapOptions) RunRemoveConfigMap(fSys filesys.FileSystem) err m, err := mf.Read() if err != nil { - return fmt.Errorf("could not read kustomization file: %w", err) + return fmt.Errorf("could not read kustomization file contents: %w", err) } foundConfigMaps := make(map[string]struct{}) @@ -77,6 +79,11 @@ func (o *removeConfigMapOptions) RunRemoveConfigMap(fSys filesys.FileSystem) err newConfigMaps = append(newConfigMaps, currentConfigMap) } + if len(foundConfigMaps) == 0 { + return fmt.Errorf("no specified configmap(s) were found in the %s file", + konfig.DefaultKustomizationFileName()) + } + for _, name := range o.configMapNamesToRemove { if _, found := foundConfigMaps[name]; !found { log.Printf("configmap %s doesn't exist in kustomization file", name) @@ -86,7 +93,7 @@ func (o *removeConfigMapOptions) RunRemoveConfigMap(fSys filesys.FileSystem) err m.ConfigMapGenerator = newConfigMaps err = mf.Write(m) if err != nil { - return fmt.Errorf("configmap cannot write back to file, got %w", err) + return fmt.Errorf("failed to write kustomization file: %w", err) } return nil } diff --git a/kustomize/commands/edit/remove/removeconfigmap_test.go b/kustomize/commands/edit/remove/removeconfigmap_test.go index d44829f75..b44f5c1c5 100644 --- a/kustomize/commands/edit/remove/removeconfigmap_test.go +++ b/kustomize/commands/edit/remove/removeconfigmap_test.go @@ -1,15 +1,16 @@ -// Copyright 2019 The Kubernetes Authors. +// Copyright 2022 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 package remove //nolint:testpackage import ( "fmt" - "strings" "testing" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/assert" - testutils_test "sigs.k8s.io/kustomize/kustomize/v4/commands/internal/testutils" + testutils_test "sigs.k8s.io/kustomize/kustomize/v5/commands/internal/testutils" "sigs.k8s.io/kustomize/kyaml/filesys" ) @@ -18,9 +19,10 @@ func TestRemoveConfigMap(t *testing.T) { const configMapName02 = "example-configmap-02" tests := map[string]struct { - input string - args []string - expectedErr string + input string + args []string + expectedOutput string + expectedErr string }{ "happy path": { input: fmt.Sprintf(` @@ -32,6 +34,10 @@ configMapGenerator: - application.properties `, configMapName01), args: []string{configMapName01}, + expectedOutput: ` +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +`, }, "multiple": { input: fmt.Sprintf(` @@ -48,6 +54,10 @@ configMapGenerator: args: []string{ fmt.Sprintf("%s,%s", configMapName01, configMapName02), }, + expectedOutput: ` +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +`, }, "miss": { input: fmt.Sprintf(` @@ -58,7 +68,31 @@ configMapGenerator: files: - application.properties `, configMapName01), - args: []string{"foo"}, + args: []string{"foo"}, + expectedErr: "no specified configmap(s) were found", + }, + "no configmap name specified": { + args: []string{}, + expectedErr: "at least one configmap name must be specified", + }, + "too many configmap names specified": { + args: []string{"test1", "test2"}, + expectedErr: "too many arguments", + }, + "one existing and one non-existing": { + input: fmt.Sprintf(` +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +configMapGenerator: +- name: %s + files: + - application.properties +`, configMapName01), + args: []string{fmt.Sprintf("%s,%s", configMapName01, "foo")}, + expectedOutput: ` +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +`, }, } @@ -68,17 +102,17 @@ configMapGenerator: testutils_test.WriteTestKustomizationWith(fSys, []byte(tc.input)) cmd := newCmdRemoveConfigMap(fSys) err := cmd.RunE(cmd, tc.args) + if tc.expectedErr != "" { - assert.Error(t, err) + require.Error(t, err) assert.Contains(t, err.Error(), tc.expectedErr) - } else { - assert.NoError(t, err) - content, err := testutils_test.ReadTestKustomization(fSys) - assert.NoError(t, err) - for _, opt := range strings.Split(tc.args[0], ",") { - assert.NotContains(t, string(content), opt) - } + return } + + require.NoError(t, err) + content, err := testutils_test.ReadTestKustomization(fSys) + require.NoError(t, err) + require.Equal(t, tc.expectedOutput, string(content)) }) } } diff --git a/kustomize/go.mod b/kustomize/go.mod index 0c7e25e97..47b4e923c 100644 --- a/kustomize/go.mod +++ b/kustomize/go.mod @@ -33,7 +33,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect - golang.org/x/sys v0.8.0 // indirect + golang.org/x/sys v0.12.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/evanphx/json-patch.v5 v5.6.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/kustomize/go.sum b/kustomize/go.sum index d6ba3eebf..96ad6a12a 100644 --- a/kustomize/go.sum +++ b/kustomize/go.sum @@ -67,8 +67,8 @@ github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc= go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 9107fa3c81f7c2e88c7b2dea0da4718a61314094 Mon Sep 17 00:00:00 2001 From: Mauren Berti Date: Tue, 19 Sep 2023 22:29:33 -0400 Subject: [PATCH 3/3] fix: changes from code review * Replace last assert directive with require in the test. * Change copyright notice. --- kustomize/commands/edit/remove/removeconfigmap.go | 2 +- kustomize/commands/edit/remove/removeconfigmap_test.go | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/kustomize/commands/edit/remove/removeconfigmap.go b/kustomize/commands/edit/remove/removeconfigmap.go index 9ff632f3d..437269be5 100644 --- a/kustomize/commands/edit/remove/removeconfigmap.go +++ b/kustomize/commands/edit/remove/removeconfigmap.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Kubernetes Authors. +// Copyright 2023 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 package remove diff --git a/kustomize/commands/edit/remove/removeconfigmap_test.go b/kustomize/commands/edit/remove/removeconfigmap_test.go index b44f5c1c5..d1ac37c23 100644 --- a/kustomize/commands/edit/remove/removeconfigmap_test.go +++ b/kustomize/commands/edit/remove/removeconfigmap_test.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Kubernetes Authors. +// Copyright 2023 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 package remove //nolint:testpackage @@ -8,8 +8,6 @@ import ( "testing" "github.com/stretchr/testify/require" - - "github.com/stretchr/testify/assert" testutils_test "sigs.k8s.io/kustomize/kustomize/v5/commands/internal/testutils" "sigs.k8s.io/kustomize/kyaml/filesys" ) @@ -105,7 +103,7 @@ kind: Kustomization if tc.expectedErr != "" { require.Error(t, err) - assert.Contains(t, err.Error(), tc.expectedErr) + require.Contains(t, err.Error(), tc.expectedErr) return }