diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46c856e..df76078 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,9 @@ jobs: - name: Run 'all' make target run: make all - name: Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: file: ./coverage.txt # optional flags: unittests # optional diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..dd89762 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,8 @@ +coverage: + range: "70...90" + round: down + precision: 2 + +ignore: + - "examples" + - "generate" \ No newline at end of file diff --git a/docs/syntax.md b/docs/syntax.md index 40457d6..5b02a48 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -34,6 +34,7 @@ Below you will find the step syntax next to the name of the method it utilizes. - ` some pods in namespace with selector don't have "" in logs since time` kdt.KubeClientSet.SomePodsInNamespaceWithSelectorDontHaveStringInLogsSinceTime - ` [the] pods in namespace with selector have no errors in logs since time` kdt.KubeClientSet.PodsInNamespaceWithSelectorHaveNoErrorsInLogsSinceTime - ` [the] pods in namespace with selector have some errors in logs since time` kdt.KubeClientSet.PodsInNamespaceWithSelectorHaveSomeErrorsInLogsSinceTime +- ` [all] [the] (pod|pods) in [the] namespace with [the] label selector [should] converge to [the] field selector ` kdt.KubeClientSet.PodsInNamespaceWithLabelSelectorConvergeToFieldSelector - ` [the] pods in namespace with selector should have labels ` kdt.KubeClientSet.PodsInNamespaceWithSelectorShouldHaveLabels - ` [the] pod in namespace should have labels ` kdt.KubeClientSet.PodInNamespaceShouldHaveLabels diff --git a/generate/syntax/main.go b/generate/syntax/main.go index 70e7b9a..5472db8 100644 --- a/generate/syntax/main.go +++ b/generate/syntax/main.go @@ -21,6 +21,7 @@ import ( "strconv" "strings" + "github.com/keikoproj/kubedog/generate/syntax/replace" log "github.com/sirupsen/logrus" ) @@ -48,18 +49,37 @@ const ( destinationFileBeginning = "# Syntax" + newLine + "Below you will find the step syntax next to the name of the method it utilizes. Here GK stands for [Gherkin](https://cucumber.io/docs/gherkin/reference/#keywords) Keyword and words in brackets ([]) are optional:" + newLine ) -var replacers = []struct { - replacee string - replacer string -}{ - {`(?:`, `[`}, - {` )?`, `] `}, - {`)?`, `]`}, - {`(\d+)`, ``}, - {`(\S+)`, ``}, - {`([^"]*)`, ``}, - {`\(`, `(`}, - {`\)`, `)`}, +var replacements = replace.Replacements{ + {Replacee: `(\d+)`, Replacer: ``}, + {Replacee: `(\S+)`, Replacer: ``}, + {Replacee: `([^"]*)`, Replacer: ``}, +} + +var bracketsReplacements = replace.BracketsReplacements{ + { + Opening: replace.Replacement{ + Replacee: `(?:`, Replacer: `[`}, + Closing: replace.Replacement{ + Replacee: ` )?`, Replacer: `] `}, + }, + { + Opening: replace.Replacement{ + Replacee: `(?:`, Replacer: `[`}, + Closing: replace.Replacement{ + Replacee: `)?`, Replacer: `]`}, + }, + { + Opening: replace.Replacement{ + Replacee: `(?:`, Replacer: `(`}, + Closing: replace.Replacement{ + Replacee: `)`, Replacer: `)`}, + }, + { + Opening: replace.Replacement{ + Replacee: `\(`, Replacer: `(`}, + Closing: replace.Replacement{ + Replacee: `\)`, Replacer: `)`}, + }, } func main() { @@ -143,9 +163,8 @@ func processStep(rawStep string) string { processedStep := rawStepSplit[1] processedStep = strings.TrimPrefix(processedStep, stepPrefix) processedStep = strings.TrimSuffix(processedStep, stepSuffix) - for _, r := range replacers { - processedStep = strings.ReplaceAll(processedStep, r.replacee, r.replacer) - } + processedStep = replacements.Replace(processedStep) + processedStep = bracketsReplacements.Replace(processedStep) method := rawStepSplit[2] method = strings.TrimPrefix(method, methodPrefix) method = strings.TrimSuffix(method, methodSuffix) diff --git a/generate/syntax/replace/replace.go b/generate/syntax/replace/replace.go new file mode 100644 index 0000000..ba13d83 --- /dev/null +++ b/generate/syntax/replace/replace.go @@ -0,0 +1,89 @@ +/* +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 replace + +import ( + "bytes" + "log" + "regexp" + "strings" +) + +const regExp_CharsWithinBrackets = "([^(]*)" + +type Replacement struct { + Replacee string + Replacer string +} + +func (r Replacement) Replace(src string) string { + return strings.ReplaceAll(src, r.Replacee, r.Replacer) +} + +type Replacements []Replacement + +func (rs Replacements) Replace(src string) string { + new := src + for _, r := range rs { + new = r.Replace(new) + } + return new +} + +type BracketsReplacement struct { + Opening Replacement + Closing Replacement +} + +func (br BracketsReplacement) Replace(src string) string { + re, err := regexp.Compile(br.getRegExp()) + if err != nil { + log.Fatal(err) + } + new := re.ReplaceAllFunc([]byte(src), br.replaceSingle) + return string(new) +} + +func (br BracketsReplacement) replaceSingle(src []byte) []byte { + s := string(src) + s = br.Opening.Replace(s) + s = br.Closing.Replace(s) + return []byte(s) +} + +func (br BracketsReplacement) getRegExp() string { + return escapeEveryCharacter(br.Opening.Replacee) + + regExp_CharsWithinBrackets + + escapeEveryCharacter(br.Closing.Replacee) +} + +func escapeEveryCharacter(s string) string { + var buffer bytes.Buffer + for _, c := range s { + buffer.WriteString(`\`) + buffer.WriteRune(c) + } + return buffer.String() +} + +type BracketsReplacements []BracketsReplacement + +func (brs BracketsReplacements) Replace(src string) string { + new := src + for _, br := range brs { + new = br.Replace(new) + } + return new +} diff --git a/generate/syntax/replace/replace_test.go b/generate/syntax/replace/replace_test.go new file mode 100644 index 0000000..5a0049d --- /dev/null +++ b/generate/syntax/replace/replace_test.go @@ -0,0 +1,259 @@ +/* +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 replace + +import ( + "reflect" + "testing" +) + +func TestBracketsReplacement_Replace(t *testing.T) { + type fields struct { + Opening Replacement + Closing Replacement + } + type args struct { + src string + } + tests := []struct { + name string + fields fields + args args + want string + }{ + { + name: "Positive Test: '(?:' & ' )?' Case 1", + fields: fields{ + Opening: Replacement{ + Replacee: "(?:", + Replacer: "[", + }, + Closing: Replacement{ + Replacee: " )?", + Replacer: "] ", + }, + }, + args: args{ + src: `(?:I )?wait (?:for )?(\d+) (minutes|seconds)`, + }, + want: `[I] wait [for] (\d+) (minutes|seconds)`, + }, + { + name: "Positive Test: '(?:' & ' )?' Case 2", + fields: fields{ + Opening: Replacement{ + Replacee: "(?:", + Replacer: "[", + }, + Closing: Replacement{ + Replacee: " )?", + Replacer: "] ", + }, + }, + args: args{ + src: `(?:all )?(?:the )?(?:pod|pods) in (?:the )?namespace (\S+) with (?:the )?label selector (\S+) (?:should )?converge to (?:the )?field selector (\S+)`, + }, + want: `[all] [the] (?:pod|pods) in [the] namespace (\S+) with [the] label selector (\S+) [should] converge to [the] field selector (\S+)`, + }, + { + name: "Positive Test: '(?:' & ')' Case 1", + fields: fields{ + Opening: Replacement{ + Replacee: "(?:", + Replacer: "(", + }, + Closing: Replacement{ + Replacee: ")", + Replacer: ")", + }, + }, + args: args{ + src: `[all] [the] (?:pod|pods) in [the] namespace (\S+) with [the] label selector (\S+) [should] converge to [the] field selector (\S+)`, + }, + want: `[all] [the] (pod|pods) in [the] namespace (\S+) with [the] label selector (\S+) [should] converge to [the] field selector (\S+)`, + }, + { + name: "Positive Test: '(?:' & ' )?' Case 3", + fields: fields{ + Opening: Replacement{ + Replacee: "(?:", + Replacer: "[", + }, + Closing: Replacement{ + Replacee: " )?", + Replacer: "] ", + }, + }, + args: args{ + src: `(?:I )?send (\d+) tps to ingress (\S+) in (?:the )?namespace (\S+) (?:available )?on port (\d+) and path ([^"]*) for (\d+) (minutes|seconds) expecting up to (\d+) error(?:s)?`, + }, + want: `[I] send (\d+) tps to ingress (\S+) in [the] namespace (\S+) [available] on port (\d+) and path ([^"]*) for (\d+) (minutes|seconds) expecting up to (\d+) error(?:s)?`, + }, + { + name: "Positive Test: '(?:' & ')?' Case 1", + fields: fields{ + Opening: Replacement{ + Replacee: "(?:", + Replacer: "[", + }, + Closing: Replacement{ + Replacee: ")?", + Replacer: "]", + }, + }, + args: args{ + src: `[I] send (\d+) tps to ingress (\S+) in [the] namespace (\S+) [available] on port (\d+) and path ([^"]*) for (\d+) (minutes|seconds) expecting up to (\d+) error(?:s)?`, + }, + want: `[I] send (\d+) tps to ingress (\S+) in [the] namespace (\S+) [available] on port (\d+) and path ([^"]*) for (\d+) (minutes|seconds) expecting up to (\d+) error[s]`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + br := BracketsReplacement{ + Opening: tt.fields.Opening, + Closing: tt.fields.Closing, + } + if got := br.Replace(tt.args.src); got != tt.want { + t.Errorf("BracketsReplacement.Replace() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestBracketsReplacement_replaceSingle(t *testing.T) { + type fields struct { + Opening Replacement + Closing Replacement + } + type args struct { + src []byte + } + tests := []struct { + name string + fields fields + args args + want []byte + }{ + { + name: "Positive Test", + fields: fields{ + Opening: Replacement{ + Replacee: "(?:", + Replacer: "(", + }, + Closing: Replacement{ + Replacee: " )", + Replacer: ")", + }, + }, + args: args{ + src: []byte("(?:I )"), + }, + want: []byte("(I)"), + }, + { + name: "Positive Test", + fields: fields{ + Opening: Replacement{ + Replacee: "(?:", + Replacer: "(", + }, + Closing: Replacement{ + Replacee: ")", + Replacer: ")", + }, + }, + args: args{ + src: []byte("(?:pod|pods)"), + }, + want: []byte("(pod|pods)"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + br := BracketsReplacement{ + Opening: tt.fields.Opening, + Closing: tt.fields.Closing, + } + if got := br.replaceSingle(tt.args.src); !reflect.DeepEqual(got, tt.want) { + t.Errorf("BracketsReplacement.replaceSingle() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestBracketsReplacement_getRegExp(t *testing.T) { + type fields struct { + Opening Replacement + Closing Replacement + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "Positive Test", + fields: fields{ + Opening: Replacement{ + Replacee: "(?:", + Replacer: "(", + }, + Closing: Replacement{ + Replacee: " )", + Replacer: ")", + }, + }, + want: `\(\?\:` + regExp_CharsWithinBrackets + `\ \)`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + br := BracketsReplacement{ + Opening: tt.fields.Opening, + Closing: tt.fields.Closing, + } + if got := br.getRegExp(); got != tt.want { + t.Errorf("BracketsReplacement.getRegExp() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_escapeEveryCharacter(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "Positive Test", + args: args{ + s: "(?:", + }, + want: `\(\?\:`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := escapeEveryCharacter(tt.args.s); got != tt.want { + t.Errorf("escapeEveryCharacter() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/kubedog.go b/kubedog.go index 43c5e08..8096eea 100644 --- a/kubedog.go +++ b/kubedog.go @@ -65,6 +65,7 @@ func (kdt *Test) SetScenario(scenario *godog.ScenarioContext) { kdt.scenario.Step(`^some pods in namespace (\S+) with selector (\S+) don't have "([^"]*)" in logs since ([^"]*) time$`, kdt.KubeClientSet.SomePodsInNamespaceWithSelectorDontHaveStringInLogsSinceTime) kdt.scenario.Step(`^(?:the )?pods in namespace (\S+) with selector (\S+) have no errors in logs since ([^"]*) time$`, kdt.KubeClientSet.PodsInNamespaceWithSelectorHaveNoErrorsInLogsSinceTime) kdt.scenario.Step(`^(?:the )?pods in namespace (\S+) with selector (\S+) have some errors in logs since ([^"]*) time$`, kdt.KubeClientSet.PodsInNamespaceWithSelectorHaveSomeErrorsInLogsSinceTime) + kdt.scenario.Step(`^(?:all )?(?:the )?(?:pod|pods) in (?:the )?namespace (\S+) with (?:the )?label selector (\S+) (?:should )?converge to (?:the )?field selector (\S+)$`, kdt.KubeClientSet.PodsInNamespaceWithLabelSelectorConvergeToFieldSelector) kdt.scenario.Step(`^(?:the )?pods in namespace (\S+) with selector (\S+) should have labels (\S+)$`, kdt.KubeClientSet.PodsInNamespaceWithSelectorShouldHaveLabels) kdt.scenario.Step(`^(?:the )?pod (\S+) in namespace (\S+) should have labels (\S+)$`, kdt.KubeClientSet.PodInNamespaceShouldHaveLabels) //syntax-generation:title-2:Others diff --git a/pkg/kube/kube.go b/pkg/kube/kube.go index 474f8cb..335cf30 100644 --- a/pkg/kube/kube.go +++ b/pkg/kube/kube.go @@ -256,6 +256,10 @@ func (kc *ClientSet) PodsInNamespaceWithSelectorHaveSomeErrorsInLogsSinceTime(na return pod.PodsInNamespaceWithSelectorHaveSomeErrorsInLogsSinceTime(kc.KubeInterface, namespace, selector, timestamp) } +func (kc *ClientSet) PodsInNamespaceWithLabelSelectorConvergeToFieldSelector(namespace, labelSelector, fieldSelector string) error { + return pod.PodsInNamespaceWithLabelSelectorConvergeToFieldSelector(kc.KubeInterface, kc.getExpBackoff(), namespace, labelSelector, fieldSelector) +} + func (kc *ClientSet) PodsInNamespaceWithSelectorShouldHaveLabels(namespace, selector, labels string) error { return pod.PodsInNamespaceWithSelectorShouldHaveLabels(kc.KubeInterface, namespace, selector, labels) } diff --git a/pkg/kube/pod/pod.go b/pkg/kube/pod/pod.go index faaa342..719de94 100644 --- a/pkg/kube/pod/pod.go +++ b/pkg/kube/pod/pod.go @@ -17,6 +17,7 @@ package pod import ( "context" "fmt" + "reflect" "strings" "time" @@ -85,6 +86,46 @@ func PodsWithSelectorHaveRestartCountLessThan(kubeClientset kubernetes.Interface return nil } +func PodsInNamespaceWithLabelSelectorConvergeToFieldSelector(kubeClientset kubernetes.Interface, expBackoff wait.Backoff, namespace, labelSelector, fieldSelector string) error { + return util.RetryOnAnyError(&expBackoff, func() error { + podList, err := GetPodListWithLabelSelector(kubeClientset, namespace, labelSelector) + if err != nil { + return err + } + n := len(podList.Items) + if n == 0 { + return fmt.Errorf("no pods matched label selector '%s'", labelSelector) + } + log.Infof("found '%d' pods with label selector '%s'", n, labelSelector) + + podListWithSelector, err := GetPodListWithLabelSelectorAndFieldSelector(kubeClientset, namespace, labelSelector, fieldSelector) + if err != nil { + return err + } + m := len(podListWithSelector.Items) + if m == 0 { + return fmt.Errorf("no pods matched label selector '%s' and field selector '%s'", labelSelector, fieldSelector) + } + log.Infof("found '%d' pods with label selector '%s' and field selector '%s'", m, labelSelector, fieldSelector) + + message := fmt.Sprintf("'%d/%d' pod(s) with label selector '%s' converged to field selector '%s'", m, n, labelSelector, fieldSelector) + if n != m { + return errors.New(message) + } + podsUIDs := []string{} + podsWithSelectorUIDs := []string{} + for i := range podList.Items { + podsUIDs = append(podsUIDs, string(podList.Items[i].UID)) + podsWithSelectorUIDs = append(podsWithSelectorUIDs, string(podListWithSelector.Items[i].UID)) + } + if !reflect.DeepEqual(podsUIDs, podsWithSelectorUIDs) { + return fmt.Errorf("pods UIDs with label selector '%s' do not match pods UIDs with said label selector and field selector '%s': '%v' and '%v', respectively", labelSelector, fieldSelector, podsUIDs, podsWithSelectorUIDs) + } + log.Info(message) + return nil + }) +} + func SomeOrAllPodsInNamespaceWithSelectorHaveStringInLogsSinceTime(kubeClientset kubernetes.Interface, expBackoff wait.Backoff, SomeOrAll, namespace, selector, searchKeyword string, since time.Time) error { return util.RetryOnAnyError(&expBackoff, func() error { pods, err := GetPodListWithLabelSelector(kubeClientset, namespace, selector) diff --git a/pkg/kube/pod/pod_helper.go b/pkg/kube/pod/pod_helper.go index 6a0d1df..a4130af 100644 --- a/pkg/kube/pod/pod_helper.go +++ b/pkg/kube/pod/pod_helper.go @@ -29,13 +29,17 @@ import ( "k8s.io/client-go/kubernetes" ) -func GetPodListWithLabelSelector(kubeClientset kubernetes.Interface, namespace, selector string) (*corev1.PodList, error) { +func GetPodListWithLabelSelector(kubeClientset kubernetes.Interface, namespace, labelSelector string) (*corev1.PodList, error) { + return GetPodListWithLabelSelectorAndFieldSelector(kubeClientset, namespace, labelSelector, "") +} + +func GetPodListWithLabelSelectorAndFieldSelector(kubeClientset kubernetes.Interface, namespace, labelSelector, fieldSelector string) (*corev1.PodList, error) { if err := common.ValidateClientset(kubeClientset); err != nil { return nil, err } pods, err := util.RetryOnError(&util.DefaultRetry, util.IsRetriable, func() (interface{}, error) { - return kubeClientset.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{LabelSelector: selector}) + return kubeClientset.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{LabelSelector: labelSelector, FieldSelector: fieldSelector}) }) if err != nil { return nil, errors.Wrap(err, "failed to list pods") diff --git a/pkg/kube/pod/pod_test.go b/pkg/kube/pod/pod_test.go index 3dd0856..ba27815 100644 --- a/pkg/kube/pod/pod_test.go +++ b/pkg/kube/pod/pod_test.go @@ -17,9 +17,11 @@ package pod import ( "testing" + "github.com/keikoproj/kubedog/internal/util" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/wait" fakeDiscovery "k8s.io/client-go/discovery/fake" "k8s.io/client-go/dynamic" fakeDynamic "k8s.io/client-go/dynamic/fake" @@ -131,3 +133,61 @@ func Test_PodsInNamespaceWithSelectorShouldHaveLabels(t *testing.T) { }) } } + +func TestPodsInNamespaceWithLabelSelectorConvergeToFieldSelector(t *testing.T) { + type args struct { + kubeClientset kubernetes.Interface + expBackoff wait.Backoff + namespace string + labelSelector string + fieldSelector string + } + namespaceName := "test-ns" + ns := v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespaceName}} + podSucceeded := v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod-xhhxj", + Namespace: "test-ns", + Labels: map[string]string{ + "app": "test-service", + }, + }, + Status: v1.PodStatus{ + Phase: v1.PodSucceeded, + }, + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Positive Test", + args: args{ + kubeClientset: fake.NewSimpleClientset(&ns, &podSucceeded), + expBackoff: util.DefaultRetry, + namespace: namespaceName, + labelSelector: "app=test-service", + fieldSelector: "status.phase=Succeeded", + }, + }, + { + name: "Negative Test: no pods with label selector", + args: args{ + kubeClientset: fake.NewSimpleClientset(&ns), + expBackoff: util.DefaultRetry, + namespace: namespaceName, + labelSelector: "app=test-service", + fieldSelector: "status.phase=Succeeded", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := PodsInNamespaceWithLabelSelectorConvergeToFieldSelector(tt.args.kubeClientset, tt.args.expBackoff, tt.args.namespace, tt.args.labelSelector, tt.args.fieldSelector); (err != nil) != tt.wantErr { + t.Errorf("PodsInNamespaceWithLabelSelectorConvergeToFieldSelector() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}