├── codecov.yml ├── examples ├── usage │ ├── templates │ │ ├── namespace.yaml │ │ └── pod.yaml │ ├── features │ │ └── deploy-pod.feature │ ├── main_test.go │ └── go.mod └── templating │ ├── kube │ ├── templates │ │ ├── namespace.yaml │ │ └── pod.yaml │ ├── features │ │ └── deploy-pod.feature │ ├── main_test.go │ └── go.mod │ └── generic │ ├── files │ ├── pod.yaml │ └── generated_pod.yaml │ ├── go.mod │ ├── main.go │ └── go.sum ├── pkg ├── generic │ ├── test │ │ ├── templated.yaml │ │ └── templated-bad-kind.yaml │ ├── generic.go │ ├── generic_test.go │ ├── template.go │ └── template_test.go ├── kube │ ├── unstructured │ │ ├── test │ │ │ ├── templates │ │ │ │ └── templated.yaml │ │ │ └── files │ │ │ │ ├── instance-group.yaml │ │ │ │ ├── instance-group-not-ready.yaml │ │ │ │ ├── resource-no-ns.yaml │ │ │ │ ├── resource.yaml │ │ │ │ ├── multi-resource-no-ns.yaml │ │ │ │ ├── multi-resource.yaml │ │ │ │ └── analysis-template.yaml │ │ └── unstructured_helper.go │ ├── common │ │ └── common.go │ ├── kube_helper.go │ ├── pod │ │ ├── pod_helper.go │ │ ├── pod_test.go │ │ └── pod.go │ ├── structured │ │ └── structured_helper.go │ └── kube.go └── aws │ ├── aws_helper_test.go │ ├── iam │ ├── iam_helper_test.go │ ├── iam.go │ ├── iam_test.go │ └── iam_helper.go │ ├── aws_helper.go │ ├── aws_test.go │ └── aws.go ├── .gitignore ├── .github ├── CODEOWNERS ├── workflows │ ├── syntax.yml │ ├── examples.yml │ ├── golangci-lint.yml │ └── build-test-coverage.yml └── dependabot.yml ├── README.md ├── Makefile ├── docs ├── examples.md └── syntax.md ├── generate └── syntax │ ├── replace │ ├── replace.go │ └── replace_test.go │ └── main.go ├── internal └── util │ ├── util_test.go │ └── util.go ├── go.mod ├── kubedog.go └── LICENSE /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: "70...90" 3 | round: down 4 | precision: 2 5 | 6 | ignore: 7 | - "examples" -------------------------------------------------------------------------------- /examples/usage/templates/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: kubedog-example -------------------------------------------------------------------------------- /pkg/generic/test/templated.yaml: -------------------------------------------------------------------------------- 1 | kind: {{.Kind}} 2 | apiVersion: {{.ApiVersion}} 3 | metadata: 4 | name: {{.Name}} -------------------------------------------------------------------------------- /examples/templating/kube/templates/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: {{.Namespace}} -------------------------------------------------------------------------------- /pkg/generic/test/templated-bad-kind.yaml: -------------------------------------------------------------------------------- 1 | kind: {{.Kindd}} 2 | apiVersion: {{.ApiVersion}} 3 | metadata: 4 | name: {{.Name}} -------------------------------------------------------------------------------- /pkg/kube/unstructured/test/templates/templated.yaml: -------------------------------------------------------------------------------- 1 | kind: {{.Kind}} 2 | apiVersion: {{.ApiVersion}} 3 | metadata: 4 | name: {{.Name}} -------------------------------------------------------------------------------- /pkg/kube/unstructured/test/files/instance-group.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: instancemgr.keikoproj.io/v1alpha1 2 | kind: InstanceGroup 3 | metadata: 4 | name: hello-world 5 | namespace: instance-manager 6 | status: 7 | currentState: Ready -------------------------------------------------------------------------------- /pkg/kube/unstructured/test/files/instance-group-not-ready.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: instancemgr.keikoproj.io/v1alpha1 2 | kind: InstanceGroup 3 | metadata: 4 | name: hello-world 5 | namespace: instance-manager 6 | status: 7 | currentState: NotReady -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | .DS_Store 4 | *.test 5 | pkg/generic/test/generated_* 6 | pkg/kube/unstructured/test/templates/generated_* 7 | kubedog 8 | coverage.txt 9 | examples/templating/generic/generic 10 | 11 | # ignore edit files 12 | .*~ 13 | .*.sw? 14 | -------------------------------------------------------------------------------- /pkg/kube/unstructured/test/files/resource-no-ns.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: someGroup.apiVersion/SomeVersion 2 | kind: SomeKind 3 | metadata: 4 | name: someResource 5 | labels: 6 | someTestKey: someTestValue 7 | status: 8 | conditions: 9 | - type: someConditionType 10 | status: "True" -------------------------------------------------------------------------------- /examples/usage/templates/pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: nginx-kubedog-example 5 | namespace: kubedog-example 6 | labels: 7 | tier: backend 8 | spec: 9 | containers: 10 | - name: hello 11 | image: busybox:1.28 12 | command: ['sh', '-c', 'echo "Hello, Kubedog!" && sleep 3600'] 13 | restartPolicy: OnFailure -------------------------------------------------------------------------------- /examples/templating/generic/files/pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: nginx-{{.Namespace}} 5 | namespace: {{.Namespace}} 6 | labels: 7 | tier: backend 8 | spec: 9 | containers: 10 | - name: hello 11 | image: {{.Image}} 12 | command: ['sh', '-c', 'echo "{{.Message}}" && sleep 3600'] 13 | restartPolicy: OnFailure -------------------------------------------------------------------------------- /examples/templating/kube/templates/pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: nginx-{{.Namespace}} 5 | namespace: {{.Namespace}} 6 | labels: 7 | tier: backend 8 | spec: 9 | containers: 10 | - name: hello 11 | image: {{.Image}} 12 | command: ['sh', '-c', 'echo "{{.Message}}" && sleep 3600'] 13 | restartPolicy: OnFailure -------------------------------------------------------------------------------- /examples/templating/generic/files/generated_pod.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: nginx-kubedog-example 5 | namespace: kubedog-example 6 | labels: 7 | tier: backend 8 | spec: 9 | containers: 10 | - name: hello 11 | image: busybox:1.28 12 | command: ['sh', '-c', 'echo "Hello, Kubedog!" && sleep 3600'] 13 | restartPolicy: OnFailure -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # These owners will be the default owners for everything in 2 | # the repo. Unless a later match takes precedence, 3 | # review when someone opens a pull request. 4 | * @keikoproj/authorized-approvers 5 | 6 | # Admins own root and CI. 7 | .github/** @keikoproj/keiko-admins @keikoproj/keiko-maintainers 8 | /* @keikoproj/keiko-admins @keikoproj/keiko-maintainers 9 | -------------------------------------------------------------------------------- /.github/workflows/syntax.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | pull_request: 9 | jobs: 10 | build: 11 | name: syntax 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/setup-go@v5 15 | with: 16 | go-version: ^1.21 17 | id: go 18 | - uses: actions/checkout@v4 19 | - run: make check-syntax 20 | -------------------------------------------------------------------------------- /.github/workflows/examples.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | pull_request: 9 | jobs: 10 | build: 11 | name: examples 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/setup-go@v5 15 | with: 16 | go-version: ^1.21 17 | id: go 18 | - uses: actions/checkout@v4 19 | - run: make build-examples 20 | -------------------------------------------------------------------------------- /pkg/kube/unstructured/test/files/resource.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: someGroup.apiVersion/SomeVersion 2 | kind: SomeKind 3 | metadata: 4 | name: someResource 5 | namespace: someTestNamespace 6 | labels: 7 | someTestKey: someTestValue 8 | status: 9 | conditions: 10 | - type: someConditionType 11 | status: "True" 12 | replicaCount: 2 13 | spec: 14 | template: 15 | containers: 16 | - name: someContainer 17 | image: someImage 18 | version: 1.0.0 19 | ports: 20 | - containerPort: 8080 21 | - containerPort: 8940 -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | pull_request: 9 | permissions: 10 | contents: read 11 | jobs: 12 | build: 13 | name: golangci-lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/setup-go@v5 17 | with: 18 | go-version: 1.21 19 | - uses: actions/checkout@v4 20 | - uses: golangci/golangci-lint-action@v6 21 | with: 22 | version: v1.55.2 23 | args: --timeout 3m --verbose -------------------------------------------------------------------------------- /pkg/kube/unstructured/test/files/multi-resource-no-ns.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: someGroup.apiVersion/SomeVersion 2 | kind: SomeKind 3 | metadata: 4 | name: someResource 5 | labels: 6 | someTestKey: someTestValue 7 | status: 8 | conditions: 9 | - type: someConditionType 10 | status: "True" 11 | --- 12 | apiVersion: otherGroup.apiVersion/OtherVersion 13 | kind: OtherKind 14 | metadata: 15 | name: otherResource 16 | labels: 17 | someTestKey: someTestValue 18 | status: 19 | conditions: 20 | - type: someConditionType 21 | status: "True" -------------------------------------------------------------------------------- /examples/templating/generic/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/keikoproj/kubedog/examples/templating/generic 2 | 3 | go 1.21 4 | 5 | replace github.com/keikoproj/kubedog => ../../../ 6 | 7 | require github.com/keikoproj/kubedog v1.2.3 8 | 9 | require ( 10 | github.com/go-logr/logr v1.2.4 // indirect 11 | github.com/pkg/errors v0.9.1 // indirect 12 | github.com/sirupsen/logrus v1.9.3 // indirect 13 | golang.org/x/sys v0.18.0 // indirect 14 | k8s.io/apimachinery v0.28.12 // indirect 15 | k8s.io/klog/v2 v2.100.1 // indirect 16 | k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /pkg/kube/unstructured/test/files/multi-resource.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: someGroup.apiVersion/SomeVersion 2 | kind: SomeKind 3 | metadata: 4 | name: someResource 5 | namespace: someTestNamespace 6 | labels: 7 | someTestKey: someTestValue 8 | status: 9 | conditions: 10 | - type: someConditionType 11 | status: "True" 12 | --- 13 | apiVersion: otherGroup.apiVersion/OtherVersion 14 | kind: OtherKind 15 | metadata: 16 | name: otherResource 17 | namespace: someTestNamespace 18 | labels: 19 | someTestKey: someTestValue 20 | status: 21 | conditions: 22 | - type: someConditionType 23 | status: "True" -------------------------------------------------------------------------------- /examples/usage/features/deploy-pod.feature: -------------------------------------------------------------------------------- 1 | Feature: Successfully deploying a Kubernetes Pod 2 | 3 | Background: Valid Credentials 4 | Given valid AWS Credentials 5 | And a Kubernetes cluster 6 | 7 | Scenario: Create Namespace and Pod, validate successfull deployment 8 | Then create resource namespace.yaml 9 | And store current time as pod-creation 10 | And create resource pod.yaml 11 | Then resource pod.yaml should be created 12 | And resource pod.yaml condition Ready should be True 13 | Then all pods in namespace kubedog-example with selector tier=backend have "Hello, Kubedog!" in logs since pod-creation time -------------------------------------------------------------------------------- /examples/templating/kube/features/deploy-pod.feature: -------------------------------------------------------------------------------- 1 | Feature: Successfully deploying a Kubernetes Pod 2 | 3 | Background: Valid Credentials 4 | Given valid AWS Credentials 5 | And a Kubernetes cluster 6 | 7 | Scenario: Create Namespace and Pod, validate successfull deployment 8 | Then create resource namespace.yaml 9 | And store current time as pod-creation 10 | And create resource pod.yaml 11 | Then resource pod.yaml should be created 12 | And resource pod.yaml condition Ready should be True 13 | Then all pods in namespace kubedog-example with selector tier=backend have "Hello, Kubedog!" in logs since pod-creation time -------------------------------------------------------------------------------- /.github/workflows/build-test-coverage.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | pull_request: 9 | jobs: 10 | build: 11 | name: build-test-coverage 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/setup-go@v5 15 | with: 16 | go-version: ^1.21 17 | id: go 18 | - uses: actions/checkout@v4 19 | - run: make build 20 | 21 | - name: Upload coverage reports to Codecov 22 | uses: codecov/codecov-action@v4 23 | with: 24 | file: ./coverage.txt # optional 25 | flags: unittests # optional 26 | fail_ci_if_error: true # optional (default = false) 27 | verbose: true 28 | token: ${{ secrets.CODECOV_TOKEN }} 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubedog 2 | 3 | ![Test Status](https://github.com/keikoproj/kubedog/workflows/Test/badge.svg) [![codecov](https://codecov.io/gh/keikoproj/kubedog/branch/master/graph/badge.svg)](https://codecov.io/gh/keikoproj/kubedog) 4 | 5 | This is a simple wrapper of [Godog]( https://github.com/cucumber/godog) with some predefined steps and their implementations. It targets the [functional testing](https://cucumber.io/docs/bdd/) of [Kubernetes](https://kubernetes.io/) components working in [AWS](https://aws.amazon.com/). 6 | 7 | The library has one purpose – save you from the hassle of implementing steps and hooks with the compromise of using predefined syntax. But of course, you could always redefine the steps using custom syntax and pass the corresponding kubedog methods. 8 | 9 | ## Resources 10 | - [Examples](docs/examples.md) 11 | - [Syntax](docs/syntax.md) 12 | - [GoDocs](https://godoc.org/github.com/keikoproj/kubedog) -------------------------------------------------------------------------------- /pkg/aws/aws_helper_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package aws 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/onsi/gomega" 21 | ) 22 | 23 | func TestGetAccountNumber(t *testing.T) { 24 | g := gomega.NewGomegaWithT(t) 25 | stsClient := &STSMocker{} 26 | 27 | output := getAccountNumber(stsClient) 28 | g.Expect(output).ToNot(gomega.Equal("")) 29 | } 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | ignore: 13 | - dependency-name: "k8s.io*" ## K8s module version updates should be done explicitly 14 | update-types: ["version-update:semver-major", "version-update:semver-minor"] 15 | - dependency-name: "sigs.k8s.io*" ## K8s module version updates should be done explicitly 16 | update-types: ["version-update:semver-major", "version-update:semver-minor"] 17 | - dependency-name: "*" ## Major version updates should be done explicitly 18 | update-types: ["version-update:semver-major"] 19 | 20 | - package-ecosystem: "github-actions" 21 | directory: "/" 22 | schedule: 23 | interval: "monthly" 24 | -------------------------------------------------------------------------------- /pkg/kube/unstructured/test/files/analysis-template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: AnalysisTemplate 3 | metadata: 4 | labels: 5 | env: dev 6 | name: args-test 7 | spec: 8 | args: 9 | - name: namespace 10 | - name: stable-hash 11 | - name: canary-hash 12 | - name: prometheus-port 13 | - name: cpu-utilization-limit-perc 14 | - name: initial-delay 15 | value: 1m 16 | - name: count 17 | value: "10" 18 | - name: interval 19 | value: 60s 20 | - name: failure-limit 21 | value: "1" 22 | - name: inconclusive-limit 23 | value: "1" 24 | metrics: 25 | - count: '{{args.count}}' 26 | failureLimit: '{{args.failure-limit}}' 27 | inconclusiveLimit: '{{args.inconclusive-limit}}' 28 | initialDelay: '{{args.initial-delay}}' 29 | interval: '{{args.interval}}' 30 | name: cpu-utilization 31 | provider: 32 | prometheus: 33 | address: http://prometheus.addon-metricset-ns.svc.cluster.local:{{args.prometheus-port}} 34 | query: (quantile(0.5, quantile_over_time(0.5, namespace_pod_cpu_utilization{namespace="{{args.namespace}}", 35 | pod=~".*-{{args.canary-hash}}-.*"}[11m]))) 36 | successCondition: result[0] <= {{args.cpu-utilization-limit-perc}} 37 | -------------------------------------------------------------------------------- /examples/templating/generic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/keikoproj/kubedog/pkg/generic" 7 | ) 8 | 9 | // Using the generic package to: 10 | // - get template arguments from environment variables 11 | // - generate file from template using the obtained arguments 12 | func main() { 13 | templateArguments := []generic.TemplateArgument{ 14 | { 15 | Key: "Namespace", 16 | EnvironmentVariable: "KUBEDOG_EXAMPLE_NAMESPACE", 17 | Mandatory: false, 18 | Default: "kubedog-example", 19 | }, 20 | { 21 | Key: "Image", 22 | EnvironmentVariable: "KUBEDOG_EXAMPLE_IMAGE", 23 | Mandatory: false, 24 | Default: "busybox:1.28", 25 | }, 26 | { 27 | Key: "Message", 28 | EnvironmentVariable: "KUBEDOG_EXAMPLE_MESSAGE", 29 | Mandatory: false, 30 | Default: "Hello, Kubedog!", 31 | }, 32 | } 33 | args, err := generic.TemplateArgumentsToMap(templateArguments...) 34 | if err != nil { 35 | log.Fatalln(err) 36 | } 37 | templatedFilePath := "files/pod.yaml" 38 | _, err = generic.GenerateFileFromTemplate(templatedFilePath, args) 39 | if err != nil { 40 | log.Fatalln(err) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/usage/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/cucumber/godog" 8 | "github.com/keikoproj/kubedog" 9 | ) 10 | 11 | var k kubedog.Test 12 | 13 | // Required for 'go test' 14 | func TestFeatures(t *testing.T) { 15 | // Setting Godog and running test 16 | status := godog.TestSuite{ 17 | Name: "kubedog-example", 18 | TestSuiteInitializer: InitializeTestSuite, 19 | ScenarioInitializer: InitializeScenario, 20 | Options: &godog.Options{ 21 | Format: "pretty", 22 | Paths: []string{"features"}, 23 | TestingT: t, 24 | }, 25 | }.Run() 26 | if status != 0 { 27 | t.Fatal("non-zero status returned, failed to run feature tests") 28 | } 29 | } 30 | 31 | // Required for godog 32 | func InitializeScenario(ctx *godog.ScenarioContext) { 33 | // Required for Kubedog 34 | k.SetScenario(ctx) 35 | } 36 | 37 | // Required for Godog 38 | func InitializeTestSuite(ctx *godog.TestSuiteContext) { 39 | // Optional: recommended hook 40 | ctx.BeforeSuite(func() { 41 | if err := k.KubeClientSet.DeleteAllTestResources(); err != nil { 42 | log.Printf("Failed deleting the test resources: %v\n\n", err) 43 | } 44 | }) 45 | // Optional: recommended hook 46 | ctx.AfterSuite(func() { 47 | if err := k.KubeClientSet.DeleteAllTestResources(); err != nil { 48 | log.Printf("Failed deleting the test resources: %v\n\n", err) 49 | } 50 | }) 51 | // Required for Kubedog 52 | k.SetTestSuite(ctx) 53 | } 54 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO_LDFLAGS := -ldflags="-s -w" 2 | BINARY := kubedog 3 | COVER_FILE := coverage.txt 4 | 5 | 6 | all: check-syntax build 7 | 8 | check-syntax: generate check-dirty-repo 9 | 10 | generate: download 11 | go generate kubedog.go 12 | 13 | build: test 14 | GOOS=linux GOARCH=amd64 go build $(GO_LDFLAGS) -o ${BINARY} ./ 15 | 16 | test: fmt vet 17 | go test -race -timeout=300s -tags test -coverprofile=${COVER_FILE} ./... 18 | 19 | .PHONY: fmt 20 | fmt: 21 | go fmt ./... 22 | 23 | .PHONY: vet 24 | vet: 25 | go vet ./... 26 | 27 | .PHONY: download 28 | download: 29 | go mod download 30 | 31 | .PHONY: lint 32 | lint: 33 | @echo "golangci-lint" 34 | golangci-lint run ./... 35 | 36 | .PHONY: cover 37 | cover: 38 | @$(MAKE) test 39 | @go tool cover -html=${COVER_FILE} 40 | 41 | .PHONY: check-dirty-repo 42 | check-dirty-repo: 43 | @git diff --quiet HEAD || (\ 44 | echo "Untracked files in git repo: " && \ 45 | git status --short && \ 46 | echo "- If 'docs/syntax.md' is up there, try running 'make generate' and commit the generated documentation" && \ 47 | echo "- If 'go.mod' is up there, try running 'go mod tidy' and commit the changes" && \false) 48 | 49 | .PHONY: clean 50 | clean: 51 | @rm -f ${BINARY} 52 | @rm -f ${COVER_FILE} 53 | 54 | build-examples: build-example-templating-generic build-example-templating-kube build-example-usage check-dirty-repo 55 | 56 | .PHONY: build-example-templating-generic 57 | build-example-templating-generic: 58 | cd examples/templating/generic && \ 59 | go build 60 | 61 | .PHONY: build-example-templating-kube 62 | build-example-templating-kube: 63 | cd examples/templating/kube && \ 64 | go test -c 65 | 66 | .PHONY: build-example-usage 67 | build-example-usage: 68 | cd examples/usage && \ 69 | go test -c -------------------------------------------------------------------------------- /pkg/kube/common/common.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package common 16 | 17 | import ( 18 | "time" 19 | 20 | "github.com/pkg/errors" 21 | "k8s.io/client-go/kubernetes" 22 | ) 23 | 24 | const ( 25 | OperationCreate = "create" 26 | OperationSubmit = "submit" 27 | OperationUpdate = "update" 28 | OperationDelete = "delete" 29 | OperationUpsert = "upsert" 30 | 31 | StateCreated = "created" 32 | StateDeleted = "deleted" 33 | StateUpgraded = "upgraded" 34 | StateReady = "ready" 35 | StateFound = "found" 36 | ) 37 | 38 | type WaiterConfig struct { 39 | tries int 40 | interval time.Duration 41 | } 42 | 43 | func NewWaiterConfig(tries int, interval time.Duration) WaiterConfig { 44 | return WaiterConfig{tries: tries, interval: interval} 45 | } 46 | 47 | func (w WaiterConfig) GetInterval() time.Duration { 48 | defaultWaiterInterval := time.Second * 30 49 | if w.interval > 0 { 50 | return w.interval 51 | } 52 | return defaultWaiterInterval 53 | } 54 | 55 | func (w WaiterConfig) GetTries() int { 56 | defaultWaiterTries := 40 57 | if w.tries > 0 { 58 | return w.tries 59 | } 60 | return defaultWaiterTries 61 | } 62 | 63 | func ValidateClientset(kubeClientset kubernetes.Interface) error { 64 | if kubeClientset == nil { 65 | return errors.Errorf("'k8s.io/client-go/kubernetes.Interface' is nil.") 66 | } 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | All examples are rooted in a directory that holds a standalone [golang module](https://go.dev/ref/mod#introduction) with a [replace directive](https://go.dev/ref/mod#go-mod-file-replace) to consume the local version of `kubedog`. 3 | 4 | 1. Clone the repository 5 | 2. Move to the directory of the example you want to run 6 | - [`cd examples/usage`](../examples/usage) 7 | - [`cd examples/templating/kube`](../examples/templating/kube) 8 | - [`cd examples/templating/generic`](../examples/templating/generic) 9 | 3. Run 10 | - `go test` for [usage](#usage) and [templating/kube](#templatingkube) 11 | - `go run main.go` for [templating/generic](#templatinggeneric) 12 | 13 | To run the examples outside of the repository, you need to remove the replace directive and use a valid `kubedog` version. 14 | 15 | ## usage 16 | 17 | 1. [usage/features/deploy-pod.feature](../examples/usage/features/deploy-pod.feature): is the `*.feature` file defining the behavior to test 18 | 2. [usage/templates](../examples/usage/templates): holds the Kubernetes yaml files, they could be templated 19 | - [namespace.yaml](../examples/usage/templates/namespace.yaml) 20 | - [pod.yaml](../examples/usage/templates/pod.yaml) 21 | 3. [usage/main_test.go](../examples/usage/main_test.go): is the test implementation with the minimum recommended setup for `godog` and `kubedog` 22 | 23 | ## templating/kube 24 | 25 | The [kube](../pkg/kube) package has built-in templating support, this example showcases that. 26 | 27 | 1. [templating/kube/features/deploy-pod.feature](../examples/templating/kube/features/deploy-pod.feature): same as [usage](#usage) 28 | 2. [templating/kube/templates](../examples/templating/kube/templates): similar to [usage](#usage), but the files are **templated** 29 | 3. [templating/kube/main_test.go](../examples/templating/kube/main_test.go): similar to [usage](#usage), but it adds templating implementation 30 | 31 | ## templating/generic 32 | 33 | The [generic](../pkg/generic/template.go) package offers general purpose file templating, this example showcases that. 34 | 35 | 1. [templating/generic/files](../examples/templating/generic/files): templated files 36 | 2. [templating/generic/main.go](../examples/templating/generic/main.go): templating implementation 37 | 38 | -------------------------------------------------------------------------------- /examples/templating/kube/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/cucumber/godog" 8 | "github.com/keikoproj/kubedog" 9 | "github.com/keikoproj/kubedog/pkg/generic" 10 | ) 11 | 12 | var k kubedog.Test 13 | 14 | func TestFeatures(t *testing.T) { 15 | status := godog.TestSuite{ 16 | Name: "kubedog-example", 17 | TestSuiteInitializer: InitializeTestSuite, 18 | ScenarioInitializer: InitializeScenario, 19 | Options: &godog.Options{ 20 | Format: "pretty", 21 | Paths: []string{"features"}, 22 | TestingT: t, 23 | }, 24 | }.Run() 25 | if status != 0 { 26 | t.Fatal("non-zero status returned, failed to run feature tests") 27 | } 28 | } 29 | 30 | func InitializeScenario(ctx *godog.ScenarioContext) { 31 | // Required to populate templated Kubernetes yaml files 32 | k.KubeClientSet.SetTemplateArguments(getTemplateArguments()) 33 | k.SetScenario(ctx) 34 | } 35 | 36 | func InitializeTestSuite(ctx *godog.TestSuiteContext) { 37 | ctx.BeforeSuite(func() { 38 | if err := k.KubeClientSet.DeleteAllTestResources(); err != nil { 39 | log.Printf("Failed deleting the test resources: %v\n\n", err) 40 | } 41 | }) 42 | ctx.AfterSuite(func() { 43 | if err := k.KubeClientSet.DeleteAllTestResources(); err != nil { 44 | log.Printf("Failed deleting the test resources: %v\n\n", err) 45 | } 46 | }) 47 | k.SetTestSuite(ctx) 48 | } 49 | 50 | // Using the generic package to get template arguments from environment variables 51 | func getTemplateArguments() map[string]string { 52 | templateArguments := []generic.TemplateArgument{ 53 | { 54 | Key: "Namespace", 55 | EnvironmentVariable: "KUBEDOG_EXAMPLE_NAMESPACE", 56 | Mandatory: false, 57 | Default: "kubedog-example", 58 | }, 59 | { 60 | Key: "Image", 61 | EnvironmentVariable: "KUBEDOG_EXAMPLE_IMAGE", 62 | Mandatory: false, 63 | Default: "busybox:1.28", 64 | }, 65 | { 66 | Key: "Message", 67 | EnvironmentVariable: "KUBEDOG_EXAMPLE_MESSAGE", 68 | Mandatory: false, 69 | Default: "Hello, Kubedog!", 70 | }, 71 | } 72 | args, err := generic.TemplateArgumentsToMap(templateArguments...) 73 | if err != nil { 74 | panic(err) 75 | } 76 | return args 77 | } 78 | -------------------------------------------------------------------------------- /generate/syntax/replace/replace.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package replace 16 | 17 | import ( 18 | "bytes" 19 | "log" 20 | "regexp" 21 | "strings" 22 | ) 23 | 24 | const regExp_CharsWithinBrackets = "([^(]*)" 25 | 26 | type Replacement struct { 27 | Replacee string 28 | Replacer string 29 | } 30 | 31 | func (r Replacement) Replace(src string) string { 32 | return strings.ReplaceAll(src, r.Replacee, r.Replacer) 33 | } 34 | 35 | type Replacements []Replacement 36 | 37 | func (rs Replacements) Replace(src string) string { 38 | new := src 39 | for _, r := range rs { 40 | new = r.Replace(new) 41 | } 42 | return new 43 | } 44 | 45 | type BracketsReplacement struct { 46 | Opening Replacement 47 | Closing Replacement 48 | } 49 | 50 | func (br BracketsReplacement) Replace(src string) string { 51 | re, err := regexp.Compile(br.getRegExp()) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | new := re.ReplaceAllFunc([]byte(src), br.replaceSingle) 56 | return string(new) 57 | } 58 | 59 | func (br BracketsReplacement) replaceSingle(src []byte) []byte { 60 | s := string(src) 61 | s = br.Opening.Replace(s) 62 | s = br.Closing.Replace(s) 63 | return []byte(s) 64 | } 65 | 66 | func (br BracketsReplacement) getRegExp() string { 67 | return escapeEveryCharacter(br.Opening.Replacee) + 68 | regExp_CharsWithinBrackets + 69 | escapeEveryCharacter(br.Closing.Replacee) 70 | } 71 | 72 | func escapeEveryCharacter(s string) string { 73 | var buffer bytes.Buffer 74 | for _, c := range s { 75 | buffer.WriteString(`\`) 76 | buffer.WriteRune(c) 77 | } 78 | return buffer.String() 79 | } 80 | 81 | type BracketsReplacements []BracketsReplacement 82 | 83 | func (brs BracketsReplacements) Replace(src string) string { 84 | new := src 85 | for _, br := range brs { 86 | new = br.Replace(new) 87 | } 88 | return new 89 | } 90 | -------------------------------------------------------------------------------- /internal/util/util_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package util 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | var ( 22 | sampleResource = map[string]any{ 23 | "spec": map[string]any{ 24 | "template": map[string]any{ 25 | "containers": []map[string]any{ 26 | { 27 | "name": "someContainer", 28 | "image": "someImage", 29 | "version": 1.4, 30 | "ports": []map[string]any{ 31 | {"containerPort": 8080}, 32 | {"containerPort": 8940}, 33 | }, 34 | }, 35 | }, 36 | }, 37 | }, 38 | } 39 | ) 40 | 41 | func TestExtractField(t *testing.T) { 42 | type args struct { 43 | data any 44 | path []string 45 | expectedValue any 46 | } 47 | tests := []struct { 48 | name string 49 | args args 50 | wantErr bool 51 | }{ 52 | { 53 | name: "Positive Test", 54 | args: args{ 55 | data: sampleResource, 56 | path: []string{"spec", "template", "containers[0]", "name"}, 57 | expectedValue: "someContainer", 58 | }, 59 | }, 60 | { 61 | name: "Positive Test - multiple array", 62 | args: args{ 63 | data: sampleResource, 64 | path: []string{"spec", "template", "containers[0]", "ports[1]", "containerPort"}, 65 | expectedValue: 8940, 66 | }, 67 | }, 68 | { 69 | name: "Negative Test", 70 | args: args{ 71 | data: sampleResource, 72 | path: []string{"spec", "path", "doesnt", "exist"}, 73 | expectedValue: nil, 74 | }, 75 | wantErr: true, 76 | }, 77 | } 78 | for _, tt := range tests { 79 | t.Run(tt.name, func(t *testing.T) { 80 | if extractedValue, err := ExtractField(tt.args.data, tt.args.path); (err != nil) != tt.wantErr || extractedValue != tt.args.expectedValue { 81 | t.Errorf("ExtractField() error = %v, wantErr %v", err, tt.wantErr) 82 | } 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pkg/kube/kube_helper.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package kube 16 | 17 | import ( 18 | "fmt" 19 | "path/filepath" 20 | "time" 21 | 22 | "github.com/keikoproj/kubedog/internal/util" 23 | "github.com/keikoproj/kubedog/pkg/kube/common" 24 | "github.com/pkg/errors" 25 | "k8s.io/apimachinery/pkg/util/wait" 26 | "k8s.io/client-go/discovery" 27 | ) 28 | 29 | type configuration struct { 30 | filesPath string 31 | templateArguments interface{} 32 | waiterInterval time.Duration 33 | waiterTries int 34 | } 35 | 36 | func (kc *ClientSet) GetTimestamp(timestampName string) (time.Time, error) { 37 | commonErrorMessage := fmt.Sprintf("failed getting timestamp '%s'", timestampName) 38 | if kc.timestamps == nil { 39 | return time.Time{}, errors.Errorf("%s: 'ClientSet.Timestamps' is nil", commonErrorMessage) 40 | } 41 | timestamp, ok := kc.timestamps[timestampName] 42 | if !ok { 43 | return time.Time{}, errors.Errorf("%s: Timestamp not found", commonErrorMessage) 44 | } 45 | return timestamp, nil 46 | } 47 | 48 | func (kc *ClientSet) getResourcePath(resourceFileName string) string { 49 | templatesPath := kc.getTemplatesPath() 50 | return filepath.Join(templatesPath, resourceFileName) 51 | } 52 | 53 | func (kc *ClientSet) getTemplatesPath() string { 54 | defaultFilePath := "templates" 55 | if kc.config.filesPath != "" { 56 | return kc.config.filesPath 57 | } 58 | return defaultFilePath 59 | } 60 | 61 | func (kc *ClientSet) getWaiterInterval() time.Duration { 62 | defaultWaiterInterval := time.Second * 30 63 | if kc.config.waiterInterval > 0 { 64 | return kc.config.waiterInterval 65 | } 66 | return defaultWaiterInterval 67 | } 68 | 69 | func (kc *ClientSet) getWaiterTries() int { 70 | defaultWaiterTries := 40 71 | if kc.config.waiterTries > 0 { 72 | return kc.config.waiterTries 73 | } 74 | return defaultWaiterTries 75 | } 76 | 77 | func (kc *ClientSet) getWaiterConfig() common.WaiterConfig { 78 | return common.NewWaiterConfig(kc.getWaiterTries(), kc.getWaiterInterval()) 79 | } 80 | 81 | func (kc *ClientSet) getExpBackoff() wait.Backoff { 82 | return util.GetExpBackoff(kc.getWaiterTries()) 83 | } 84 | 85 | func (kc *ClientSet) getDiscoveryClient() discovery.DiscoveryInterface { 86 | if kc.KubeInterface != nil { 87 | return kc.KubeInterface.Discovery() 88 | } 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/keikoproj/kubedog 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go v1.34.0 7 | github.com/cucumber/godog v0.14.1 8 | github.com/onsi/gomega v1.30.0 9 | github.com/pkg/errors v0.9.1 10 | github.com/sirupsen/logrus v1.9.3 11 | github.com/tsenart/vegeta/v12 v12.11.1 12 | golang.org/x/text v0.14.0 13 | k8s.io/api v0.28.12 14 | k8s.io/apimachinery v0.28.12 15 | k8s.io/client-go v0.28.12 16 | sigs.k8s.io/yaml v1.3.0 17 | ) 18 | 19 | require ( 20 | github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect 21 | github.com/cucumber/messages/go/v21 v21.0.1 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/emicklei/go-restful/v3 v3.9.0 // indirect 24 | github.com/evanphx/json-patch v4.12.0+incompatible // indirect 25 | github.com/go-logr/logr v1.2.4 // indirect 26 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 27 | github.com/go-openapi/jsonreference v0.20.2 // indirect 28 | github.com/go-openapi/swag v0.22.3 // indirect 29 | github.com/gofrs/uuid v4.3.1+incompatible // indirect 30 | github.com/gogo/protobuf v1.3.2 // indirect 31 | github.com/golang/protobuf v1.5.4 // indirect 32 | github.com/google/gnostic-models v0.6.8 // indirect 33 | github.com/google/go-cmp v0.6.0 // indirect 34 | github.com/google/gofuzz v1.2.0 // indirect 35 | github.com/google/uuid v1.3.0 // indirect 36 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 37 | github.com/hashicorp/go-memdb v1.3.4 // indirect 38 | github.com/hashicorp/golang-lru v0.5.4 // indirect 39 | github.com/imdario/mergo v0.3.10 // indirect 40 | github.com/influxdata/tdigest v0.0.1 // indirect 41 | github.com/jmespath/go-jmespath v0.3.0 // indirect 42 | github.com/josharian/intern v1.0.0 // indirect 43 | github.com/json-iterator/go v1.1.12 // indirect 44 | github.com/mailru/easyjson v0.7.7 // indirect 45 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 46 | github.com/modern-go/reflect2 v1.0.2 // indirect 47 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 48 | github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect 49 | github.com/spf13/pflag v1.0.5 // indirect 50 | golang.org/x/net v0.23.0 // indirect 51 | golang.org/x/oauth2 v0.8.0 // indirect 52 | golang.org/x/sync v0.3.0 // indirect 53 | golang.org/x/sys v0.18.0 // indirect 54 | golang.org/x/term v0.18.0 // indirect 55 | golang.org/x/time v0.3.0 // indirect 56 | google.golang.org/appengine v1.6.7 // indirect 57 | google.golang.org/protobuf v1.33.0 // indirect 58 | gopkg.in/inf.v0 v0.9.1 // indirect 59 | gopkg.in/yaml.v2 v2.4.0 // indirect 60 | gopkg.in/yaml.v3 v3.0.1 // indirect 61 | k8s.io/klog/v2 v2.100.1 // indirect 62 | k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect 63 | k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect 64 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 65 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 66 | ) 67 | -------------------------------------------------------------------------------- /pkg/generic/generic.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package generic 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "os/exec" 21 | "strings" 22 | "time" 23 | 24 | "github.com/keikoproj/kubedog/internal/util" 25 | log "github.com/sirupsen/logrus" 26 | ) 27 | 28 | func WaitFor(duration int, durationUnits string) error { 29 | switch durationUnits { 30 | case util.DurationMinutes: 31 | increment := 1 32 | d := increment 33 | for d <= duration { 34 | time.Sleep(time.Duration(increment) * time.Minute) 35 | log.Infof("waited '%d' out of '%d' '%s'", d, duration, durationUnits) 36 | d += increment 37 | } 38 | return nil 39 | case util.DurationSeconds: 40 | increment := 30 41 | d := increment 42 | for d <= duration { 43 | time.Sleep(time.Duration(increment) * time.Second) 44 | log.Infof("waited '%d' out of '%d' '%s'", d, duration, durationUnits) 45 | d += increment 46 | } 47 | lastIncrement := duration - d + increment 48 | if lastIncrement > 0 { 49 | time.Sleep(time.Duration(lastIncrement) * time.Second) 50 | d += lastIncrement - increment 51 | log.Infof("waited '%d' out of '%d' '%s'", d, duration, durationUnits) 52 | } 53 | return nil 54 | default: 55 | return fmt.Errorf("unsupported duration units: '%s'", durationUnits) 56 | } 57 | } 58 | 59 | func CommandExists(command string) error { 60 | if _, err := exec.LookPath(command); err != nil { 61 | return err 62 | } 63 | 64 | return nil 65 | } 66 | 67 | func RunCommand(command string, args, successOrFail string) error { 68 | // split to support args being passed from .feature file. 69 | // slice param type not supported by godog. 70 | splitArgs := strings.Split(args, " ") 71 | toRun := exec.Command(command, splitArgs...) 72 | 73 | var stdout bytes.Buffer 74 | var stderr bytes.Buffer 75 | toRun.Stdout = &stdout 76 | toRun.Stderr = &stderr 77 | 78 | cmdStr := toRun.String() 79 | log.Infof("Running command: %s", cmdStr) 80 | err := toRun.Run() 81 | if len(stdout.String()) > 0 { 82 | log.Infof("Logging stdout from command: %s", stdout.String()) 83 | } 84 | 85 | if successOrFail == "succeeds" && err != nil { 86 | return fmt.Errorf("command '%s' did not succeed. error: '%v'. stderr: '%s'", cmdStr, err, stderr.String()) 87 | } 88 | 89 | if successOrFail == "fails" && err == nil { 90 | return fmt.Errorf("command '%s' succeeded but expected to fail: '%s'", cmdStr, stdout.String()) 91 | } 92 | 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /pkg/generic/generic_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package generic 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestCommandExists(t *testing.T) { 22 | type args struct { 23 | command string 24 | } 25 | tests := []struct { 26 | name string 27 | args args 28 | wantErr bool 29 | }{ 30 | { 31 | name: "Command SHOULD exist", 32 | args: args{ 33 | command: "echo", 34 | }, 35 | wantErr: false, 36 | }, 37 | { 38 | name: "Command SHOULD NOT exist", 39 | args: args{ 40 | command: "doesnotexist", 41 | }, 42 | wantErr: true, 43 | }, 44 | } 45 | for _, tt := range tests { 46 | t.Run(tt.name, func(t *testing.T) { 47 | if err := CommandExists(tt.args.command); (err != nil) != tt.wantErr { 48 | t.Errorf("CommandExists() error = %v, wantErr %v", err, tt.wantErr) 49 | } 50 | }) 51 | } 52 | } 53 | 54 | func TestRunCommand(t *testing.T) { 55 | type args struct { 56 | command string 57 | args string 58 | successOrFail string 59 | } 60 | tests := []struct { 61 | name string 62 | args args 63 | wantErr bool 64 | }{ 65 | { 66 | name: "Command FAILS and is EXPECTED TO", 67 | args: args{ 68 | command: "doesnotexist", 69 | args: "not real", 70 | successOrFail: "fails", 71 | }, 72 | wantErr: false, 73 | }, 74 | { 75 | name: "Command FAILS and is NOT EXPECTED TO", 76 | args: args{ 77 | command: "doesnotexist", 78 | args: "not real", 79 | successOrFail: "succeeds", 80 | }, 81 | wantErr: true, 82 | }, 83 | { 84 | name: "Command SUCCEEDS and is EXPECTED TO", 85 | args: args{ 86 | command: "echo", 87 | args: "I want to succeed", 88 | successOrFail: "succeeds", 89 | }, 90 | wantErr: false, 91 | }, 92 | { 93 | name: "Command SUCCEEDS and is NOT EXPECTED TO", 94 | args: args{ 95 | command: "echo", 96 | args: "what why would I succeed?", 97 | successOrFail: "fails", 98 | }, 99 | wantErr: true, 100 | }, 101 | } 102 | for _, tt := range tests { 103 | t.Run(tt.name, func(t *testing.T) { 104 | if err := RunCommand(tt.args.command, tt.args.args, tt.args.successOrFail); (err != nil) != tt.wantErr { 105 | t.Errorf("RunCommand() error = %v, wantErr %v", err, tt.wantErr) 106 | } 107 | }) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /examples/usage/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/keikoproj/kubedog/examples/usage 2 | 3 | go 1.21 4 | 5 | replace github.com/keikoproj/kubedog => ../../ 6 | 7 | require ( 8 | github.com/cucumber/godog v0.14.1 9 | github.com/keikoproj/kubedog v1.2.3 10 | ) 11 | 12 | require ( 13 | github.com/aws/aws-sdk-go v1.34.0 // indirect 14 | github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect 15 | github.com/cucumber/messages/go/v21 v21.0.1 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/emicklei/go-restful/v3 v3.9.0 // indirect 18 | github.com/go-logr/logr v1.2.4 // indirect 19 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 20 | github.com/go-openapi/jsonreference v0.20.2 // indirect 21 | github.com/go-openapi/swag v0.22.3 // indirect 22 | github.com/gofrs/uuid v4.3.1+incompatible // indirect 23 | github.com/gogo/protobuf v1.3.2 // indirect 24 | github.com/golang/protobuf v1.5.4 // indirect 25 | github.com/google/gnostic-models v0.6.8 // indirect 26 | github.com/google/go-cmp v0.6.0 // indirect 27 | github.com/google/gofuzz v1.2.0 // indirect 28 | github.com/google/uuid v1.3.0 // indirect 29 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 30 | github.com/hashicorp/go-memdb v1.3.4 // indirect 31 | github.com/hashicorp/golang-lru v0.5.4 // indirect 32 | github.com/imdario/mergo v0.3.10 // indirect 33 | github.com/influxdata/tdigest v0.0.1 // indirect 34 | github.com/jmespath/go-jmespath v0.3.0 // indirect 35 | github.com/josharian/intern v1.0.0 // indirect 36 | github.com/json-iterator/go v1.1.12 // indirect 37 | github.com/mailru/easyjson v0.7.7 // indirect 38 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 39 | github.com/modern-go/reflect2 v1.0.2 // indirect 40 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 41 | github.com/pkg/errors v0.9.1 // indirect 42 | github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect 43 | github.com/sirupsen/logrus v1.9.3 // indirect 44 | github.com/spf13/pflag v1.0.5 // indirect 45 | github.com/tsenart/vegeta/v12 v12.11.1 // indirect 46 | golang.org/x/net v0.23.0 // indirect 47 | golang.org/x/oauth2 v0.8.0 // indirect 48 | golang.org/x/sync v0.3.0 // indirect 49 | golang.org/x/sys v0.18.0 // indirect 50 | golang.org/x/term v0.18.0 // indirect 51 | golang.org/x/text v0.14.0 // indirect 52 | golang.org/x/time v0.3.0 // indirect 53 | google.golang.org/appengine v1.6.7 // indirect 54 | google.golang.org/protobuf v1.33.0 // indirect 55 | gopkg.in/inf.v0 v0.9.1 // indirect 56 | gopkg.in/yaml.v2 v2.4.0 // indirect 57 | gopkg.in/yaml.v3 v3.0.1 // indirect 58 | k8s.io/api v0.28.12 // indirect 59 | k8s.io/apimachinery v0.28.12 // indirect 60 | k8s.io/client-go v0.28.12 // indirect 61 | k8s.io/klog/v2 v2.100.1 // indirect 62 | k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect 63 | k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect 64 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 65 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 66 | sigs.k8s.io/yaml v1.3.0 // indirect 67 | ) 68 | -------------------------------------------------------------------------------- /examples/templating/kube/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/keikoproj/kubedog/examples/templating/kube 2 | 3 | go 1.21 4 | 5 | replace github.com/keikoproj/kubedog => ../../../ 6 | 7 | require ( 8 | github.com/cucumber/godog v0.14.1 9 | github.com/keikoproj/kubedog v1.2.3 10 | ) 11 | 12 | require ( 13 | github.com/aws/aws-sdk-go v1.34.0 // indirect 14 | github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect 15 | github.com/cucumber/messages/go/v21 v21.0.1 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/emicklei/go-restful/v3 v3.9.0 // indirect 18 | github.com/go-logr/logr v1.2.4 // indirect 19 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 20 | github.com/go-openapi/jsonreference v0.20.2 // indirect 21 | github.com/go-openapi/swag v0.22.3 // indirect 22 | github.com/gofrs/uuid v4.3.1+incompatible // indirect 23 | github.com/gogo/protobuf v1.3.2 // indirect 24 | github.com/golang/protobuf v1.5.4 // indirect 25 | github.com/google/gnostic-models v0.6.8 // indirect 26 | github.com/google/go-cmp v0.6.0 // indirect 27 | github.com/google/gofuzz v1.2.0 // indirect 28 | github.com/google/uuid v1.3.0 // indirect 29 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 30 | github.com/hashicorp/go-memdb v1.3.4 // indirect 31 | github.com/hashicorp/golang-lru v0.5.4 // indirect 32 | github.com/imdario/mergo v0.3.10 // indirect 33 | github.com/influxdata/tdigest v0.0.1 // indirect 34 | github.com/jmespath/go-jmespath v0.3.0 // indirect 35 | github.com/josharian/intern v1.0.0 // indirect 36 | github.com/json-iterator/go v1.1.12 // indirect 37 | github.com/mailru/easyjson v0.7.7 // indirect 38 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 39 | github.com/modern-go/reflect2 v1.0.2 // indirect 40 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 41 | github.com/pkg/errors v0.9.1 // indirect 42 | github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect 43 | github.com/sirupsen/logrus v1.9.3 // indirect 44 | github.com/spf13/pflag v1.0.5 // indirect 45 | github.com/tsenart/vegeta/v12 v12.11.1 // indirect 46 | golang.org/x/net v0.23.0 // indirect 47 | golang.org/x/oauth2 v0.8.0 // indirect 48 | golang.org/x/sync v0.3.0 // indirect 49 | golang.org/x/sys v0.18.0 // indirect 50 | golang.org/x/term v0.18.0 // indirect 51 | golang.org/x/text v0.14.0 // indirect 52 | golang.org/x/time v0.3.0 // indirect 53 | google.golang.org/appengine v1.6.7 // indirect 54 | google.golang.org/protobuf v1.33.0 // indirect 55 | gopkg.in/inf.v0 v0.9.1 // indirect 56 | gopkg.in/yaml.v2 v2.4.0 // indirect 57 | gopkg.in/yaml.v3 v3.0.1 // indirect 58 | k8s.io/api v0.28.12 // indirect 59 | k8s.io/apimachinery v0.28.12 // indirect 60 | k8s.io/client-go v0.28.12 // indirect 61 | k8s.io/klog/v2 v2.100.1 // indirect 62 | k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect 63 | k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect 64 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 65 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 66 | sigs.k8s.io/yaml v1.3.0 // indirect 67 | ) 68 | -------------------------------------------------------------------------------- /pkg/aws/iam/iam_helper_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package iam 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | "github.com/aws/aws-sdk-go/aws" 22 | "github.com/aws/aws-sdk-go/service/iam" 23 | "github.com/onsi/gomega" 24 | "sigs.k8s.io/yaml" 25 | ) 26 | 27 | func TestCreateIAMRole(t *testing.T) { 28 | g := gomega.NewGomegaWithT(t) 29 | 30 | iamClient := &FakeIAMClient{} 31 | policyYAML := `| 32 | Version: '2012-10-17' 33 | Statement: 34 | - Effect: Allow 35 | Action: 36 | - autoscaling:TerminateInstanceInAutoScalingGroup 37 | - autoscaling:DescribeAutoScalingGroups 38 | - ec2:DescribeTags 39 | - ec2:DescribeInstances 40 | Resource: 41 | - "*"` 42 | policyJSON, err := yaml.YAMLToJSON([]byte(policyYAML)) 43 | g.Expect(err).To(gomega.BeNil()) 44 | 45 | output, err := createIAMRole("arn:aws:iam::aws:policy/test-role", "Description", policyJSON, iamClient) 46 | g.Expect(err).To(gomega.BeNil()) 47 | g.Expect(output).ToNot(gomega.BeNil()) 48 | } 49 | 50 | func TestDeleteManagedPolicyVersion(t *testing.T) { 51 | g := gomega.NewGomegaWithT(t) 52 | 53 | iamClient := &FakeIAMClient{} 54 | 55 | err := deleteManagedPolicyVersion("arn:aws:iam::aws:policy/test-role", "testid", iamClient) 56 | g.Expect(err).To(gomega.BeNil()) 57 | } 58 | 59 | func TestCreateManagedPolicy(t *testing.T) { 60 | g := gomega.NewGomegaWithT(t) 61 | 62 | iamClient := &FakeIAMClient{} 63 | policyYAML := `| 64 | Version: '2012-10-17' 65 | Statement: 66 | - Effect: Allow 67 | Action: 68 | - autoscaling:TerminateInstanceInAutoScalingGroup 69 | - autoscaling:DescribeAutoScalingGroups 70 | - ec2:DescribeTags 71 | - ec2:DescribeInstances 72 | Resource: 73 | - "*"` 74 | policyJSON, err := yaml.YAMLToJSON([]byte(policyYAML)) 75 | g.Expect(err).To(gomega.BeNil()) 76 | 77 | _, err = createManagedPolicy("arn:aws:iam::aws:policy/test-role", "Description", policyJSON, iamClient) 78 | g.Expect(err).To(gomega.BeNil()) 79 | } 80 | 81 | func TestGetOldestVersionID(t *testing.T) { 82 | g := gomega.NewGomegaWithT(t) 83 | 84 | var versions []*iam.PolicyVersion 85 | versions = append(versions, &iam.PolicyVersion{VersionId: aws.String("v1"), CreateDate: aws.Time(time.Now().Add(5 * time.Minute))}) 86 | versions = append(versions, &iam.PolicyVersion{VersionId: aws.String("v2"), CreateDate: aws.Time(time.Now())}) 87 | versions = append(versions, &iam.PolicyVersion{VersionId: aws.String("v3"), CreateDate: aws.Time(time.Now().Add(10 * time.Minute))}) 88 | versions = append(versions, &iam.PolicyVersion{VersionId: aws.String("v4"), CreateDate: aws.Time(time.Now())}) 89 | 90 | oldestId := getOldestVersionID(versions) 91 | g.Expect(oldestId).To(gomega.Equal("v2")) 92 | } 93 | -------------------------------------------------------------------------------- /examples/templating/generic/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 5 | github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= 6 | github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 7 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 8 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 9 | github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= 10 | github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= 11 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 12 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 15 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 16 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 17 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 18 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 19 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 20 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 21 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 22 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 23 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 24 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 25 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 26 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 27 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 28 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 29 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 30 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 31 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 32 | k8s.io/apimachinery v0.28.12 h1:VepMEVOi9o7L/4wMAXJq+3BK9tqBIeerTB+HSOTKeo0= 33 | k8s.io/apimachinery v0.28.12/go.mod h1:zUG757HaKs6Dc3iGtKjzIpBfqTM4yiRsEe3/E7NX15o= 34 | k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= 35 | k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= 36 | k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= 37 | k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 38 | -------------------------------------------------------------------------------- /pkg/generic/template.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package generic 16 | 17 | import ( 18 | "os" 19 | "path/filepath" 20 | "text/template" 21 | 22 | "github.com/pkg/errors" 23 | log "github.com/sirupsen/logrus" 24 | ) 25 | 26 | type TemplateArgument struct { 27 | Key string 28 | EnvironmentVariable string 29 | Default string 30 | Mandatory bool 31 | } 32 | 33 | // GetValue returns the value of the Environment Variable defined by 'TemplateArgument.EnvironmentVariable'. 34 | // If 'TemplateArgument.EnvironmentVariable' is empty or the ENV. VAR. it defines is unset, 'TemplateArgument.Default' is returned. 35 | // That is, if 'TemplateArgument.Mandatory' is not 'true', in which case, an error is returned. 36 | func (ta TemplateArgument) GetValue() (string, error) { 37 | if ta.Key == "" { 38 | return "", errors.Errorf("'TemplateArgument.Key' can not be empty.") 39 | } else if value, ok := os.LookupEnv(ta.EnvironmentVariable); ok { 40 | return value, nil 41 | } else if ta.Mandatory { 42 | return "", errors.Errorf("'TemplateArgument.Mandatory'='true' but the Environment Variable '%s' defined by 'TemplateArgument.EnvironmentVariable' is not set", ta.EnvironmentVariable) 43 | } else { 44 | return ta.Default, nil 45 | } 46 | } 47 | 48 | // TemplateArgumentsToMap uses the elements of 'templateArguments' to populate the key:value pairs of the returned map. 49 | // The key is the '.Key' variable of the corresponding element, and the value is the string returned by the 'GetValue' method of said element. 50 | func TemplateArgumentsToMap(templateArguments ...TemplateArgument) (map[string]string, error) { 51 | args := map[string]string{} 52 | for i, ta := range templateArguments { 53 | value, err := ta.GetValue() 54 | if err != nil { 55 | return args, errors.Errorf("'templateArguments[%d].GetValue()' failed. 'templateArguments[%d]'='%v'. error: '%v'", i, i, ta, err) 56 | } 57 | args[ta.Key] = value 58 | } 59 | return args, nil 60 | } 61 | 62 | // GenerateFileFromTemplate applies the template defined in templatedFilePath to templateArgs. 63 | // The generated file will be named 'generated_' and it will be created in the same directory of the template. 64 | func GenerateFileFromTemplate(templatedFilePath string, templateArgs interface{}) (string, error) { 65 | t, err := template.ParseFiles(templatedFilePath) 66 | if err != nil { 67 | return "", errors.Errorf("Error parsing templated file '%s': %v", templatedFilePath, err) 68 | } 69 | 70 | templatedFileDir := filepath.Dir(templatedFilePath) 71 | templatedFileName := filepath.Base(templatedFilePath) 72 | generatedFilePath := filepath.Join(templatedFileDir, "generated_"+templatedFileName) 73 | f, err := os.Create(generatedFilePath) 74 | if err != nil { 75 | return "", errors.Errorf("Error creating generated file '%s': %v", generatedFilePath, err) 76 | } 77 | defer f.Close() 78 | 79 | err = t.Execute(f, templateArgs) 80 | if err != nil { 81 | return "", errors.Errorf("Error executing template '%v' against '%s': %v", templateArgs, templatedFilePath, err) 82 | } 83 | generated, err := os.ReadFile(generatedFilePath) 84 | if err != nil { 85 | return "", errors.Errorf("Error reading generated file '%s': %v", generatedFilePath, err) 86 | } 87 | 88 | log.Infof("Generated file '%s': \n %s", generatedFilePath, string(generated)) 89 | 90 | return generatedFilePath, nil 91 | } 92 | -------------------------------------------------------------------------------- /pkg/kube/pod/pod_helper.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package pod 16 | 17 | import ( 18 | "bufio" 19 | "context" 20 | "strings" 21 | "time" 22 | 23 | "github.com/keikoproj/kubedog/internal/util" 24 | "github.com/keikoproj/kubedog/pkg/kube/common" 25 | "github.com/pkg/errors" 26 | log "github.com/sirupsen/logrus" 27 | corev1 "k8s.io/api/core/v1" 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 29 | "k8s.io/client-go/kubernetes" 30 | ) 31 | 32 | func GetPodListWithLabelSelector(kubeClientset kubernetes.Interface, namespace, labelSelector string) (*corev1.PodList, error) { 33 | return GetPodListWithLabelSelectorAndFieldSelector(kubeClientset, namespace, labelSelector, "") 34 | } 35 | 36 | func GetPodListWithLabelSelectorAndFieldSelector(kubeClientset kubernetes.Interface, namespace, labelSelector, fieldSelector string) (*corev1.PodList, error) { 37 | if err := common.ValidateClientset(kubeClientset); err != nil { 38 | return nil, err 39 | } 40 | 41 | pods, err := util.RetryOnError(&util.DefaultRetry, util.IsRetriable, func() (interface{}, error) { 42 | return kubeClientset.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{LabelSelector: labelSelector, FieldSelector: fieldSelector}) 43 | }) 44 | if err != nil { 45 | return nil, errors.Wrap(err, "failed to list pods") 46 | } 47 | 48 | return pods.(*corev1.PodList), nil 49 | } 50 | 51 | func DeletePodListWithLabelSelector(kubeClientset kubernetes.Interface, namespace, labelSelector string) error { 52 | return DeletePodListWithLabelSelectorAndFieldSelector(kubeClientset, namespace, labelSelector, "") 53 | } 54 | 55 | func DeletePodListWithLabelSelectorAndFieldSelector(kubeClientset kubernetes.Interface, namespace, labelSelector, fieldSelector string) error { 56 | if err := common.ValidateClientset(kubeClientset); err != nil { 57 | return err 58 | } 59 | 60 | err := kubeClientset.CoreV1().Pods(namespace).DeleteCollection(context.Background(), metav1.DeleteOptions{}, metav1.ListOptions{ 61 | LabelSelector: labelSelector, 62 | FieldSelector: fieldSelector, 63 | }) 64 | if err != nil { 65 | return errors.Wrapf(err, "failed to delete pods with label selector %s and field selector %s in namespace %s", labelSelector, fieldSelector, namespace) 66 | } 67 | return nil 68 | } 69 | 70 | func countStringInPodLogs(kubeClientset kubernetes.Interface, pod corev1.Pod, since time.Time, stringsToFind ...string) (int, error) { 71 | foundCount := 0 72 | if err := common.ValidateClientset(kubeClientset); err != nil { 73 | return foundCount, err 74 | } 75 | var sinceTime metav1.Time = metav1.NewTime(since) 76 | for _, container := range pod.Spec.Containers { 77 | podLogOpts := corev1.PodLogOptions{ 78 | SinceTime: &sinceTime, 79 | Container: container.Name, 80 | } 81 | 82 | req := kubeClientset.CoreV1().Pods(pod.Namespace).GetLogs(pod.Name, &podLogOpts) 83 | podLogs, err := req.Stream(context.Background()) 84 | if err != nil { 85 | return 0, errors.Errorf("Error in opening stream for pod '%s', container '%s' : '%s'", pod.Name, container.Name, string(err.Error())) 86 | } 87 | 88 | scanner := bufio.NewScanner(podLogs) 89 | for scanner.Scan() { 90 | line := scanner.Text() 91 | for _, stringToFind := range stringsToFind { 92 | if strings.Contains(line, stringToFind) { 93 | foundCount += 1 94 | log.Infof("Found string '%s' in line '%s' in container '%s' of pod '%s'", stringToFind, line, container.Name, pod.Name) 95 | } 96 | } 97 | } 98 | podLogs.Close() 99 | } 100 | return foundCount, nil 101 | } 102 | -------------------------------------------------------------------------------- /pkg/aws/aws_helper.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package aws 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | 21 | "github.com/aws/aws-sdk-go/aws" 22 | "github.com/aws/aws-sdk-go/service/eks" 23 | "github.com/aws/aws-sdk-go/service/route53" 24 | "github.com/aws/aws-sdk-go/service/sts" 25 | "github.com/aws/aws-sdk-go/service/sts/stsiface" 26 | "github.com/pkg/errors" 27 | log "github.com/sirupsen/logrus" 28 | ) 29 | 30 | const ( 31 | clusterNameEnvironmentVariable = "CLUSTER_NAME" 32 | ) 33 | 34 | func (c *ClientSet) GetEksVpc() (string, error) { 35 | clusterName, err := getClusterName() 36 | if err != nil { 37 | return "", err 38 | } 39 | input := &eks.DescribeClusterInput{ 40 | Name: aws.String(clusterName), 41 | } 42 | result, err := c.EKSClient.DescribeCluster(input) 43 | if err != nil { 44 | return "", err 45 | } 46 | return aws.StringValue(result.Cluster.ResourcesVpcConfig.VpcId), nil 47 | } 48 | 49 | func getAccountNumber(svc stsiface.STSAPI) string { 50 | // Region is defaulted to "us-west-2" 51 | input := &sts.GetCallerIdentityInput{} 52 | result, err := svc.GetCallerIdentity(input) 53 | if err != nil { 54 | log.Infof("Failed to get caller identity: %s", err.Error()) 55 | return "" 56 | } 57 | return *result.Account 58 | } 59 | 60 | func (c *ClientSet) getDNSRecord(dnsName string, hostedZoneID string) (string, error) { 61 | params := &route53.ListResourceRecordSetsInput{ 62 | HostedZoneId: aws.String(hostedZoneID), 63 | MaxItems: aws.String("1"), 64 | StartRecordName: aws.String(dnsName), 65 | } 66 | resp, err := c.Route53Client.ListResourceRecordSets(params) 67 | if err != nil { 68 | return "", err 69 | } 70 | if len(resp.ResourceRecordSets) == 0 { 71 | return "", fmt.Errorf("no record set exists for hostedZoneID %v with dnsName %v", hostedZoneID, dnsName) 72 | } 73 | recordSet := resp.ResourceRecordSets[0] 74 | 75 | if recordSet.AliasTarget != nil { 76 | aliasRecordValue := aws.StringValue(recordSet.AliasTarget.DNSName) 77 | if aliasRecordValue == "" { 78 | return "", errors.New(fmt.Sprintf("no record set exists for hostedZoneID %v with dnsName %v", hostedZoneID, dnsName)) 79 | } 80 | return aliasRecordValue, nil 81 | } else { 82 | if len(recordSet.ResourceRecords) == 0 { 83 | return "", errors.New(fmt.Sprintf("no record set exists for hostedZoneID %v with dnsName %v", hostedZoneID, dnsName)) 84 | } 85 | 86 | recordValue := aws.StringValue(recordSet.ResourceRecords[0].Value) 87 | if recordValue == "" { 88 | return "", fmt.Errorf("no record set exists for hostedZoneID %v with dnsName %v", hostedZoneID, dnsName) 89 | } 90 | return recordValue, nil 91 | } 92 | } 93 | 94 | func (c *ClientSet) dnsNameInHostedZoneID(dnsName, hostedZoneID string) error { 95 | recordValue, err := c.getDNSRecord(dnsName, hostedZoneID) 96 | if err != nil { 97 | if recordValue != "" { 98 | log.Infof("records for hostedZoneID %s with dnsName %s exists", hostedZoneID, dnsName) 99 | return nil 100 | } else { 101 | return errors.Errorf("records for hostedZoneID %s with dnsName %s doesn't exists", hostedZoneID, dnsName) 102 | } 103 | } 104 | log.Infof("records for hostedZoneID %s with dnsName %s exists", hostedZoneID, dnsName) 105 | return nil 106 | } 107 | 108 | func getClusterName() (string, error) { 109 | return getEnv(clusterNameEnvironmentVariable) 110 | } 111 | 112 | func getEnv(envName string) (string, error) { 113 | if envValue, ok := os.LookupEnv(envName); ok { 114 | return envValue, nil 115 | } 116 | return "", fmt.Errorf("could not get environment variable '%s'", envName) 117 | } 118 | -------------------------------------------------------------------------------- /internal/util/util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package util 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "os" 21 | "path/filepath" 22 | "reflect" 23 | "runtime" 24 | "strconv" 25 | "strings" 26 | "time" 27 | 28 | "github.com/pkg/errors" 29 | log "github.com/sirupsen/logrus" 30 | "k8s.io/apimachinery/pkg/util/wait" 31 | ) 32 | 33 | const ( 34 | DurationMinutes = "minutes" 35 | DurationSeconds = "seconds" 36 | ) 37 | 38 | var ( 39 | DefaultRetry = wait.Backoff{ 40 | Steps: 6, 41 | Duration: 1000 * time.Millisecond, 42 | Factor: 1.0, 43 | Jitter: 0.1, 44 | } 45 | retriableErrors = []string{ 46 | "Unable to reach the kubernetes API", 47 | "Unable to connect to the server", 48 | "EOF", 49 | "transport is closing", 50 | "the object has been modified", 51 | "an error on the server", 52 | } 53 | ) 54 | 55 | type FuncToRetryWithReturn func() (interface{}, error) 56 | type FuncToRetry func() error 57 | 58 | func PathToOSFile(relativePath string) (*os.File, error) { 59 | path, err := filepath.Abs(relativePath) 60 | if err != nil { 61 | return nil, errors.Wrap(err, fmt.Sprintf("failed generate absolute file path of %s", relativePath)) 62 | } 63 | 64 | manifest, err := os.Open(path) 65 | if err != nil { 66 | return nil, errors.Wrap(err, fmt.Sprintf("failed to open file %s", path)) 67 | } 68 | 69 | return manifest, nil 70 | } 71 | 72 | func DeleteEmpty(s []string) []string { 73 | var r []string 74 | for _, str := range s { 75 | if str != "" { 76 | r = append(r, str) 77 | } 78 | } 79 | return r 80 | } 81 | 82 | func IsRetriable(err error) bool { 83 | for _, msg := range retriableErrors { 84 | if strings.Contains(err.Error(), msg) { 85 | return true 86 | } 87 | } 88 | return false 89 | } 90 | 91 | func GetExpBackoff(steps int) wait.Backoff { 92 | return wait.Backoff{ 93 | Duration: 2 * time.Second, 94 | Factor: 2.0, 95 | Jitter: 0.5, 96 | Steps: steps, 97 | Cap: 10 * time.Minute, 98 | } 99 | } 100 | 101 | func RetryOnError(backoff *wait.Backoff, retryExpected func(error) bool, fn FuncToRetryWithReturn) (interface{}, error) { 102 | var ex, lastErr error 103 | var out interface{} 104 | caller := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() 105 | err := wait.ExponentialBackoff(*backoff, func() (bool, error) { 106 | out, ex = fn() 107 | switch { 108 | case ex == nil: 109 | return true, nil 110 | case retryExpected(ex): 111 | lastErr = ex 112 | log.Warnf("A caller %v retried due to exception: %v", caller, ex) 113 | return false, nil 114 | default: 115 | return false, ex 116 | } 117 | }) 118 | if wait.Interrupted(err) { 119 | err = lastErr 120 | } 121 | return out, err 122 | } 123 | 124 | func RetryOnAnyError(backoff *wait.Backoff, fn FuncToRetry) error { 125 | _, err := RetryOnError(backoff, func(err error) bool { 126 | return true 127 | }, func() (interface{}, error) { 128 | return nil, fn() 129 | }) 130 | return err 131 | } 132 | 133 | func StructToPrettyString(st interface{}) string { 134 | s, _ := json.MarshalIndent(st, "", "\t") 135 | return string(s) 136 | } 137 | 138 | func ExtractField(data any, path []string) (any, error) { 139 | if len(path) == 0 || data == nil { 140 | return data, nil 141 | } 142 | 143 | currKey := path[0] 144 | 145 | maybeArr := strings.Split(currKey, "[") 146 | if len(maybeArr) >= 2 { 147 | indexStr := strings.TrimSuffix(maybeArr[1], "]") 148 | i, err := strconv.Atoi(indexStr) 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | dataAtIdx := data.(map[string]any)[maybeArr[0]] 154 | switch dataAtIdx := dataAtIdx.(type) { 155 | case []interface{}: 156 | return ExtractField(dataAtIdx[i], path[1:]) 157 | default: 158 | return ExtractField(dataAtIdx.([]map[string]any)[i], path[1:]) 159 | } 160 | } 161 | 162 | for key, val := range data.(map[string]any) { 163 | if key == currKey { 164 | return ExtractField(val, path[1:]) 165 | } 166 | } 167 | 168 | return nil, errors.New("field not found") 169 | } 170 | -------------------------------------------------------------------------------- /pkg/kube/unstructured/unstructured_helper.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package unstructured 16 | 17 | import ( 18 | "bytes" 19 | "context" 20 | "fmt" 21 | "html/template" 22 | "os" 23 | 24 | "github.com/pkg/errors" 25 | "k8s.io/apimachinery/pkg/api/meta" 26 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 28 | "k8s.io/apimachinery/pkg/runtime/schema" 29 | serializer "k8s.io/apimachinery/pkg/runtime/serializer/yaml" 30 | "k8s.io/client-go/discovery" 31 | "k8s.io/client-go/discovery/cached/memory" 32 | "k8s.io/client-go/dynamic" 33 | "k8s.io/client-go/restmapper" 34 | ) 35 | 36 | const ( 37 | yamlSeparator = "\n---" 38 | trimTokens = "\n " 39 | ) 40 | 41 | type unstructuredResource struct { 42 | GVR *meta.RESTMapping 43 | Resource *unstructured.Unstructured 44 | } 45 | 46 | func GetResource(dc discovery.DiscoveryInterface, TemplateArguments interface{}, resourceFilePath string) (unstructuredResource, error) { 47 | data, err := os.ReadFile(resourceFilePath) 48 | if err != nil { 49 | return unstructuredResource{nil, nil}, err 50 | } 51 | return getResourceFromString(string(data), dc, TemplateArguments) 52 | } 53 | 54 | func GetResources(dc discovery.DiscoveryInterface, TemplateArguments interface{}, resourcesFilePath string) ([]unstructuredResource, error) { 55 | data, err := os.ReadFile(resourcesFilePath) 56 | if err != nil { 57 | return nil, err 58 | } 59 | manifests := bytes.Split(data, []byte(yamlSeparator)) 60 | resourceList := make([]unstructuredResource, 0) 61 | for _, manifest := range manifests { 62 | if len(bytes.Trim(manifest, trimTokens)) == 0 { 63 | continue 64 | } 65 | resource, err := getResourceFromString(string(manifest), dc, TemplateArguments) 66 | if err != nil { 67 | return nil, err 68 | } 69 | resourceList = append(resourceList, resource) 70 | } 71 | return resourceList, err 72 | } 73 | 74 | func GetInstanceGroupList(dynamicClient dynamic.Interface) (*unstructured.UnstructuredList, error) { 75 | const ( 76 | instanceGroupNamespace = "instance-manager" 77 | customResourceGroup = "instancemgr" 78 | customResourceAPIVersion = "v1alpha1" 79 | customeResourceDomain = "keikoproj.io" 80 | customResourceKind = "instancegroups" 81 | ) 82 | var ( 83 | customResourceName = fmt.Sprintf("%v.%v", customResourceGroup, customeResourceDomain) 84 | instanceGroupResource = schema.GroupVersionResource{Group: customResourceName, Version: customResourceAPIVersion, Resource: customResourceKind} 85 | ) 86 | igs, err := dynamicClient.Resource(instanceGroupResource).Namespace(instanceGroupNamespace).List(context.Background(), metav1.ListOptions{}) 87 | if err != nil { 88 | return nil, err 89 | } 90 | return igs, nil 91 | } 92 | 93 | func validateDynamicClient(dynamicClient dynamic.Interface) error { 94 | if dynamicClient == nil { 95 | return errors.Errorf("'k8s.io/client-go/dynamic.Interface' is nil.") 96 | } 97 | return nil 98 | } 99 | 100 | func getResourceFromString(resourceString string, dc discovery.DiscoveryInterface, args interface{}) (unstructuredResource, error) { 101 | resource := &unstructured.Unstructured{} 102 | var renderBuffer bytes.Buffer 103 | 104 | if args != nil { 105 | template, err := template.New("Resource").Parse(resourceString) 106 | if err != nil { 107 | return unstructuredResource{GVR: nil, Resource: resource}, err 108 | } 109 | 110 | err = template.Execute(&renderBuffer, &args) 111 | if err != nil { 112 | return unstructuredResource{GVR: nil, Resource: resource}, err 113 | } 114 | } else { 115 | renderBuffer.WriteString(resourceString) 116 | } 117 | 118 | dec := serializer.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) 119 | _, gvk, err := dec.Decode(renderBuffer.Bytes(), nil, resource) 120 | if err != nil { 121 | return unstructuredResource{GVR: nil, Resource: resource}, err 122 | } 123 | gvr, err := getGVR(gvk, dc) 124 | if err != nil { 125 | return unstructuredResource{GVR: nil, Resource: resource}, err 126 | } 127 | return unstructuredResource{GVR: gvr, Resource: resource}, err 128 | } 129 | 130 | func getGVR(gvk *schema.GroupVersionKind, dc discovery.DiscoveryInterface) (*meta.RESTMapping, error) { 131 | if dc == nil { 132 | return nil, errors.Errorf("'k8s.io/client-go/discovery.DiscoveryInterface' is nil.") 133 | } 134 | 135 | CachedDiscoveryInterface := memory.NewMemCacheClient(dc) 136 | DeferredDiscoveryRESTMapper := restmapper.NewDeferredDiscoveryRESTMapper(CachedDiscoveryInterface) 137 | RESTMapping, err := DeferredDiscoveryRESTMapper.RESTMapping(gvk.GroupKind(), gvk.Version) 138 | if err != nil { 139 | return nil, err 140 | } 141 | return RESTMapping, nil 142 | } 143 | -------------------------------------------------------------------------------- /pkg/aws/iam/iam.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package iam 16 | 17 | import ( 18 | "fmt" 19 | "net/url" 20 | "reflect" 21 | "strings" 22 | 23 | "github.com/aws/aws-sdk-go/aws" 24 | "github.com/aws/aws-sdk-go/aws/awserr" 25 | "github.com/aws/aws-sdk-go/service/iam" 26 | "github.com/aws/aws-sdk-go/service/iam/iamiface" 27 | "github.com/keikoproj/kubedog/internal/util" 28 | log "github.com/sirupsen/logrus" 29 | ) 30 | 31 | func GetIamRole(roleName string, iamClient iamiface.IAMAPI) (*iam.Role, error) { 32 | params := &iam.GetRoleInput{ 33 | RoleName: aws.String(roleName), 34 | } 35 | out, err := util.RetryOnError(&util.DefaultRetry, isThrottling, func() (interface{}, error) { 36 | return iamClient.GetRole(params) 37 | }) 38 | if err != nil { 39 | return nil, fmt.Errorf("failed to get iam role %q. %v", roleName, err) 40 | } 41 | 42 | return out.(*iam.GetRoleOutput).Role, nil 43 | } 44 | 45 | func PutIAMRole(name, description string, policyJSON []byte, iamClient iamiface.IAMAPI, tags ...*iam.Tag) (*iam.Role, error) { 46 | out, err := GetIamRole(name, iamClient) 47 | if err != nil { 48 | out, err := createIAMRole(name, description, policyJSON, iamClient, tags...) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return out, nil 53 | } 54 | // If role already exits just update assume role policy 55 | _, err = UpdateIAMAssumeRole(name, policyJSON, iamClient) 56 | if err != nil { 57 | return out, err 58 | } 59 | return out, nil 60 | } 61 | 62 | func UpdateIAMAssumeRole(roleName string, policyJSON []byte, iamClient iamiface.IAMAPI) (*iam.UpdateAssumeRolePolicyOutput, error) { 63 | json := string(policyJSON) 64 | params := &iam.UpdateAssumeRolePolicyInput{ 65 | RoleName: aws.String(roleName), 66 | PolicyDocument: aws.String(json), 67 | } 68 | out, err := util.RetryOnError(&util.DefaultRetry, isThrottling, func() (interface{}, error) { 69 | return iamClient.UpdateAssumeRolePolicy(params) 70 | }) 71 | if err != nil { 72 | return nil, fmt.Errorf("failed to update assume role policy for %q .%v", roleName, err) 73 | } 74 | 75 | return out.(*iam.UpdateAssumeRolePolicyOutput), nil 76 | } 77 | 78 | func PutManagedPolicy(name, arn, description string, policyJSON []byte, iamClient iamiface.IAMAPI) (*iam.Policy, error) { 79 | existingPolicy, existingPolicyVersion, err := getManagedPolicy(arn, iamClient) 80 | if err != nil { 81 | if strings.Contains(err.Error(), iam.ErrCodeNoSuchEntityException) { 82 | out, err := createManagedPolicy(name, description, policyJSON, iamClient) 83 | if err != nil { 84 | return nil, fmt.Errorf("failed to create managed policy %q. %v", name, err) 85 | } 86 | return out, nil 87 | } 88 | return nil, err 89 | } 90 | 91 | existingPolicyDoc, err := url.QueryUnescape(*existingPolicyVersion.Document) 92 | if err != nil { 93 | return nil, fmt.Errorf("failed to url decode managed policy document %q. %v", name, err) 94 | } 95 | if reflect.DeepEqual(existingPolicyDoc, string(policyJSON)) { 96 | log.Infof("managed policy %s is same as before, hence not updating", name) 97 | return existingPolicy, nil 98 | } 99 | 100 | out, err := updateManagedPolicy(arn, policyJSON, iamClient) 101 | if err != nil { 102 | return nil, fmt.Errorf("failed to update managed policy %q. %v", arn, err) 103 | } 104 | 105 | return &iam.Policy{ 106 | Arn: existingPolicy.Arn, 107 | CreateDate: out.CreateDate, 108 | DefaultVersionId: out.VersionId, 109 | PermissionsBoundaryUsageCount: existingPolicy.PermissionsBoundaryUsageCount, 110 | PolicyId: existingPolicy.PolicyId, 111 | PolicyName: existingPolicy.PolicyName, 112 | }, nil 113 | } 114 | 115 | func DeleteManagedPolicy(arn string, iamClient iamiface.IAMAPI) error { 116 | // first list all version in order to delete them 117 | policyVersions, err := listManagedPolicyVersions(arn, iamClient) 118 | if err != nil { 119 | return fmt.Errorf("failed to list managed policy versions %q. %v", arn, err) 120 | } 121 | 122 | // delete all versions except default, as default version can only be deleted with policy 123 | for _, ver := range policyVersions { 124 | if !(*ver.IsDefaultVersion) { 125 | err := deleteManagedPolicyVersion(arn, *ver.VersionId, iamClient) 126 | if err != nil { 127 | return fmt.Errorf("failed to delete managed policy %q version %q. %v", arn, *ver.VersionId, err) 128 | } 129 | } 130 | } 131 | 132 | // now delete policy 133 | params := &iam.DeletePolicyInput{ 134 | PolicyArn: aws.String(arn), 135 | } 136 | _, err = util.RetryOnError(&util.DefaultRetry, isThrottling, func() (interface{}, error) { 137 | return iamClient.DeletePolicy(params) 138 | }) 139 | if err != nil { 140 | return err 141 | } 142 | return nil 143 | } 144 | 145 | func DeleteIAMRole(roleName string, iamClient iamiface.IAMAPI) error { 146 | params := &iam.DeleteRoleInput{ 147 | RoleName: aws.String(roleName), 148 | } 149 | _, err := util.RetryOnError(&util.DefaultRetry, isThrottling, func() (interface{}, error) { 150 | return iamClient.DeleteRole(params) 151 | }) 152 | if err != nil { 153 | if aerr, ok := err.(awserr.Error); ok { 154 | if aerr.Code() == "NoSuchEntity" { 155 | return nil 156 | } 157 | } 158 | return fmt.Errorf("failed to delete iam role %q. %v", roleName, err) 159 | } 160 | 161 | return nil 162 | } 163 | -------------------------------------------------------------------------------- /pkg/kube/pod/pod_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package pod 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/keikoproj/kubedog/internal/util" 21 | v1 "k8s.io/api/core/v1" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | "k8s.io/apimachinery/pkg/runtime" 24 | "k8s.io/apimachinery/pkg/util/wait" 25 | fakeDiscovery "k8s.io/client-go/discovery/fake" 26 | "k8s.io/client-go/dynamic" 27 | fakeDynamic "k8s.io/client-go/dynamic/fake" 28 | "k8s.io/client-go/kubernetes" 29 | "k8s.io/client-go/kubernetes/fake" 30 | ) 31 | 32 | func Test_PodsInNamespaceWithSelectorShouldHaveLabels(t *testing.T) { 33 | ns := v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "foo"}} 34 | podWithLabels1 := v1.Pod{ 35 | ObjectMeta: metav1.ObjectMeta{ 36 | Name: "pod-foo-xhhxj", 37 | Namespace: "foo", 38 | Labels: map[string]string{ 39 | "app": "foo", 40 | "label": "true", 41 | }, 42 | }, 43 | } 44 | podWithLabels2 := v1.Pod{ 45 | ObjectMeta: metav1.ObjectMeta{ 46 | Name: "pod-foo-xhhzd", 47 | Namespace: "foo", 48 | Labels: map[string]string{ 49 | "app": "foo", 50 | "label": "true", 51 | }, 52 | }, 53 | } 54 | podMissingLabel := v1.Pod{ 55 | ObjectMeta: metav1.ObjectMeta{ 56 | Name: "pod-foo-xhhzr", 57 | Namespace: "foo", 58 | Labels: map[string]string{ 59 | "app": "foo", 60 | }, 61 | }, 62 | } 63 | clientNoErr := fake.NewSimpleClientset(&ns, &podWithLabels1, &podWithLabels2) 64 | clientErr := fake.NewSimpleClientset(&ns, &podWithLabels1, &podWithLabels2, &podMissingLabel) 65 | dynScheme := runtime.NewScheme() 66 | fakeDynamicClient := fakeDynamic.NewSimpleDynamicClient(dynScheme) 67 | fakeDiscoveryClient := fakeDiscovery.FakeDiscovery{} 68 | 69 | type fields struct { 70 | KubeInterface kubernetes.Interface 71 | DynamicInterface dynamic.Interface 72 | DiscoveryInterface *fakeDiscovery.FakeDiscovery 73 | } 74 | type args struct { 75 | namespace string 76 | selector string 77 | labels string 78 | } 79 | tests := []struct { 80 | name string 81 | fields fields 82 | args args 83 | wantErr bool 84 | }{ 85 | { 86 | name: "No pods found", 87 | fields: fields{ 88 | KubeInterface: clientErr, 89 | DiscoveryInterface: &fakeDiscoveryClient, 90 | DynamicInterface: fakeDynamicClient, 91 | }, 92 | args: args{ 93 | selector: "app=doesnotexist", 94 | namespace: "foo", 95 | labels: "app=foo,label=true", 96 | }, 97 | wantErr: true, 98 | }, 99 | { 100 | name: "Pods should have labels", 101 | fields: fields{ 102 | KubeInterface: clientNoErr, 103 | DiscoveryInterface: &fakeDiscoveryClient, 104 | DynamicInterface: fakeDynamicClient, 105 | }, 106 | args: args{ 107 | selector: "app=foo", 108 | namespace: "foo", 109 | labels: "app=foo,label=true", 110 | }, 111 | wantErr: false, 112 | }, 113 | { 114 | name: "Error from pod missing label", 115 | fields: fields{ 116 | KubeInterface: clientErr, 117 | DiscoveryInterface: &fakeDiscoveryClient, 118 | DynamicInterface: fakeDynamicClient, 119 | }, 120 | args: args{ 121 | selector: "app=foo", 122 | namespace: "foo", 123 | labels: "app=foo,label=true", 124 | }, 125 | wantErr: true, 126 | }, 127 | } 128 | for _, tt := range tests { 129 | t.Run(tt.name, func(t *testing.T) { 130 | if err := PodsInNamespaceWithSelectorShouldHaveLabels(tt.fields.KubeInterface, tt.args.namespace, tt.args.selector, tt.args.labels); (err != nil) != tt.wantErr { 131 | t.Errorf("ThePodsInNamespaceWithSelectorShouldHaveLabels() error = %v, wantErr %v", err, tt.wantErr) 132 | } 133 | }) 134 | } 135 | } 136 | 137 | func TestPodsInNamespaceWithLabelSelectorConvergeToFieldSelector(t *testing.T) { 138 | type args struct { 139 | kubeClientset kubernetes.Interface 140 | expBackoff wait.Backoff 141 | namespace string 142 | labelSelector string 143 | fieldSelector string 144 | } 145 | namespaceName := "test-ns" 146 | ns := v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespaceName}} 147 | podSucceeded := v1.Pod{ 148 | ObjectMeta: metav1.ObjectMeta{ 149 | Name: "pod-xhhxj", 150 | Namespace: "test-ns", 151 | Labels: map[string]string{ 152 | "app": "test-service", 153 | }, 154 | }, 155 | Status: v1.PodStatus{ 156 | Phase: v1.PodSucceeded, 157 | }, 158 | } 159 | tests := []struct { 160 | name string 161 | args args 162 | wantErr bool 163 | }{ 164 | { 165 | name: "Positive Test", 166 | args: args{ 167 | kubeClientset: fake.NewSimpleClientset(&ns, &podSucceeded), 168 | expBackoff: util.DefaultRetry, 169 | namespace: namespaceName, 170 | labelSelector: "app=test-service", 171 | fieldSelector: "status.phase=Succeeded", 172 | }, 173 | }, 174 | { 175 | name: "Negative Test: no pods with label selector", 176 | args: args{ 177 | kubeClientset: fake.NewSimpleClientset(&ns), 178 | expBackoff: util.DefaultRetry, 179 | namespace: namespaceName, 180 | labelSelector: "app=test-service", 181 | fieldSelector: "status.phase=Succeeded", 182 | }, 183 | wantErr: true, 184 | }, 185 | } 186 | for _, tt := range tests { 187 | t.Run(tt.name, func(t *testing.T) { 188 | if err := PodsInNamespaceWithLabelSelectorConvergeToFieldSelector(tt.args.kubeClientset, tt.args.expBackoff, tt.args.namespace, tt.args.labelSelector, tt.args.fieldSelector); (err != nil) != tt.wantErr { 189 | t.Errorf("PodsInNamespaceWithLabelSelectorConvergeToFieldSelector() error = %v, wantErr %v", err, tt.wantErr) 190 | } 191 | }) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /pkg/aws/iam/iam_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package iam 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | "github.com/aws/aws-sdk-go/aws" 22 | "github.com/aws/aws-sdk-go/service/iam" 23 | "github.com/aws/aws-sdk-go/service/iam/iamiface" 24 | "github.com/onsi/gomega" 25 | "sigs.k8s.io/yaml" 26 | ) 27 | 28 | // mock IAM client 29 | type FakeIAMClient struct { 30 | iamiface.IAMAPI 31 | RoleArn string 32 | } 33 | 34 | func (fiam *FakeIAMClient) CreateRole(roleInput *iam.CreateRoleInput) (*iam.CreateRoleOutput, error) { 35 | output := &iam.CreateRoleOutput{ 36 | Role: &iam.Role{RoleName: roleInput.RoleName}, 37 | } 38 | if fiam.RoleArn != "" { 39 | output.Role.Arn = aws.String(fiam.RoleArn) 40 | } 41 | return output, nil 42 | } 43 | 44 | func (fiam *FakeIAMClient) GetRole(roleInput *iam.GetRoleInput) (*iam.GetRoleOutput, error) { 45 | output := &iam.GetRoleOutput{ 46 | Role: &iam.Role{RoleName: roleInput.RoleName}, 47 | } 48 | if fiam.RoleArn != "" { 49 | output.Role.Arn = aws.String(fiam.RoleArn) 50 | } 51 | return output, nil 52 | } 53 | 54 | func (fiam *FakeIAMClient) DeleteRole(delRoleInput *iam.DeleteRoleInput) (*iam.DeleteRoleOutput, error) { 55 | return &iam.DeleteRoleOutput{}, nil 56 | } 57 | 58 | func (fiam *FakeIAMClient) UpdateAssumeRolePolicy(*iam.UpdateAssumeRolePolicyInput) (*iam.UpdateAssumeRolePolicyOutput, error) { 59 | return &iam.UpdateAssumeRolePolicyOutput{}, nil 60 | } 61 | 62 | func (fiam *FakeIAMClient) GetPolicy(*iam.GetPolicyInput) (*iam.GetPolicyOutput, error) { 63 | return &iam.GetPolicyOutput{ 64 | Policy: &iam.Policy{DefaultVersionId: aws.String("v1")}, 65 | }, nil 66 | } 67 | 68 | func (fiam *FakeIAMClient) GetPolicyVersion(*iam.GetPolicyVersionInput) (*iam.GetPolicyVersionOutput, error) { 69 | return &iam.GetPolicyVersionOutput{ 70 | PolicyVersion: &iam.PolicyVersion{Document: aws.String("Version: '2012-10-17'")}, 71 | }, nil 72 | } 73 | 74 | func (fiam *FakeIAMClient) CreatePolicy(*iam.CreatePolicyInput) (*iam.CreatePolicyOutput, error) { 75 | return &iam.CreatePolicyOutput{}, nil 76 | } 77 | 78 | func (fiam *FakeIAMClient) ListPolicyVersions(*iam.ListPolicyVersionsInput) (*iam.ListPolicyVersionsOutput, error) { 79 | return &iam.ListPolicyVersionsOutput{}, nil 80 | } 81 | 82 | func (fiam *FakeIAMClient) CreatePolicyVersion(*iam.CreatePolicyVersionInput) (*iam.CreatePolicyVersionOutput, error) { 83 | return &iam.CreatePolicyVersionOutput{ 84 | PolicyVersion: &iam.PolicyVersion{ 85 | CreateDate: aws.Time(time.Now()), 86 | IsDefaultVersion: aws.Bool(true), 87 | VersionId: aws.String("v2"), 88 | }, 89 | }, nil 90 | } 91 | 92 | func (fiam *FakeIAMClient) DeletePolicy(*iam.DeletePolicyInput) (*iam.DeletePolicyOutput, error) { 93 | return &iam.DeletePolicyOutput{}, nil 94 | } 95 | 96 | func (fiam *FakeIAMClient) DeletePolicyVersion(input *iam.DeletePolicyVersionInput) (*iam.DeletePolicyVersionOutput, error) { 97 | return &iam.DeletePolicyVersionOutput{}, nil 98 | } 99 | 100 | func TestDeleteIAMRole(t *testing.T) { 101 | g := gomega.NewGomegaWithT(t) 102 | 103 | iamClient := &FakeIAMClient{} 104 | 105 | err := DeleteIAMRole("arn:aws:iam::aws:policy/test-role", iamClient) 106 | g.Expect(err).To(gomega.BeNil()) 107 | } 108 | 109 | func TestGetIamRole(t *testing.T) { 110 | g := gomega.NewGomegaWithT(t) 111 | 112 | iamClient := &FakeIAMClient{} 113 | 114 | output, err := GetIamRole("arn:aws:iam::aws:policy/test-role", iamClient) 115 | g.Expect(err).To(gomega.BeNil()) 116 | g.Expect(output).ToNot(gomega.BeNil()) 117 | } 118 | 119 | func TestUpdateIAMAssumeRole(t *testing.T) { 120 | g := gomega.NewGomegaWithT(t) 121 | 122 | iamClient := &FakeIAMClient{} 123 | policyYAML := `| 124 | Version: '2012-10-17' 125 | Statement: 126 | - Effect: Allow 127 | Action: 128 | - autoscaling:TerminateInstanceInAutoScalingGroup 129 | - autoscaling:DescribeAutoScalingGroups 130 | - ec2:DescribeTags 131 | - ec2:DescribeInstances 132 | Resource: 133 | - "*"` 134 | policyJSON, err := yaml.YAMLToJSON([]byte(policyYAML)) 135 | g.Expect(err).To(gomega.BeNil()) 136 | 137 | output, err := UpdateIAMAssumeRole("arn:aws:iam::aws:policy/test-role", policyJSON, iamClient) 138 | g.Expect(err).To(gomega.BeNil()) 139 | g.Expect(output).ToNot(gomega.BeNil()) 140 | } 141 | 142 | func TestPutIAMRole(t *testing.T) { 143 | g := gomega.NewGomegaWithT(t) 144 | 145 | iamClient := &FakeIAMClient{} 146 | policyYAML := `| 147 | Version: '2012-10-17' 148 | Statement: 149 | - Effect: Allow 150 | Action: 151 | - autoscaling:TerminateInstanceInAutoScalingGroup 152 | - autoscaling:DescribeAutoScalingGroups 153 | - ec2:DescribeTags 154 | - ec2:DescribeInstances 155 | Resource: 156 | - "*"` 157 | policyJSON, err := yaml.YAMLToJSON([]byte(policyYAML)) 158 | g.Expect(err).To(gomega.BeNil()) 159 | 160 | output, err := PutIAMRole("arn:aws:iam::aws:policy/test-role", "Description", policyJSON, iamClient) 161 | g.Expect(err).To(gomega.BeNil()) 162 | g.Expect(output).ToNot(gomega.BeNil()) 163 | } 164 | 165 | func TestPutManagedPolicy(t *testing.T) { 166 | g := gomega.NewGomegaWithT(t) 167 | 168 | iamClient := &FakeIAMClient{} 169 | policyYAML := `| 170 | Version: '2012-10-17' 171 | Statement: 172 | - Effect: Allow 173 | Action: 174 | - autoscaling:TerminateInstanceInAutoScalingGroup 175 | - autoscaling:DescribeAutoScalingGroups 176 | - ec2:DescribeTags 177 | - ec2:DescribeInstances 178 | Resource: 179 | - "*"` 180 | policyJSON, err := yaml.YAMLToJSON([]byte(policyYAML)) 181 | g.Expect(err).To(gomega.BeNil()) 182 | 183 | output, err := PutManagedPolicy("arn:aws:iam::aws:policy/test-role", "arn:aws:iam::aws:policy/test-arn", "Description", policyJSON, iamClient) 184 | g.Expect(err).To(gomega.BeNil()) 185 | g.Expect(output).ToNot(gomega.BeNil()) 186 | } 187 | 188 | func TestDeleteManagedPolicy(t *testing.T) { 189 | g := gomega.NewGomegaWithT(t) 190 | 191 | iamClient := &FakeIAMClient{} 192 | 193 | err := DeleteManagedPolicy("arn:aws:iam::aws:policy/test-role", iamClient) 194 | g.Expect(err).To(gomega.BeNil()) 195 | } 196 | -------------------------------------------------------------------------------- /pkg/aws/iam/iam_helper.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package iam 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/aws/aws-sdk-go/aws" 21 | "github.com/aws/aws-sdk-go/aws/awserr" 22 | "github.com/aws/aws-sdk-go/service/iam" 23 | "github.com/aws/aws-sdk-go/service/iam/iamiface" 24 | "github.com/keikoproj/kubedog/internal/util" 25 | ) 26 | 27 | func getManagedPolicy(policyARN string, iamClient iamiface.IAMAPI) (*iam.Policy, *iam.PolicyVersion, error) { 28 | policyParams := &iam.GetPolicyInput{ 29 | PolicyArn: aws.String(policyARN), 30 | } 31 | out, err := util.RetryOnError(&util.DefaultRetry, isThrottling, func() (interface{}, error) { 32 | return iamClient.GetPolicy(policyParams) 33 | }) 34 | if err != nil { 35 | return nil, nil, fmt.Errorf("failed to get managed policy %q. %v", policyARN, err) 36 | } 37 | policyVersionParams := &iam.GetPolicyVersionInput{ 38 | PolicyArn: aws.String(policyARN), 39 | VersionId: out.(*iam.GetPolicyOutput).Policy.DefaultVersionId, 40 | } 41 | policyVersionOut, err := util.RetryOnError(&util.DefaultRetry, isThrottling, func() (interface{}, error) { 42 | return iamClient.GetPolicyVersion(policyVersionParams) 43 | }) 44 | if err != nil { 45 | return out.(*iam.GetPolicyOutput).Policy, nil, fmt.Errorf("failed to get managed policy version %q. %v", policyARN, err) 46 | } 47 | return out.(*iam.GetPolicyOutput).Policy, policyVersionOut.(*iam.GetPolicyVersionOutput).PolicyVersion, nil 48 | } 49 | 50 | func updateManagedPolicy(arn string, policyJSON []byte, iamClient iamiface.IAMAPI) (*iam.PolicyVersion, error) { 51 | versions, err := listManagedPolicyVersions(arn, iamClient) 52 | if err != nil { 53 | return nil, fmt.Errorf("failed to list managed policy versions %q. %v", arn, err) 54 | } 55 | 56 | if len(versions) >= 3 { 57 | verId := getOldestVersionID(versions) 58 | err := deleteManagedPolicyVersion(arn, verId, iamClient) 59 | if err != nil { 60 | return nil, fmt.Errorf("failed to delete managed policy %q version %q. %v", arn, verId, err) 61 | } 62 | } 63 | 64 | out, err := createManagedPolicyVersion(arn, policyJSON, true, iamClient) 65 | if err != nil { 66 | return nil, fmt.Errorf("failed to create managed policy version %q. %v", arn, err) 67 | } 68 | 69 | return out, nil 70 | } 71 | 72 | func createManagedPolicyVersion(arn string, policyJSON []byte, isDefault bool, iamClient iamiface.IAMAPI) (*iam.PolicyVersion, error) { 73 | json := string(policyJSON) 74 | params := &iam.CreatePolicyVersionInput{ 75 | PolicyArn: aws.String(arn), 76 | PolicyDocument: aws.String(json), 77 | SetAsDefault: aws.Bool(isDefault), 78 | } 79 | out, err := util.RetryOnError(&util.DefaultRetry, isThrottling, func() (interface{}, error) { 80 | return iamClient.CreatePolicyVersion(params) 81 | }) 82 | if err != nil { 83 | return nil, fmt.Errorf("faild to create managed policy version %q. %v", arn, err) 84 | } 85 | return out.(*iam.CreatePolicyVersionOutput).PolicyVersion, nil 86 | } 87 | 88 | func createManagedPolicy(name, description string, policyJSON []byte, iamClient iamiface.IAMAPI) (*iam.Policy, error) { 89 | json := string(policyJSON) 90 | params := &iam.CreatePolicyInput{ 91 | Description: aws.String(description), 92 | PolicyDocument: aws.String(json), 93 | PolicyName: aws.String(name), 94 | } 95 | 96 | out, err := util.RetryOnError(&util.DefaultRetry, isThrottling, func() (interface{}, error) { 97 | return iamClient.CreatePolicy(params) 98 | }) 99 | if err != nil { 100 | return nil, fmt.Errorf("failed to create managed policy %q. %v", name, err) 101 | } 102 | 103 | return out.(*iam.CreatePolicyOutput).Policy, nil 104 | } 105 | 106 | func listManagedPolicyVersions(arn string, iamClient iamiface.IAMAPI) ([]*iam.PolicyVersion, error) { 107 | params := &iam.ListPolicyVersionsInput{ 108 | PolicyArn: aws.String(arn), 109 | } 110 | listVersionsOutput, err := util.RetryOnError(&util.DefaultRetry, isThrottling, func() (interface{}, error) { 111 | return iamClient.ListPolicyVersions(params) 112 | }) 113 | if err != nil { 114 | return nil, fmt.Errorf("failed to list managed policy versions %q. %v", arn, err) 115 | } 116 | return listVersionsOutput.(*iam.ListPolicyVersionsOutput).Versions, nil 117 | } 118 | 119 | func getOldestVersionID(versions []*iam.PolicyVersion) string { 120 | earliestTime := *versions[0].CreateDate 121 | oldestVersionID := *versions[0].VersionId 122 | for _, ver := range versions { 123 | if earliestTime.After(*ver.CreateDate) { 124 | oldestVersionID = *ver.VersionId 125 | earliestTime = *ver.CreateDate 126 | } 127 | } 128 | return oldestVersionID 129 | } 130 | 131 | func deleteManagedPolicyVersion(arn, id string, iamClient iamiface.IAMAPI) error { 132 | params := &iam.DeletePolicyVersionInput{ 133 | PolicyArn: aws.String(arn), 134 | VersionId: aws.String(id), 135 | } 136 | _, err := util.RetryOnError(&util.DefaultRetry, isThrottling, func() (interface{}, error) { 137 | return iamClient.DeletePolicyVersion(params) 138 | }) 139 | if err != nil { 140 | return fmt.Errorf("failed to delete managed policy %q version %q. %v", arn, id, err) 141 | } 142 | return nil 143 | } 144 | 145 | func isThrottling(err error) bool { 146 | if awsErr, ok := err.(awserr.Error); ok { 147 | if awsErr.Code() == "Throttling" { 148 | return true 149 | } 150 | } 151 | return false 152 | } 153 | 154 | func createIAMRole(name, description string, policyJSON []byte, iamClient iamiface.IAMAPI, tags ...*iam.Tag) (*iam.Role, error) { 155 | json := string(policyJSON) 156 | role := &iam.CreateRoleInput{ 157 | RoleName: aws.String(name), 158 | AssumeRolePolicyDocument: aws.String(json), 159 | Description: aws.String(description), 160 | } 161 | if len(tags) > 0 { 162 | role.Tags = tags 163 | } 164 | out, err := util.RetryOnError(&util.DefaultRetry, isThrottling, func() (interface{}, error) { 165 | return iamClient.CreateRole(role) 166 | }) 167 | 168 | if err != nil { 169 | return nil, fmt.Errorf("failed to create iam role with policy %q. %v", name, err) 170 | } 171 | 172 | return out.(*iam.CreateRoleOutput).Role, nil 173 | } 174 | -------------------------------------------------------------------------------- /pkg/aws/aws_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package aws 16 | 17 | import ( 18 | "errors" 19 | "testing" 20 | 21 | "github.com/aws/aws-sdk-go/aws" 22 | "github.com/aws/aws-sdk-go/service/autoscaling" 23 | "github.com/aws/aws-sdk-go/service/autoscaling/autoscalingiface" 24 | "github.com/aws/aws-sdk-go/service/sts" 25 | "github.com/aws/aws-sdk-go/service/sts/stsiface" 26 | "github.com/onsi/gomega" 27 | ) 28 | 29 | const ( 30 | TestAwsAccountNumber = "0000123456789" 31 | ) 32 | 33 | type mockAutoScalingClient struct { 34 | autoscalingiface.AutoScalingAPI 35 | ASGs []*autoscaling.Group 36 | Err error 37 | } 38 | 39 | type STSMocker struct { 40 | stsiface.STSAPI 41 | } 42 | 43 | func (s *STSMocker) GetCallerIdentity(*sts.GetCallerIdentityInput) (*sts.GetCallerIdentityOutput, error) { 44 | output := &sts.GetCallerIdentityOutput{ 45 | Account: aws.String(TestAwsAccountNumber), 46 | } 47 | 48 | return output, nil 49 | } 50 | 51 | func TestAnASGNamed(t *testing.T) { 52 | var ( 53 | g = gomega.NewWithT(t) 54 | tests = []struct { 55 | ASClient mockAutoScalingClient 56 | expectedASG *autoscaling.Group 57 | expectError bool 58 | }{ 59 | { // ASClient.DescribeAutoScalingGroups fails 60 | ASClient: mockAutoScalingClient{ 61 | Err: errors.New("some DescribeAutoScalingGroups error"), 62 | }, 63 | expectedASG: &autoscaling.Group{ 64 | AutoScalingGroupName: aws.String("some-ASG-name"), 65 | }, 66 | expectError: true, 67 | }, 68 | { // case len(out.AutoScalingGroups) = 0 69 | ASClient: mockAutoScalingClient{ 70 | ASGs: []*autoscaling.Group{ 71 | { 72 | AutoScalingGroupName: aws.String("ASG-name-1"), 73 | LaunchConfigurationName: aws.String("ASG-name-1-LC"), 74 | AutoScalingGroupARN: aws.String("ASG-name-1-ARN"), 75 | }, 76 | }, 77 | Err: nil, 78 | }, 79 | expectedASG: &autoscaling.Group{ 80 | AutoScalingGroupName: aws.String("ASG-name-2"), 81 | }, 82 | expectError: true, 83 | }, 84 | { // case len(out.AutoScalingGroups) = 1 85 | ASClient: mockAutoScalingClient{ 86 | ASGs: []*autoscaling.Group{ 87 | { 88 | AutoScalingGroupName: aws.String("ASG-name-1"), 89 | LaunchConfigurationName: aws.String("ASG-name-1-LC"), 90 | AutoScalingGroupARN: aws.String("ASG-name-1-ARN"), 91 | }, 92 | { 93 | AutoScalingGroupName: aws.String("ASG-name-2"), 94 | LaunchConfigurationName: aws.String("ASG-name-2-LC"), 95 | AutoScalingGroupARN: aws.String("ASG-name-2-ARN"), 96 | }, 97 | }, 98 | Err: nil, 99 | }, 100 | expectedASG: &autoscaling.Group{ 101 | AutoScalingGroupName: aws.String("ASG-name-2"), 102 | LaunchConfigurationName: aws.String("ASG-name-2-LC"), 103 | }, 104 | expectError: false, 105 | }, 106 | } 107 | ) 108 | 109 | // Not ASClient 110 | ASC := ClientSet{} 111 | err := ASC.AnASGNamed("Some-ASG-Name") 112 | g.Expect(err).Should(gomega.HaveOccurred()) 113 | 114 | for _, test := range tests { 115 | client := ClientSet{ASClient: &test.ASClient} 116 | ASG_Name := aws.StringValue(test.expectedASG.AutoScalingGroupName) 117 | err := client.AnASGNamed(ASG_Name) 118 | 119 | if test.expectError { 120 | g.Expect(err).Should(gomega.HaveOccurred()) 121 | g.Expect(client.asgName).To(gomega.Equal("")) 122 | g.Expect(client.launchConfigName).To(gomega.Equal("")) 123 | } else { 124 | LC_Name := aws.StringValue(test.expectedASG.LaunchConfigurationName) 125 | g.Expect(err).ShouldNot(gomega.HaveOccurred()) 126 | g.Expect(client.asgName).To(gomega.Equal(ASG_Name)) 127 | g.Expect(client.launchConfigName).To(gomega.Equal(LC_Name)) 128 | } 129 | } 130 | } 131 | func TestPositiveUpdateFieldOfCurrentASG(t *testing.T) { 132 | var ( 133 | g = gomega.NewWithT(t) 134 | ASC = ClientSet{ 135 | ASClient: &mockAutoScalingClient{ 136 | Err: nil, 137 | }, 138 | asgName: "asg-test", 139 | launchConfigName: "current-lc-asg-test", 140 | } 141 | ) 142 | 143 | const ( 144 | someLaunchConfigName = "new-lc-asg-test" 145 | someNumber = "3" 146 | ) 147 | 148 | err := ASC.UpdateFieldOfCurrentASG("LaunchConfigurationName", someLaunchConfigName) 149 | g.Expect(err).ShouldNot(gomega.HaveOccurred()) 150 | 151 | err = ASC.UpdateFieldOfCurrentASG("MinSize", someNumber) 152 | g.Expect(err).ShouldNot(gomega.HaveOccurred()) 153 | 154 | err = ASC.UpdateFieldOfCurrentASG("DesiredCapacity", someNumber) 155 | g.Expect(err).ShouldNot(gomega.HaveOccurred()) 156 | 157 | err = ASC.UpdateFieldOfCurrentASG("MaxSize", someNumber) 158 | g.Expect(err).ShouldNot(gomega.HaveOccurred()) 159 | } 160 | 161 | func TestNegativeUpdateFieldOfCurrentASG(t *testing.T) { 162 | var ( 163 | g = gomega.NewWithT(t) 164 | emptyASC = ClientSet{} 165 | ASC = ClientSet{ 166 | ASClient: &mockAutoScalingClient{ 167 | Err: errors.New("some-UASG-error"), 168 | }, 169 | asgName: "asg-test", 170 | launchConfigName: "current-lc-asg-test", 171 | } 172 | ) 173 | 174 | const ( 175 | someLaunchConfigName = "new-lc-asg-test" 176 | someNumber = "3" 177 | ) 178 | 179 | // Empty client 180 | err := emptyASC.UpdateFieldOfCurrentASG("someField", "someValue") 181 | g.Expect(err).Should(gomega.HaveOccurred()) 182 | // Invalid size value 183 | err = ASC.UpdateFieldOfCurrentASG("someSizeField", "someInvalidValue") 184 | g.Expect(err).Should(gomega.HaveOccurred()) 185 | // Unsupported field 186 | err = ASC.UpdateFieldOfCurrentASG("someNotSupportedField", someNumber) 187 | g.Expect(err).Should(gomega.HaveOccurred()) 188 | // Error updating Launch Config 189 | err = ASC.UpdateFieldOfCurrentASG("LaunchConfigurationName", someLaunchConfigName) 190 | g.Expect(err).Should(gomega.HaveOccurred()) 191 | // Error updating the size 192 | err = ASC.UpdateFieldOfCurrentASG("MinSize", someNumber) 193 | g.Expect(err).Should(gomega.HaveOccurred()) 194 | err = ASC.UpdateFieldOfCurrentASG("DesiredCapacity", someNumber) 195 | g.Expect(err).Should(gomega.HaveOccurred()) 196 | err = ASC.UpdateFieldOfCurrentASG("MaxSize", someNumber) 197 | g.Expect(err).Should(gomega.HaveOccurred()) 198 | } 199 | 200 | func TestPositiveScaleCurrentASG(t *testing.T) { 201 | var ( 202 | g = gomega.NewWithT(t) 203 | ASC = ClientSet{ 204 | ASClient: &mockAutoScalingClient{ 205 | Err: nil, 206 | }, 207 | asgName: "asg-test", 208 | } 209 | ) 210 | 211 | const someNumber int64 = 3 212 | 213 | err := ASC.ScaleCurrentASG(someNumber, someNumber) 214 | g.Expect(err).ShouldNot(gomega.HaveOccurred()) 215 | } 216 | 217 | func TestNegativeScaleCurrentASG(t *testing.T) { 218 | var ( 219 | g = gomega.NewWithT(t) 220 | emptyASC = ClientSet{} 221 | ASC = ClientSet{ 222 | ASClient: &mockAutoScalingClient{ 223 | Err: errors.New("some-UASG-error"), 224 | }, 225 | asgName: "asg-test", 226 | } 227 | ) 228 | 229 | const someNumber int64 = 3 230 | 231 | // Empty client 232 | err := emptyASC.ScaleCurrentASG(someNumber, someNumber) 233 | g.Expect(err).Should(gomega.HaveOccurred()) 234 | 235 | // Error scaling ASG 236 | err = ASC.ScaleCurrentASG(someNumber, someNumber) 237 | g.Expect(err).Should(gomega.HaveOccurred()) 238 | } 239 | 240 | func (asc *mockAutoScalingClient) UpdateAutoScalingGroup(input *autoscaling.UpdateAutoScalingGroupInput) (*autoscaling.UpdateAutoScalingGroupOutput, error) { 241 | return &autoscaling.UpdateAutoScalingGroupOutput{}, asc.Err 242 | } 243 | 244 | func (asc *mockAutoScalingClient) DescribeAutoScalingGroups(input *autoscaling.DescribeAutoScalingGroupsInput) (*autoscaling.DescribeAutoScalingGroupsOutput, error) { 245 | ASGs := []*autoscaling.Group{} 246 | for _, inName := range aws.StringValueSlice(input.AutoScalingGroupNames) { 247 | for _, Group := range asc.ASGs { 248 | if aws.StringValue(Group.AutoScalingGroupName) == inName { 249 | ASGs = append(ASGs, Group) 250 | break 251 | } 252 | } 253 | } 254 | out := &autoscaling.DescribeAutoScalingGroupsOutput{ 255 | AutoScalingGroups: ASGs, 256 | } 257 | return out, asc.Err 258 | } 259 | -------------------------------------------------------------------------------- /pkg/kube/structured/structured_helper.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package structured 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "time" 21 | 22 | "github.com/keikoproj/kubedog/internal/util" 23 | "github.com/keikoproj/kubedog/pkg/kube/common" 24 | "github.com/pkg/errors" 25 | log "github.com/sirupsen/logrus" 26 | appsv1 "k8s.io/api/apps/v1" 27 | corev1 "k8s.io/api/core/v1" 28 | networkingv1 "k8s.io/api/networking/v1" 29 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | "k8s.io/client-go/kubernetes" 31 | ) 32 | 33 | func GetNodeList(kubeClientset kubernetes.Interface) (*corev1.NodeList, error) { 34 | if err := common.ValidateClientset(kubeClientset); err != nil { 35 | return nil, err 36 | } 37 | 38 | nodes, err := util.RetryOnError(&util.DefaultRetry, util.IsRetriable, func() (interface{}, error) { 39 | return kubeClientset.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{}) 40 | }) 41 | if err != nil { 42 | return nil, errors.Wrap(err, "failed to list nodes") 43 | } 44 | 45 | return nodes.(*corev1.NodeList), nil 46 | } 47 | 48 | func GetDaemonSet(kubeClientset kubernetes.Interface, name, namespace string) (*appsv1.DaemonSet, error) { 49 | if err := common.ValidateClientset(kubeClientset); err != nil { 50 | return nil, err 51 | } 52 | 53 | ds, err := util.RetryOnError(&util.DefaultRetry, util.IsRetriable, func() (interface{}, error) { 54 | return kubeClientset.AppsV1().DaemonSets(namespace).Get(context.Background(), name, metav1.GetOptions{}) 55 | }) 56 | if err != nil { 57 | return nil, errors.Wrap(err, "failed to get daemonset") 58 | } 59 | return ds.(*appsv1.DaemonSet), nil 60 | } 61 | 62 | func GetDeployment(kubeClientset kubernetes.Interface, name, namespace string) (*appsv1.Deployment, error) { 63 | if err := common.ValidateClientset(kubeClientset); err != nil { 64 | return nil, err 65 | } 66 | 67 | deploy, err := util.RetryOnError(&util.DefaultRetry, util.IsRetriable, func() (interface{}, error) { 68 | return kubeClientset.AppsV1().Deployments(namespace).Get(context.Background(), name, metav1.GetOptions{}) 69 | }) 70 | if err != nil { 71 | return nil, errors.Wrap(err, "failed to get deployment") 72 | } 73 | return deploy.(*appsv1.Deployment), nil 74 | } 75 | 76 | func GetConfigMap(kubeClientset kubernetes.Interface, name, namespace string) (*corev1.ConfigMap, error) { 77 | configmaps, err := kubeClientset.CoreV1().ConfigMaps(namespace).Get(context.Background(), name, metav1.GetOptions{}) 78 | if err != nil || configmaps.Name != name { 79 | return nil, errors.Wrap(err, "failed to get configmap") 80 | } 81 | 82 | return configmaps, nil 83 | } 84 | 85 | func GetPersistentVolume(kubeClientset kubernetes.Interface, name string) (*corev1.PersistentVolume, error) { 86 | if err := common.ValidateClientset(kubeClientset); err != nil { 87 | return nil, err 88 | } 89 | 90 | pvs, err := util.RetryOnError(&util.DefaultRetry, util.IsRetriable, func() (interface{}, error) { 91 | return kubeClientset.CoreV1().PersistentVolumes().Get(context.Background(), name, metav1.GetOptions{}) 92 | }) 93 | if err != nil { 94 | return nil, errors.Wrap(err, "failed to get persistentvolume") 95 | } 96 | return pvs.(*corev1.PersistentVolume), nil 97 | } 98 | 99 | func GetPersistentVolumeClaim(kubeClientset kubernetes.Interface, name string, namespace string) (*corev1.PersistentVolumeClaim, error) { 100 | if err := common.ValidateClientset(kubeClientset); err != nil { 101 | return nil, err 102 | } 103 | 104 | pvc, err := util.RetryOnError(&util.DefaultRetry, util.IsRetriable, func() (interface{}, error) { 105 | return kubeClientset.CoreV1().PersistentVolumeClaims(namespace).Get(context.Background(), name, metav1.GetOptions{}) 106 | }) 107 | if err != nil { 108 | return nil, errors.Wrap(err, "failed to get persistentvolumeclaim") 109 | } 110 | return pvc.(*corev1.PersistentVolumeClaim), nil 111 | } 112 | 113 | func GetStatefulSetList(kubeClientset kubernetes.Interface, namespace string) (*appsv1.StatefulSetList, error) { 114 | if err := common.ValidateClientset(kubeClientset); err != nil { 115 | return nil, err 116 | } 117 | 118 | sts, err := util.RetryOnError(&util.DefaultRetry, util.IsRetriable, func() (interface{}, error) { 119 | return kubeClientset.AppsV1().StatefulSets(namespace).List(context.Background(), metav1.ListOptions{}) 120 | }) 121 | if err != nil { 122 | return nil, errors.Wrap(err, "failed to list statefulsets") 123 | } 124 | return sts.(*appsv1.StatefulSetList), nil 125 | } 126 | 127 | func GetPersistentVolumeList(kubeClientset kubernetes.Interface) (*corev1.PersistentVolumeList, error) { 128 | if err := common.ValidateClientset(kubeClientset); err != nil { 129 | return nil, err 130 | } 131 | 132 | pvs, err := util.RetryOnError(&util.DefaultRetry, util.IsRetriable, func() (interface{}, error) { 133 | return kubeClientset.CoreV1().PersistentVolumes().List(context.Background(), metav1.ListOptions{}) 134 | }) 135 | if err != nil { 136 | return nil, errors.Wrap(err, "failed to list persistentvolumes") 137 | } 138 | return pvs.(*corev1.PersistentVolumeList), nil 139 | } 140 | 141 | func GetIngress(kubeClientset kubernetes.Interface, name, namespace string) (*networkingv1.Ingress, error) { 142 | if err := common.ValidateClientset(kubeClientset); err != nil { 143 | return nil, err 144 | } 145 | 146 | ingress, err := util.RetryOnError(&util.DefaultRetry, util.IsRetriable, func() (interface{}, error) { 147 | return kubeClientset.NetworkingV1().Ingresses(namespace).Get(context.Background(), name, metav1.GetOptions{}) 148 | }) 149 | if err != nil { 150 | return nil, errors.Wrapf(err, "failed to get ingress '%v'", name) 151 | } 152 | return ingress.(*networkingv1.Ingress), nil 153 | } 154 | 155 | // TODO: remove use of service.beta.kubernetes.io/aws-load-balancer-subnets or make generic 156 | func GetIngressEndpoint(kubeClientset kubernetes.Interface, w common.WaiterConfig, name, namespace string, port int, path string) (string, error) { 157 | var ( 158 | counter int 159 | ) 160 | for { 161 | log.Info("waiting for ingress availability") 162 | if counter >= w.GetTries() { 163 | return "", errors.New("waiter timed out waiting for resource state") 164 | } 165 | ingress, err := GetIngress(kubeClientset, name, namespace) 166 | if err != nil { 167 | return "", err 168 | } 169 | annotations := ingress.GetAnnotations() 170 | albSubnets := annotations["service.beta.kubernetes.io/aws-load-balancer-subnets"] 171 | log.Infof("Alb IngressSubnets associated are: %v", albSubnets) 172 | var ingressReconciled bool 173 | ingressStatus := ingress.Status.LoadBalancer.Ingress 174 | if ingressStatus == nil { 175 | log.Infof("ingress %v/%v is not ready yet", namespace, name) 176 | } else { 177 | ingressReconciled = true 178 | } 179 | if ingressReconciled { 180 | hostname := ingressStatus[0].Hostname 181 | endpoint := fmt.Sprintf("http://%v:%v%v", hostname, port, path) 182 | return endpoint, nil 183 | } 184 | counter++ 185 | time.Sleep(w.GetInterval()) 186 | } 187 | } 188 | 189 | // TODO: This is hardcoded based on prometheus names in IKS clusters. Might be worth making it more generic in the future 190 | func validatePrometheusPVLabels(kubeClientset kubernetes.Interface, volumeClaimTemplatesName string) error { 191 | // Get prometheus PersistentVolume list 192 | pv, err := GetPersistentVolumeList(kubeClientset) 193 | if err != nil { 194 | log.Fatal(err) 195 | } 196 | for _, item := range pv.Items { 197 | pvcname := item.Spec.ClaimRef.Name 198 | if pvcname == volumeClaimTemplatesName+"-prometheus-k8s-prometheus-0" || pvcname == volumeClaimTemplatesName+"-prometheus-k8s-prometheus-1" { 199 | if k1, k2 := item.Labels["failure-domain.beta.kubernetes.io/zone"], item.Labels["topology.kubernetes.io/zone"]; k1 == "" && k2 == "" { 200 | return errors.Errorf("Prometheus volumes does not exist label - kubernetes.io/zone") 201 | } 202 | } 203 | } 204 | return nil 205 | } 206 | 207 | func isNodeReady(n corev1.Node) bool { 208 | for _, condition := range n.Status.Conditions { 209 | if condition.Type == "Ready" { 210 | if condition.Status == "True" { 211 | return true 212 | } 213 | } 214 | } 215 | return false 216 | } 217 | -------------------------------------------------------------------------------- /docs/syntax.md: -------------------------------------------------------------------------------- 1 | # Syntax 2 | 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: 3 | 4 | ## Generic steps 5 | - ` [I] wait [for] (minutes|seconds)` generic.WaitFor 6 | - ` the command is available` generic.CommandExists 7 | - ` I run the command with the args and the command (fails|succeeds)` generic.RunCommand 8 | 9 | ## Kubernetes steps 10 | - ` ([a] Kubernetes cluster|[there are] [valid] Kubernetes Credentials)` kdt.KubeClientSet.DiscoverClients 11 | - ` [the] Kubernetes cluster should be (created|deleted|upgraded)` kdt.KubeClientSet.KubernetesClusterShouldBe 12 | - ` [I] store [the] current time as ` kdt.KubeClientSet.SetTimestamp 13 | 14 | ### Unstructured Resources 15 | - ` [I] (create|submit|delete|update|upsert) [the] resource ` kdt.KubeClientSet.ResourceOperation 16 | - ` [I] (create|submit|delete|update|upsert) [the] resource in [the] namespace` kdt.KubeClientSet.ResourceOperationInNamespace 17 | - ` [I] (create|submit|delete|update|upsert) [the] resources in ` kdt.KubeClientSet.ResourcesOperation 18 | - ` [I] (create|submit|delete|update|upsert) [the] resources in in [the] namespace` kdt.KubeClientSet.ResourcesOperationInNamespace 19 | - ` [I] (create|submit|delete|update|upsert) [the] resource , the operation should (succeed|fail)` kdt.KubeClientSet.ResourceOperationWithResult 20 | - ` [I] (create|submit|delete|update|upsert) [the] resource in [the] namespace, the operation should (succeed|fail)` kdt.KubeClientSet.ResourceOperationWithResultInNamespace 21 | - ` [the] resource should be (created|deleted)` kdt.KubeClientSet.ResourceShouldBe 22 | - ` [the] resource [should] converge to selector ` kdt.KubeClientSet.ResourceShouldConvergeToSelector 23 | - ` [the] resource [should] converge to field ` kdt.KubeClientSet.ResourceShouldConvergeToField 24 | - ` [the] resource condition should be ` kdt.KubeClientSet.ResourceConditionShouldBe 25 | - ` [I] update [the] resource with set to ` kdt.KubeClientSet.UpdateResourceWithField 26 | - ` [I] verify InstanceGroups [are] in "ready" state` kdt.KubeClientSet.VerifyInstanceGroups 27 | 28 | ### Structured Resources 29 | 30 | #### Pods 31 | - ` [I] (get|list|delete) [the] pods in namespace ` kdt.KubeClientSet.PodOperation 32 | - ` [I] (get|list|delete) [the] pods in namespace with selector ` kdt.KubeClientSet.PodOperationWithSelector 33 | - ` [I] delete [the] pods in namespace with field selector ` kdt.KubeClientSet.DeletePodWithFieldSelector 34 | - ` [the] pods in namespace with selector have restart count less than ` kdt.KubeClientSet.PodsWithSelectorHaveRestartCountLessThan 35 | - ` (some|all) pods in namespace with selector have "" in logs since time` kdt.KubeClientSet.SomeOrAllPodsInNamespaceWithSelectorHaveStringInLogsSinceTime 36 | - ` some pods in namespace with selector don't have "" in logs since time` kdt.KubeClientSet.SomePodsInNamespaceWithSelectorDontHaveStringInLogsSinceTime 37 | - ` [the] pods in namespace with selector have no errors in logs since time` kdt.KubeClientSet.PodsInNamespaceWithSelectorHaveNoErrorsInLogsSinceTime 38 | - ` [the] pods in namespace with selector have some errors in logs since time` kdt.KubeClientSet.PodsInNamespaceWithSelectorHaveSomeErrorsInLogsSinceTime 39 | - ` [all] [the] (pod|pods) in [the] namespace with [the] label selector [should] (converge to|have) [the] field selector ` kdt.KubeClientSet.PodsInNamespaceWithLabelSelectorConvergeToFieldSelector 40 | - ` [the] pods in namespace with selector should have labels ` kdt.KubeClientSet.PodsInNamespaceWithSelectorShouldHaveLabels 41 | - ` [the] pod in namespace should have labels ` kdt.KubeClientSet.PodInNamespaceShouldHaveLabels 42 | 43 | #### Others 44 | - ` [I] (create|submit|update) [the] secret in namespace from [environment variable] ` kdt.KubeClientSet.SecretOperationFromEnvironmentVariable 45 | - ` [I] delete [the] secret in namespace ` kdt.KubeClientSet.SecretDelete 46 | - ` node[s] with selector should be (found|ready)` kdt.KubeClientSet.NodesWithSelectorShouldBe 47 | - ` [the] (deployment|hpa|horizontalpodautoscaler|service|pdb|poddisruptionbudget|sa|serviceaccount|configmap) (is|is not) in namespace ` kdt.KubeClientSet.ResourceInNamespace 48 | - ` [I] scale [the] deployment in namespace to ` kdt.KubeClientSet.ScaleDeployment 49 | - ` [I] validate Prometheus Statefulset in namespace has volumeClaimTemplates name ` kdt.KubeClientSet.ValidatePrometheusVolumeClaimTemplatesName 50 | - ` [I] get [the] nodes list` kdt.KubeClientSet.ListNodes 51 | - ` [the] daemonset is running in namespace ` kdt.KubeClientSet.DaemonSetIsRunning 52 | - ` [the] deployment is running in namespace ` kdt.KubeClientSet.DeploymentIsRunning 53 | - ` [the] data in [the] ConfigMap "" in namespace "" has key "" with value ""` kdt.KubeClientSet.ConfigMapDataHasKeyAndValue 54 | - ` [the] persistentvolume exists with status (Available|Bound|Released|Failed|Pending)` kdt.KubeClientSet.PersistentVolExists 55 | - ` [the] persistentvolumeclaim exists with status (Available|Bound|Released|Failed|Pending) in namespace ` kdt.KubeClientSet.PersistentVolClaimExists 56 | - ` [the] (clusterrole|clusterrolebinding) with name should be found` kdt.KubeClientSet.ClusterRbacIsFound 57 | - ` [the] ingress in [the] namespace [is] [available] on port and path ` kdt.KubeClientSet.IngressAvailable 58 | - ` [I] send tps to ingress in [the] namespace [available] on port and path for (minutes|seconds) expecting up to error[s]` kdt.KubeClientSet.SendTrafficToIngress 59 | 60 | ## AWS steps 61 | - ` [there are] [valid] AWS Credentials` kdt.AwsClientSet.DiscoverClients 62 | - ` an Auto Scaling Group named ` kdt.AwsClientSet.AnASGNamed 63 | - ` [I] update [the] current Auto Scaling Group with set to ` kdt.AwsClientSet.UpdateFieldOfCurrentASG 64 | - ` [the] current Auto Scaling Group [is] scaled to (min, max) = (, )` kdt.AwsClientSet.ScaleCurrentASG 65 | - ` [the] DNS name (should|should not) be created in hostedZoneID ` kdt.AwsClientSet.DnsNameShouldOrNotInHostedZoneID 66 | - ` [I] (add|remove) [the] role as trusted entity to iam role ` kdt.AwsClientSet.IamRoleTrust 67 | - ` [I] (add|remove) cluster shared iam role` kdt.AwsClientSet.ClusterSharedIamOperation 68 | -------------------------------------------------------------------------------- /generate/syntax/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package main 16 | 17 | import ( 18 | "bufio" 19 | "fmt" 20 | "os" 21 | "strconv" 22 | "strings" 23 | 24 | "github.com/keikoproj/kubedog/generate/syntax/replace" 25 | log "github.com/sirupsen/logrus" 26 | ) 27 | 28 | const ( 29 | sourceFilePath = "kubedog.go" 30 | destinationFilePath = "docs/syntax.md" 31 | newLine = "\n" 32 | actionIndicator = "//syntax-generation" 33 | actionDelimiter = ":" 34 | actionBegin = "begin" 35 | actionEnd = "end" 36 | actionTitle = "title" 37 | titleDelimiter = "-" 38 | titleRankStep = "#" 39 | processedTitleBeginning = "## " 40 | processedStepBeginning = "- " 41 | stepIndicator = "kdt.scenario.Step" 42 | stepDelimiter = "`" 43 | stepPrefix = "^" 44 | stepSuffix = "$" 45 | methodPrefix = "," 46 | methodSuffix = ")" 47 | markdownCodeDelimiter = "`" 48 | gherkinKeyword = "" 49 | 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 50 | ) 51 | 52 | var replacements = replace.Replacements{ 53 | {Replacee: `(\d+)`, Replacer: ``}, 54 | {Replacee: `(\S+)`, Replacer: ``}, 55 | {Replacee: `([^"]*)`, Replacer: ``}, 56 | } 57 | 58 | var bracketsReplacements = replace.BracketsReplacements{ 59 | { 60 | Opening: replace.Replacement{ 61 | Replacee: `(?:`, Replacer: `[`}, 62 | Closing: replace.Replacement{ 63 | Replacee: ` )?`, Replacer: `] `}, 64 | }, 65 | { 66 | Opening: replace.Replacement{ 67 | Replacee: `(?:`, Replacer: `[`}, 68 | Closing: replace.Replacement{ 69 | Replacee: `)?`, Replacer: `]`}, 70 | }, 71 | { 72 | Opening: replace.Replacement{ 73 | Replacee: `(?:`, Replacer: `(`}, 74 | Closing: replace.Replacement{ 75 | Replacee: `)`, Replacer: `)`}, 76 | }, 77 | { 78 | Opening: replace.Replacement{ 79 | Replacee: `\(`, Replacer: `(`}, 80 | Closing: replace.Replacement{ 81 | Replacee: `\)`, Replacer: `)`}, 82 | }, 83 | } 84 | 85 | func main() { 86 | sourceFile, err := os.Open(sourceFilePath) 87 | if err != nil { 88 | log.Error(err) 89 | } 90 | defer sourceFile.Close() 91 | fileScanner := bufio.NewScanner(sourceFile) 92 | fileScanner.Split(bufio.ScanLines) 93 | rawSyntax := []string{} 94 | for fileScanner.Scan() { 95 | line := fileScanner.Text() 96 | if isBeginAction(line) { 97 | log.Debugf("found begin action as '%s'", line) 98 | for fileScanner.Scan() { 99 | line := fileScanner.Text() 100 | if isEndAction(line) { 101 | log.Debugf("found end action as '%s'", line) 102 | break 103 | } 104 | rawSyntax = append(rawSyntax, line) 105 | } 106 | break 107 | } 108 | } 109 | log.Infof("found raw syntax to process as:") 110 | printStringSlice(rawSyntax) 111 | processedSyntax := processSyntax(rawSyntax) 112 | createSyntaxDocumentation(processedSyntax) 113 | } 114 | 115 | func createSyntaxDocumentation(processedSyntax []string) { 116 | if err := os.Remove(destinationFilePath); err != nil { 117 | log.Fatal(err) 118 | } 119 | f, err := os.Create(destinationFilePath) 120 | if err != nil { 121 | log.Fatal(err) 122 | } 123 | log.Infof("writing to '%s'", destinationFilePath) 124 | if _, err := f.WriteString(destinationFileBeginning); err != nil { 125 | log.Fatal(err) 126 | } 127 | for _, processedLine := range processedSyntax { 128 | if _, err := f.WriteString(processedLine); err != nil { 129 | log.Fatal(err) 130 | } 131 | } 132 | f.Close() 133 | printFile(destinationFilePath) 134 | } 135 | 136 | func processSyntax(rawSyntax []string) []string { 137 | processedSyntax := []string{} 138 | for _, rawLine := range rawSyntax { 139 | switch { 140 | case strings.Contains(rawLine, actionIndicator): 141 | title, titleRank := mustGetTitle(rawLine) 142 | titleBeginning := getTitleProcessedRank(titleRank) 143 | processedTitle := newLine + titleBeginning + title + newLine 144 | log.Debugf("processed '%s' as: '%s'", rawLine, processedTitle) 145 | processedSyntax = append(processedSyntax, processedTitle) 146 | case strings.Contains(rawLine, stepIndicator): 147 | processedStep := processedStepBeginning + processStep(rawLine) + newLine 148 | log.Debugf("processed '%s' as: '%s'", rawLine, processedStep) 149 | processedSyntax = append(processedSyntax, processedStep) 150 | } 151 | } 152 | return processedSyntax 153 | } 154 | 155 | func processStep(rawStep string) string { 156 | if !strings.Contains(rawStep, stepIndicator) { 157 | log.Fatalf("expected '%s' to contain '%s'", rawStep, stepIndicator) 158 | } 159 | rawStepSplit := strings.Split(rawStep, stepDelimiter) 160 | if len(rawStepSplit) != 3 { 161 | log.Fatalf("expected '%s' to meet format '%s(%s%s, )'", rawStep, stepIndicator, stepDelimiter, stepDelimiter) 162 | } 163 | processedStep := rawStepSplit[1] 164 | processedStep = strings.TrimPrefix(processedStep, stepPrefix) 165 | processedStep = strings.TrimSuffix(processedStep, stepSuffix) 166 | processedStep = replacements.Replace(processedStep) 167 | processedStep = bracketsReplacements.Replace(processedStep) 168 | method := rawStepSplit[2] 169 | method = strings.TrimPrefix(method, methodPrefix) 170 | method = strings.TrimSuffix(method, methodSuffix) 171 | return markdownCodeDelimiter + gherkinKeyword + " " + processedStep + markdownCodeDelimiter + method 172 | } 173 | 174 | func mustGetTitle(line string) (string, int) { 175 | action, afterAction := getAction(line) 176 | actionSplit := strings.Split(action, titleDelimiter) 177 | if len(actionSplit) != 2 { 178 | log.Fatalf("expected '%s' to meet format '%s%s'", action, actionTitle, titleDelimiter) 179 | } 180 | action, titleRankString := actionSplit[0], actionSplit[1] 181 | if action != actionTitle { 182 | log.Fatalf("expected '%s' to contain '%s%s%s'", line, actionIndicator, actionDelimiter, actionTitle) 183 | } 184 | titleRank, err := strconv.Atoi(titleRankString) 185 | if err != nil { 186 | log.Fatalf("failed converting '%s' to integer: '%v'", titleRankString, err) 187 | } 188 | if afterAction == "" { 189 | log.Fatalf("expected '%s' to contain '%s%s%s%s'", line, actionIndicator, actionDelimiter, actionTitle, actionDelimiter) 190 | } 191 | return afterAction, titleRank 192 | } 193 | 194 | func getTitleProcessedRank(rank int) string { 195 | if rank < 0 || rank > 9 { 196 | log.Fatalf("expected '%d' to be a digit between 1 and 9)", rank) 197 | } 198 | totalRankString := strings.Repeat(titleRankStep, rank) 199 | return totalRankString + processedTitleBeginning 200 | } 201 | 202 | func isEndAction(line string) bool { 203 | return isAction(actionEnd, line) 204 | } 205 | 206 | func isBeginAction(line string) bool { 207 | return isAction(actionBegin, line) 208 | } 209 | 210 | func isAction(expectedAction, line string) bool { 211 | if expectedAction == "" { 212 | log.Fatal("expectedAction cant be empty") 213 | } 214 | action, _ := getAction(line) 215 | return action == expectedAction 216 | } 217 | 218 | func getAction(line string) (string, string) { 219 | if strings.Contains(line, actionIndicator) { 220 | lineSplit := strings.Split(line, actionDelimiter) 221 | if len(lineSplit) < 2 { 222 | log.Fatalf("expected '%s' to contain at least 2 elements, the actionIndicator '%s' and an action separated by '%s' but got '%v'", line, actionIndicator, actionDelimiter, lineSplit) 223 | } 224 | action := lineSplit[1] 225 | if len(lineSplit) > 2 { 226 | afterAction := lineSplit[2] 227 | if len(lineSplit) > 3 { 228 | log.Warnf("'%s' had more than 3 elements delimited by '%s'. took action '%s' and afterAction '%s' and ignored the rest", line, actionDelimiter, action, afterAction) 229 | } 230 | return action, afterAction 231 | } 232 | return action, "" 233 | } 234 | return "", "" 235 | } 236 | 237 | func printStringSlice(slice []string) { 238 | for _, s := range slice { 239 | fmt.Println(s) 240 | } 241 | } 242 | 243 | func printFile(path string) { 244 | file, err := os.ReadFile(path) 245 | if err != nil { 246 | log.Fatalf("failed reading '%s': '%v'", path, err) 247 | } 248 | fmt.Println(string(file)) 249 | } 250 | -------------------------------------------------------------------------------- /pkg/generic/template_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package generic 16 | 17 | import ( 18 | "os" 19 | "path/filepath" 20 | "testing" 21 | 22 | "github.com/onsi/gomega" 23 | ) 24 | 25 | func TestGetValue(t *testing.T) { 26 | var ( 27 | g = gomega.NewWithT(t) 28 | tests = []struct { 29 | templateArgument TemplateArgument 30 | setup func() 31 | expectedValue string 32 | expectError bool 33 | }{ 34 | // PositiveTests: 35 | { // Mandatory, EnvironmentVariable set 36 | templateArgument: TemplateArgument{ 37 | Key: "key1", 38 | EnvironmentVariable: "VAR1", 39 | Mandatory: true, 40 | Default: "fallback1", 41 | }, 42 | setup: func() { 43 | os.Setenv("VAR1", "value1") 44 | }, 45 | expectedValue: "value1", 46 | expectError: false, 47 | }, 48 | { // not Mandatory, EnvironmentVariable unset, Default not empty 49 | templateArgument: TemplateArgument{ 50 | Key: "key2", 51 | EnvironmentVariable: "VAR2", 52 | Mandatory: false, 53 | Default: "fallback2", 54 | }, 55 | setup: func() { 56 | os.Unsetenv("VAR2") 57 | }, 58 | expectedValue: "fallback2", 59 | expectError: false, 60 | }, 61 | { // not Mandatory, EnvironmentVariable set empty 62 | templateArgument: TemplateArgument{ 63 | Key: "key3", 64 | EnvironmentVariable: "VAR3", 65 | Mandatory: false, 66 | Default: "fallback3", 67 | }, 68 | setup: func() { 69 | os.Setenv("VAR3", "") 70 | }, 71 | expectedValue: "", 72 | expectError: false, 73 | }, 74 | { // not Mandatory, EnvironmentVariable unset, Default empty 75 | templateArgument: TemplateArgument{ 76 | Key: "key4", 77 | EnvironmentVariable: "VAR4", 78 | Mandatory: false, 79 | Default: "", 80 | }, 81 | setup: func() { 82 | os.Unsetenv("VAR4") 83 | }, 84 | expectedValue: "", 85 | expectError: false, 86 | }, 87 | { // not Mandatory, EnvironmentVariable empty 88 | templateArgument: TemplateArgument{ 89 | Key: "key5", 90 | EnvironmentVariable: "", 91 | Mandatory: false, 92 | Default: "fallback5", 93 | }, 94 | setup: func() {}, 95 | expectedValue: "fallback5", 96 | expectError: false, 97 | }, 98 | // NegativeTests: 99 | { // Mandatory, EnvironmentVariable unset 100 | templateArgument: TemplateArgument{ 101 | Key: "key", 102 | EnvironmentVariable: "VAR", 103 | Mandatory: true, 104 | Default: "fallback", 105 | }, 106 | setup: func() { 107 | os.Unsetenv("VAR") 108 | }, 109 | expectedValue: "", 110 | expectError: true, 111 | }, 112 | { // Key empty 113 | templateArgument: TemplateArgument{ 114 | Key: "", 115 | EnvironmentVariable: "VAR", 116 | Mandatory: true, 117 | Default: "fallback", 118 | }, 119 | setup: func() { 120 | os.Setenv("VAR", "value") 121 | }, 122 | expectedValue: "", 123 | expectError: true, 124 | }, 125 | { // Mandatory, EnvironmentVariable empty 126 | templateArgument: TemplateArgument{ 127 | Key: "key", 128 | EnvironmentVariable: "", 129 | Mandatory: true, 130 | Default: "fallback", 131 | }, 132 | setup: func() { 133 | os.Setenv("VAR", "value") 134 | }, 135 | expectedValue: "", 136 | expectError: true, 137 | }, 138 | } 139 | ) 140 | 141 | for _, test := range tests { 142 | test.setup() 143 | value, err := test.templateArgument.GetValue() 144 | if test.expectError { 145 | g.Expect(err).Should(gomega.HaveOccurred()) 146 | } else { 147 | g.Expect(err).ShouldNot(gomega.HaveOccurred()) 148 | } 149 | g.Expect(value).To(gomega.Equal(test.expectedValue)) 150 | } 151 | } 152 | 153 | func TestTemplateArgumentsToMap(t *testing.T) { 154 | var ( 155 | g = gomega.NewWithT(t) 156 | tests = []struct { 157 | templateArguments []TemplateArgument 158 | setup func() 159 | expectedArgs map[string]string 160 | expectError bool 161 | }{ 162 | { // PositiveTest 163 | templateArguments: []TemplateArgument{ 164 | { // Mandatory, EnvironmentVariable set 165 | Key: "key1", 166 | EnvironmentVariable: "VAR1", 167 | Mandatory: true, 168 | Default: "fallback1", 169 | }, 170 | { // not Mandatory, EnvironmentVariable unset, Default not empty 171 | Key: "key2", 172 | EnvironmentVariable: "VAR2", 173 | Mandatory: false, 174 | Default: "fallback2", 175 | }, 176 | { // not Mandatory, EnvironmentVariable set empty 177 | Key: "key3", 178 | EnvironmentVariable: "VAR3", 179 | Mandatory: false, 180 | Default: "fallback3", 181 | }, 182 | { // not Mandatory, EnvironmentVariable unset, Default empty 183 | Key: "key4", 184 | EnvironmentVariable: "VAR4", 185 | Mandatory: false, 186 | Default: "", 187 | }, 188 | { // not Mandatory, EnvironmentVariable empty 189 | Key: "key5", 190 | EnvironmentVariable: "", 191 | Mandatory: false, 192 | Default: "fallback5", 193 | }, 194 | }, 195 | setup: func() { 196 | os.Setenv("VAR1", "value1") 197 | os.Unsetenv("VAR2") 198 | os.Setenv("VAR3", "") 199 | os.Unsetenv("VAR4") 200 | }, 201 | expectedArgs: map[string]string{ 202 | "key1": "value1", 203 | "key2": "fallback2", 204 | "key3": "", 205 | "key4": "", 206 | "key5": "fallback5", 207 | }, 208 | expectError: false, 209 | }, 210 | } 211 | ) 212 | 213 | for _, test := range tests { 214 | test.setup() 215 | args, err := TemplateArgumentsToMap(test.templateArguments...) 216 | if test.expectError { 217 | g.Expect(err).Should(gomega.HaveOccurred()) 218 | } else { 219 | g.Expect(err).ShouldNot(gomega.HaveOccurred()) 220 | } 221 | g.Expect(args).To(gomega.Equal(test.expectedArgs)) 222 | } 223 | } 224 | 225 | func TestGenerateFileFromTemplate(t *testing.T) { 226 | type templateArgs struct { 227 | Kind string 228 | ApiVersion string 229 | Name string 230 | } 231 | 232 | fileToString := func(filePath string) string { 233 | file, _ := os.ReadFile(filePath) 234 | return string(file) 235 | } 236 | 237 | templateArgsToExpectedFileAsString := func(args templateArgs) string { 238 | return `kind: ` + args.Kind + ` 239 | apiVersion: ` + args.ApiVersion + ` 240 | metadata: 241 | name: ` + args.Name 242 | } 243 | 244 | var ( 245 | g = gomega.NewWithT(t) 246 | testTemplatesPath, _ = filepath.Abs("./test") 247 | tests = []struct { 248 | templatedFilePath string 249 | args templateArgs 250 | expectedFilePath string 251 | expectError bool 252 | }{ 253 | { // PositiveTest 254 | templatedFilePath: testTemplatesPath + "/templated.yaml", 255 | args: templateArgs{ 256 | Kind: "myKind", 257 | ApiVersion: "myApiVersion", 258 | Name: "myName", 259 | }, 260 | expectedFilePath: testTemplatesPath + "/generated_templated.yaml", 261 | expectError: false, 262 | }, 263 | { // NegativeTest: template.ParseFiles fails 264 | templatedFilePath: testTemplatesPath + "/wrong-file-name.yaml", 265 | args: templateArgs{ 266 | Kind: "myKind", 267 | ApiVersion: "myApiVersion", 268 | Name: "myName", 269 | }, 270 | expectedFilePath: "", 271 | expectError: true, 272 | }, 273 | { // NegativeTest: template.Execute fails 274 | templatedFilePath: testTemplatesPath + "/templated-bad-kind.yaml", 275 | args: templateArgs{ 276 | Kind: "myKind", 277 | ApiVersion: "myApiVersion", 278 | Name: "myName", 279 | }, 280 | expectedFilePath: "", 281 | expectError: true, 282 | }, 283 | } 284 | ) 285 | 286 | for _, test := range tests { 287 | generatedFilePath, err := GenerateFileFromTemplate(test.templatedFilePath, test.args) 288 | 289 | g.Expect(generatedFilePath).To(gomega.Equal(test.expectedFilePath)) 290 | 291 | if test.expectError { 292 | g.Expect(err).Should(gomega.HaveOccurred()) 293 | } else { 294 | g.Expect(err).ShouldNot(gomega.HaveOccurred()) 295 | 296 | generatedFileString := fileToString(generatedFilePath) 297 | expectedFileString := templateArgsToExpectedFileAsString(test.args) 298 | g.Expect(generatedFileString).To(gomega.Equal(expectedFileString)) 299 | } 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /kubedog.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package kubedog 16 | 17 | //go:generate go run generate/syntax/main.go 18 | import ( 19 | "github.com/cucumber/godog" 20 | aws "github.com/keikoproj/kubedog/pkg/aws" 21 | "github.com/keikoproj/kubedog/pkg/generic" 22 | "github.com/keikoproj/kubedog/pkg/kube" 23 | ) 24 | 25 | type Test struct { 26 | suite *godog.TestSuiteContext 27 | scenario *godog.ScenarioContext 28 | KubeClientSet kube.ClientSet 29 | AwsClientSet aws.ClientSet 30 | } 31 | 32 | /* 33 | SetScenario sets the ScenarioContext and contains the steps definition, should be called in the InitializeScenario function required by godog. 34 | Check https://github.com/keikoproj/kubedog/blob/master/docs/syntax.md for steps syntax details. 35 | */ 36 | func (kdt *Test) SetScenario(scenario *godog.ScenarioContext) { 37 | kdt.scenario = scenario 38 | //syntax-generation:begin 39 | //syntax-generation:title-0:Generic steps 40 | kdt.scenario.Step(`^(?:I )?wait (?:for )?(\d+) (minutes|seconds)$`, generic.WaitFor) 41 | kdt.scenario.Step(`^the (\S+) command is available$`, generic.CommandExists) 42 | kdt.scenario.Step(`^I run the (\S+) command with the ([^"]*) args and the command (fails|succeeds)$`, generic.RunCommand) 43 | //syntax-generation:title-0:Kubernetes steps 44 | kdt.scenario.Step(`^((?:a )?Kubernetes cluster|(?:there are )?(?:valid )?Kubernetes Credentials)$`, kdt.KubeClientSet.DiscoverClients) 45 | kdt.scenario.Step(`^(?:the )?Kubernetes cluster should be (created|deleted|upgraded)$`, kdt.KubeClientSet.KubernetesClusterShouldBe) 46 | kdt.scenario.Step(`^(?:I )?store (?:the )?current time as ([^"]*)$`, kdt.KubeClientSet.SetTimestamp) 47 | //syntax-generation:title-1:Unstructured Resources 48 | kdt.scenario.Step(`^(?:I )?(create|submit|delete|update|upsert) (?:the )?resource (\S+)$`, kdt.KubeClientSet.ResourceOperation) 49 | kdt.scenario.Step(`^(?:I )?(create|submit|delete|update|upsert) (?:the )?resource (\S+) in (?:the )?([^"]*) namespace$`, kdt.KubeClientSet.ResourceOperationInNamespace) 50 | kdt.scenario.Step(`^(?:I )?(create|submit|delete|update|upsert) (?:the )?resources in (\S+)$`, kdt.KubeClientSet.ResourcesOperation) 51 | kdt.scenario.Step(`^(?:I )?(create|submit|delete|update|upsert) (?:the )?resources in (\S+) in (?:the )?([^"]*) namespace$`, kdt.KubeClientSet.ResourcesOperationInNamespace) 52 | kdt.scenario.Step(`^(?:I )?(create|submit|delete|update|upsert) (?:the )?resource (\S+), the operation should (succeed|fail)$`, kdt.KubeClientSet.ResourceOperationWithResult) 53 | kdt.scenario.Step(`^(?:I )?(create|submit|delete|update|upsert) (?:the )?resource (\S+) in (?:the )?([^"]*) namespace, the operation should (succeed|fail)$`, kdt.KubeClientSet.ResourceOperationWithResultInNamespace) 54 | kdt.scenario.Step(`^(?:the )?resource ([^"]*) should be (created|deleted)$`, kdt.KubeClientSet.ResourceShouldBe) 55 | kdt.scenario.Step(`^(?:the )?resource (\S+) (?:should )?converge to selector (\S+)$`, kdt.KubeClientSet.ResourceShouldConvergeToSelector) 56 | kdt.scenario.Step(`^(?:the )?resource (\S+) (?:should )?converge to field (\S+)$`, kdt.KubeClientSet.ResourceShouldConvergeToField) 57 | kdt.scenario.Step(`^(?:the )?resource ([^"]*) condition ([^"]*) should be ([^"]*)$`, kdt.KubeClientSet.ResourceConditionShouldBe) 58 | kdt.scenario.Step(`^(?:I )?update (?:the )?resource ([^"]*) with ([^"]*) set to ([^"]*)$`, kdt.KubeClientSet.UpdateResourceWithField) 59 | kdt.scenario.Step(`^(?:I )?verify InstanceGroups (?:are )?in "ready" state$`, kdt.KubeClientSet.VerifyInstanceGroups) 60 | //syntax-generation:title-1:Structured Resources 61 | //syntax-generation:title-2:Pods 62 | kdt.scenario.Step(`^(?:I )?(get|list|delete) (?:the )?pods in namespace ([^"]*)$`, kdt.KubeClientSet.PodOperation) 63 | kdt.scenario.Step(`^(?:I )?(get|list|delete) (?:the )?pods in namespace ([^"]*) with selector (\S+)$`, kdt.KubeClientSet.PodOperationWithSelector) 64 | kdt.scenario.Step(`^(?:I )?delete (?:the )?pods in namespace ([^"]*) with field selector (\S+)$`, kdt.KubeClientSet.DeletePodWithFieldSelector) 65 | kdt.scenario.Step(`^(?:the )?pods in namespace ([^"]*) with selector (\S+) have restart count less than (\d+)$`, kdt.KubeClientSet.PodsWithSelectorHaveRestartCountLessThan) 66 | kdt.scenario.Step(`^(some|all) pods in namespace (\S+) with selector (\S+) have "([^"]*)" in logs since ([^"]*) time$`, kdt.KubeClientSet.SomeOrAllPodsInNamespaceWithSelectorHaveStringInLogsSinceTime) 67 | kdt.scenario.Step(`^some pods in namespace (\S+) with selector (\S+) don't have "([^"]*)" in logs since ([^"]*) time$`, kdt.KubeClientSet.SomePodsInNamespaceWithSelectorDontHaveStringInLogsSinceTime) 68 | kdt.scenario.Step(`^(?:the )?pods in namespace (\S+) with selector (\S+) have no errors in logs since ([^"]*) time$`, kdt.KubeClientSet.PodsInNamespaceWithSelectorHaveNoErrorsInLogsSinceTime) 69 | kdt.scenario.Step(`^(?:the )?pods in namespace (\S+) with selector (\S+) have some errors in logs since ([^"]*) time$`, kdt.KubeClientSet.PodsInNamespaceWithSelectorHaveSomeErrorsInLogsSinceTime) 70 | kdt.scenario.Step(`^(?:all )?(?:the )?(?:pod|pods) in (?:the )?namespace (\S+) with (?:the )?label selector (\S+) (?:should )?(?:converge to|have) (?:the )?field selector (\S+)$`, kdt.KubeClientSet.PodsInNamespaceWithLabelSelectorConvergeToFieldSelector) 71 | kdt.scenario.Step(`^(?:the )?pods in namespace (\S+) with selector (\S+) should have labels (\S+)$`, kdt.KubeClientSet.PodsInNamespaceWithSelectorShouldHaveLabels) 72 | kdt.scenario.Step(`^(?:the )?pod (\S+) in namespace (\S+) should have labels (\S+)$`, kdt.KubeClientSet.PodInNamespaceShouldHaveLabels) 73 | //syntax-generation:title-2:Others 74 | kdt.scenario.Step(`^(?:I )?(create|submit|update) (?:the )?secret (\S+) in namespace (\S+) from (?:environment variable )?(\S+)$`, kdt.KubeClientSet.SecretOperationFromEnvironmentVariable) 75 | kdt.scenario.Step(`^(?:I )?delete (?:the )?secret (\S+) in namespace (\S+)$`, kdt.KubeClientSet.SecretDelete) 76 | kdt.scenario.Step(`^(\d+) node(?:s)? with selector (\S+) should be (found|ready)$`, kdt.KubeClientSet.NodesWithSelectorShouldBe) 77 | kdt.scenario.Step(`^(?:the )?(deployment|hpa|horizontalpodautoscaler|service|pdb|poddisruptionbudget|sa|serviceaccount|configmap) ([^"]*) (is|is not) in namespace ([^"]*)$`, kdt.KubeClientSet.ResourceInNamespace) 78 | kdt.scenario.Step(`^(?:I )?scale (?:the )?deployment ([^"]*) in namespace ([^"]*) to (\d+)$`, kdt.KubeClientSet.ScaleDeployment) 79 | kdt.scenario.Step(`^(?:I )?validate Prometheus Statefulset ([^"]*) in namespace ([^"]*) has volumeClaimTemplates name ([^"]*)$`, kdt.KubeClientSet.ValidatePrometheusVolumeClaimTemplatesName) 80 | kdt.scenario.Step(`^(?:I )?get (?:the )?nodes list$`, kdt.KubeClientSet.ListNodes) 81 | kdt.scenario.Step(`^(?:the )?daemonset ([^"]*) is running in namespace ([^"]*)$`, kdt.KubeClientSet.DaemonSetIsRunning) 82 | kdt.scenario.Step(`^(?:the )?deployment ([^"]*) is running in namespace ([^"]*)$`, kdt.KubeClientSet.DeploymentIsRunning) 83 | kdt.scenario.Step(`^(?:the )?data in (?:the )?ConfigMap "([^"]*)" in namespace "([^"]*)" has key "([^"]*)" with value "([^"]*)"$`, kdt.KubeClientSet.ConfigMapDataHasKeyAndValue) 84 | kdt.scenario.Step(`^(?:the )?persistentvolume ([^"]*) exists with status (Available|Bound|Released|Failed|Pending)$`, kdt.KubeClientSet.PersistentVolExists) 85 | kdt.scenario.Step(`^(?:the )?persistentvolumeclaim ([^"]*) exists with status (Available|Bound|Released|Failed|Pending) in namespace ([^"]*)$`, kdt.KubeClientSet.PersistentVolClaimExists) 86 | kdt.scenario.Step(`^(?:the )?(clusterrole|clusterrolebinding) with name ([^"]*) should be found$`, kdt.KubeClientSet.ClusterRbacIsFound) 87 | kdt.scenario.Step(`^(?:the )?ingress (\S+) in (?:the )?namespace (\S+) (?:is )?(?:available )?on port (\d+) and path ([^"]*)$`, kdt.KubeClientSet.IngressAvailable) 88 | kdt.scenario.Step(`^(?: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)?$`, kdt.KubeClientSet.SendTrafficToIngress) 89 | //syntax-generation:title-0:AWS steps 90 | kdt.scenario.Step(`^(?:there are )?(?:valid )?AWS Credentials$`, kdt.AwsClientSet.DiscoverClients) 91 | kdt.scenario.Step(`^an Auto Scaling Group named ([^"]*)$`, kdt.AwsClientSet.AnASGNamed) 92 | kdt.scenario.Step(`^(?:I )?update (?:the )?current Auto Scaling Group with ([^"]*) set to ([^"]*)$`, kdt.AwsClientSet.UpdateFieldOfCurrentASG) 93 | kdt.scenario.Step(`^(?:the )?current Auto Scaling Group (?:is )?scaled to \(min, max\) = \((\d+), (\d+)\)$`, kdt.AwsClientSet.ScaleCurrentASG) 94 | kdt.scenario.Step(`^(?:the )?DNS name (\S+) (should|should not) be created in hostedZoneID (\S+)$`, kdt.AwsClientSet.DnsNameShouldOrNotInHostedZoneID) 95 | kdt.scenario.Step(`^(?:I )?(add|remove) (?:the )?(\S+) role as trusted entity to iam role ([^"]*)$`, kdt.AwsClientSet.IamRoleTrust) 96 | kdt.scenario.Step(`^(?:I )?(add|remove) cluster shared iam role$`, kdt.AwsClientSet.ClusterSharedIamOperation) 97 | //syntax-generation:end 98 | } 99 | 100 | /* 101 | SetTestSuite sets the TestSuiteContext, should be use in the InitializeTestSuite function required by godog. 102 | */ 103 | func (kdt *Test) SetTestSuite(testSuite *godog.TestSuiteContext) { 104 | kdt.suite = testSuite 105 | } 106 | -------------------------------------------------------------------------------- /pkg/aws/aws.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package aws 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "net/url" 21 | "strconv" 22 | "strings" 23 | 24 | "github.com/aws/aws-sdk-go/aws" 25 | "github.com/aws/aws-sdk-go/aws/session" 26 | "github.com/aws/aws-sdk-go/service/autoscaling" 27 | "github.com/aws/aws-sdk-go/service/autoscaling/autoscalingiface" 28 | "github.com/aws/aws-sdk-go/service/eks" 29 | "github.com/aws/aws-sdk-go/service/eks/eksiface" 30 | "github.com/aws/aws-sdk-go/service/iam" 31 | "github.com/aws/aws-sdk-go/service/iam/iamiface" 32 | "github.com/aws/aws-sdk-go/service/route53" 33 | "github.com/aws/aws-sdk-go/service/route53/route53iface" 34 | "github.com/aws/aws-sdk-go/service/sts" 35 | "github.com/aws/aws-sdk-go/service/sts/stsiface" 36 | kIam "github.com/keikoproj/kubedog/pkg/aws/iam" 37 | "github.com/pkg/errors" 38 | log "github.com/sirupsen/logrus" 39 | ) 40 | 41 | type ClientSet struct { 42 | ASClient autoscalingiface.AutoScalingAPI 43 | EKSClient eksiface.EKSAPI 44 | Route53Client route53iface.Route53API 45 | IAMClient iamiface.IAMAPI 46 | STSClient stsiface.STSAPI 47 | asgName string 48 | launchConfigName string 49 | } 50 | 51 | func (c *ClientSet) DiscoverClients() error { 52 | var ( 53 | sess *session.Session 54 | identity *sts.GetCallerIdentityOutput 55 | err error 56 | ) 57 | 58 | if sess, err = session.NewSession(); err != nil { 59 | return err 60 | } 61 | 62 | svc := sts.New(sess) 63 | if identity, err = svc.GetCallerIdentity(&sts.GetCallerIdentityInput{}); err != nil { 64 | return err 65 | } 66 | arn := aws.StringValue(identity.Arn) 67 | log.Infof("Credentials: %v", arn) 68 | 69 | c.ASClient = autoscaling.New(sess) 70 | c.EKSClient = eks.New(sess) 71 | c.Route53Client = route53.New(sess) 72 | c.IAMClient = iam.New(sess) 73 | c.STSClient = sts.New(sess) 74 | 75 | return nil 76 | } 77 | 78 | func (c *ClientSet) AnASGNamed(name string) error { 79 | if c.ASClient == nil { 80 | return errors.Errorf("Unable to get ASG %v: The AS client was not found, use the method GetAWSCredsAndClients", name) 81 | } 82 | 83 | out, err := c.ASClient.DescribeAutoScalingGroups(&autoscaling.DescribeAutoScalingGroupsInput{ 84 | AutoScalingGroupNames: []*string{aws.String(name)}, 85 | }) 86 | if err != nil { 87 | return errors.Errorf("Failed describing the ASG %v: %v", name, err) 88 | } else if len(out.AutoScalingGroups) == 0 { 89 | return errors.Errorf("No ASG found by the name: '%s'", name) 90 | } 91 | 92 | arn := aws.StringValue(out.AutoScalingGroups[0].AutoScalingGroupARN) 93 | log.Infof("Auto Scaling group: %v", arn) 94 | 95 | c.launchConfigName = aws.StringValue(out.AutoScalingGroups[0].LaunchConfigurationName) 96 | c.asgName = name 97 | 98 | return nil 99 | } 100 | 101 | func (c *ClientSet) ScaleCurrentASG(desiredMin, desiredMax int64) error { 102 | 103 | if c.ASClient == nil { 104 | return errors.Errorf("Unable to scale currrent ASG: The AS client was not found, use the method GetAWSCredsAndClients") 105 | } 106 | 107 | _, err := c.ASClient.UpdateAutoScalingGroup(&autoscaling.UpdateAutoScalingGroupInput{ 108 | AutoScalingGroupName: aws.String(c.asgName), 109 | MinSize: aws.Int64(desiredMin), 110 | MaxSize: aws.Int64(desiredMax), 111 | }) 112 | if err != nil { 113 | return errors.Errorf("Failed scaling ASG %v: %v", c.asgName, err) 114 | } 115 | 116 | return nil 117 | } 118 | 119 | func (c *ClientSet) UpdateFieldOfCurrentASG(field, value string) error { 120 | var ( 121 | err error 122 | valueInt64 int64 123 | ) 124 | 125 | if c.ASClient == nil { 126 | return errors.Errorf("Unable to update current ASG: The AS client was not found, use the method GetAWSCredsAndClients") 127 | } 128 | 129 | if field == "LaunchConfigurationName" { 130 | _, err = c.ASClient.UpdateAutoScalingGroup(&autoscaling.UpdateAutoScalingGroupInput{ 131 | AutoScalingGroupName: aws.String(c.asgName), 132 | LaunchConfigurationName: aws.String(value), 133 | }) 134 | 135 | if err != nil { 136 | return errors.Errorf("Failed updating field %v to %v of ASG %v: %v", field, value, c.asgName, err) 137 | } 138 | return nil 139 | } 140 | 141 | valueInt64, err = strconv.ParseInt(value, 10, 64) 142 | if err != nil { 143 | return errors.Errorf("Failed to convert %v to int64 while trying to update field %v of ASG %v", value, field, c.asgName) 144 | } 145 | 146 | switch field { 147 | case "MinSize": 148 | _, err = c.ASClient.UpdateAutoScalingGroup(&autoscaling.UpdateAutoScalingGroupInput{ 149 | AutoScalingGroupName: aws.String(c.asgName), 150 | MinSize: aws.Int64(valueInt64), 151 | }) 152 | case "DesiredCapacity": 153 | _, err = c.ASClient.UpdateAutoScalingGroup(&autoscaling.UpdateAutoScalingGroupInput{ 154 | AutoScalingGroupName: aws.String(c.asgName), 155 | DesiredCapacity: aws.Int64(valueInt64), 156 | }) 157 | case "MaxSize": 158 | _, err = c.ASClient.UpdateAutoScalingGroup(&autoscaling.UpdateAutoScalingGroupInput{ 159 | AutoScalingGroupName: aws.String(c.asgName), 160 | MaxSize: aws.Int64(valueInt64), 161 | }) 162 | default: 163 | return errors.Errorf("Field %v is not supported, use LaunchConfigurationName, MinSize, DesiredCapacity or MaxSize", field) 164 | } 165 | 166 | if err != nil { 167 | return errors.Errorf("Failed updating field %v to %v of ASG %v: %v", field, value, c.asgName, err) 168 | } 169 | return nil 170 | } 171 | 172 | func (c *ClientSet) IamRoleTrust(action, entityName, roleName string) error { 173 | accountId := getAccountNumber(c.STSClient) 174 | clusterName, err := getClusterName() 175 | if err != nil { 176 | return err 177 | } 178 | 179 | // Add efs-csi-role-<clustername> as trusted entity 180 | var trustedEntityArn = fmt.Sprintf("arn:aws:iam::%s:role/%s-%s", 181 | accountId, entityName, clusterName) 182 | 183 | type StatementEntry struct { 184 | Effect string 185 | Action string 186 | Principal map[string]string 187 | } 188 | type PolicyDocument struct { 189 | Version string 190 | Statement []StatementEntry 191 | } 192 | newPolicyDoc := &PolicyDocument{ 193 | Version: "2012-10-17", 194 | Statement: make([]StatementEntry, 0), 195 | } 196 | 197 | role, err := kIam.GetIamRole(roleName, c.IAMClient) 198 | if err != nil { 199 | return err 200 | } 201 | 202 | if role.AssumeRolePolicyDocument != nil { 203 | doc := &PolicyDocument{} 204 | data, err := url.QueryUnescape(*role.AssumeRolePolicyDocument) 205 | if err != nil { 206 | return err 207 | } 208 | 209 | // parse existing policy 210 | err = json.Unmarshal([]byte(data), &doc) 211 | if err != nil { 212 | return err 213 | } 214 | 215 | if len(doc.Statement) > 0 { 216 | // loop through existing statements and keep valid trusted entities 217 | for _, stmnt := range doc.Statement { 218 | if val, ok := stmnt.Principal["AWS"]; ok { 219 | if strings.HasPrefix(val, "arn:aws:iam") && val != trustedEntityArn { 220 | newPolicyDoc.Statement = append(newPolicyDoc.Statement, stmnt) 221 | } 222 | } 223 | } 224 | } 225 | } 226 | 227 | switch action { 228 | case "add": 229 | newStatment := StatementEntry{ 230 | Effect: "Allow", 231 | Principal: map[string]string{ 232 | "AWS": trustedEntityArn, 233 | }, 234 | Action: "sts:AssumeRole", 235 | } 236 | 237 | newPolicyDoc.Statement = append(newPolicyDoc.Statement, newStatment) 238 | case "remove": 239 | // Do nothing, we already cleansed the trusted entity role above if we're not adding 240 | } 241 | 242 | policyJSON, err := json.Marshal(newPolicyDoc) 243 | if err != nil { 244 | return err 245 | } 246 | 247 | _, err = kIam.UpdateIAMAssumeRole(roleName, policyJSON, c.IAMClient) 248 | if err != nil { 249 | return err 250 | } 251 | 252 | return nil 253 | } 254 | 255 | func (c *ClientSet) ClusterSharedIamOperation(operation string) error { 256 | var ( 257 | accountId = getAccountNumber(c.STSClient) 258 | iamFmt = "arn:aws:iam::%s:%s/%s" 259 | ) 260 | clusterName, err := getClusterName() 261 | if err != nil { 262 | return err 263 | } 264 | roleName := fmt.Sprintf("shared.%s", clusterName) 265 | 266 | policyDocT := `{"Version":"2012-10-17","Statement":[{"Effect": "Allow", "Action": "sts:AssumeRole", "Resource": "%s"}]}` 267 | clusterSharedrole := fmt.Sprintf(iamFmt, accountId, "role", roleName) 268 | policyDocument := []byte(fmt.Sprintf(policyDocT, clusterSharedrole)) 269 | 270 | rootIAM := fmt.Sprintf("arn:aws:iam::%s:%s", accountId, "root") 271 | assumeRoleDoc := `{"Version":"2012-10-17","Statement":[{"Effect": "Allow", "Action": "sts:AssumeRole", "Principal": {"AWS": "%s"}}]}` 272 | roleDocument := []byte(fmt.Sprintf(assumeRoleDoc, rootIAM)) 273 | 274 | clusterSharedPolicy := fmt.Sprintf(iamFmt, accountId, "policy", roleName) 275 | switch operation { 276 | case "add": 277 | role, err := kIam.PutIAMRole(roleName, "shared cluster role", roleDocument, c.IAMClient) 278 | if err != nil { 279 | return errors.Wrap(err, "failed to create shared cluster role") 280 | } 281 | log.Infof("BDD >> created shared iam role: %s", aws.StringValue(role.Arn)) 282 | 283 | policy, err := kIam.PutManagedPolicy(roleName, clusterSharedPolicy, "shared cluster policy", policyDocument, c.IAMClient) 284 | if err != nil { 285 | return errors.Wrap(err, "failed to create shared cluster managed policy") 286 | } 287 | log.Infof("BDD >> created shared iam policy: %s", aws.StringValue(policy.Arn)) 288 | case "remove": 289 | err := kIam.DeleteManagedPolicy(clusterSharedPolicy, c.IAMClient) 290 | if err != nil { 291 | return errors.Wrap(err, "failed to delete shared cluster role") 292 | } 293 | 294 | err = kIam.DeleteIAMRole(roleName, c.IAMClient) 295 | if err != nil { 296 | return errors.Wrap(err, "failed to delete shared cluster managed policy") 297 | } 298 | } 299 | return nil 300 | } 301 | 302 | func (c *ClientSet) DnsNameShouldOrNotInHostedZoneID(dnsName, shouldOrNot, hostedZoneID string) error { 303 | switch shouldOrNot { 304 | case "should": 305 | return c.dnsNameInHostedZoneID(dnsName, hostedZoneID) 306 | 307 | case "should not": 308 | if err := c.dnsNameInHostedZoneID(dnsName, hostedZoneID); err == nil { 309 | return errors.Errorf("unexpected DNS %s exists in hostedZoneID %s", hostedZoneID, dnsName) 310 | } 311 | log.Infof("records for hostedZoneID %s with dnsName %s doesn't exists", hostedZoneID, dnsName) 312 | return nil 313 | default: 314 | return fmt.Errorf("invalid option '%s'. expected 'should' or 'should not'", shouldOrNot) 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /generate/syntax/replace/replace_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package replace 16 | 17 | import ( 18 | "reflect" 19 | "testing" 20 | ) 21 | 22 | func TestBracketsReplacement_Replace(t *testing.T) { 23 | type fields struct { 24 | Opening Replacement 25 | Closing Replacement 26 | } 27 | type args struct { 28 | src string 29 | } 30 | tests := []struct { 31 | name string 32 | fields fields 33 | args args 34 | want string 35 | }{ 36 | { 37 | name: "Positive Test: '(?:' & ' )?' Case 1", 38 | fields: fields{ 39 | Opening: Replacement{ 40 | Replacee: "(?:", 41 | Replacer: "[", 42 | }, 43 | Closing: Replacement{ 44 | Replacee: " )?", 45 | Replacer: "] ", 46 | }, 47 | }, 48 | args: args{ 49 | src: `(?:I )?wait (?:for )?(\d+) (minutes|seconds)`, 50 | }, 51 | want: `[I] wait [for] (\d+) (minutes|seconds)`, 52 | }, 53 | { 54 | name: "Positive Test: '(?:' & ' )?' Case 2", 55 | fields: fields{ 56 | Opening: Replacement{ 57 | Replacee: "(?:", 58 | Replacer: "[", 59 | }, 60 | Closing: Replacement{ 61 | Replacee: " )?", 62 | Replacer: "] ", 63 | }, 64 | }, 65 | args: args{ 66 | src: `(?:all )?(?:the )?(?:pod|pods) in (?:the )?namespace (\S+) with (?:the )?label selector (\S+) (?:should )?converge to (?:the )?field selector (\S+)`, 67 | }, 68 | want: `[all] [the] (?:pod|pods) in [the] namespace (\S+) with [the] label selector (\S+) [should] converge to [the] field selector (\S+)`, 69 | }, 70 | { 71 | name: "Positive Test: '(?:' & ')' Case 1", 72 | fields: fields{ 73 | Opening: Replacement{ 74 | Replacee: "(?:", 75 | Replacer: "(", 76 | }, 77 | Closing: Replacement{ 78 | Replacee: ")", 79 | Replacer: ")", 80 | }, 81 | }, 82 | args: args{ 83 | src: `[all] [the] (?:pod|pods) in [the] namespace (\S+) with [the] label selector (\S+) [should] converge to [the] field selector (\S+)`, 84 | }, 85 | want: `[all] [the] (pod|pods) in [the] namespace (\S+) with [the] label selector (\S+) [should] converge to [the] field selector (\S+)`, 86 | }, 87 | { 88 | name: "Positive Test: '(?:' & ' )?' Case 3", 89 | fields: fields{ 90 | Opening: Replacement{ 91 | Replacee: "(?:", 92 | Replacer: "[", 93 | }, 94 | Closing: Replacement{ 95 | Replacee: " )?", 96 | Replacer: "] ", 97 | }, 98 | }, 99 | args: args{ 100 | 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)?`, 101 | }, 102 | 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)?`, 103 | }, 104 | { 105 | name: "Positive Test: '(?:' & ')?' Case 1", 106 | fields: fields{ 107 | Opening: Replacement{ 108 | Replacee: "(?:", 109 | Replacer: "[", 110 | }, 111 | Closing: Replacement{ 112 | Replacee: ")?", 113 | Replacer: "]", 114 | }, 115 | }, 116 | args: args{ 117 | 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)?`, 118 | }, 119 | 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]`, 120 | }, 121 | } 122 | for _, tt := range tests { 123 | t.Run(tt.name, func(t *testing.T) { 124 | br := BracketsReplacement{ 125 | Opening: tt.fields.Opening, 126 | Closing: tt.fields.Closing, 127 | } 128 | if got := br.Replace(tt.args.src); got != tt.want { 129 | t.Errorf("BracketsReplacement.Replace() = %v, want %v", got, tt.want) 130 | } 131 | }) 132 | } 133 | } 134 | 135 | func TestBracketsReplacement_replaceSingle(t *testing.T) { 136 | type fields struct { 137 | Opening Replacement 138 | Closing Replacement 139 | } 140 | type args struct { 141 | src []byte 142 | } 143 | tests := []struct { 144 | name string 145 | fields fields 146 | args args 147 | want []byte 148 | }{ 149 | { 150 | name: "Positive Test", 151 | fields: fields{ 152 | Opening: Replacement{ 153 | Replacee: "(?:", 154 | Replacer: "(", 155 | }, 156 | Closing: Replacement{ 157 | Replacee: " )", 158 | Replacer: ")", 159 | }, 160 | }, 161 | args: args{ 162 | src: []byte("(?:I )"), 163 | }, 164 | want: []byte("(I)"), 165 | }, 166 | { 167 | name: "Positive Test", 168 | fields: fields{ 169 | Opening: Replacement{ 170 | Replacee: "(?:", 171 | Replacer: "(", 172 | }, 173 | Closing: Replacement{ 174 | Replacee: ")", 175 | Replacer: ")", 176 | }, 177 | }, 178 | args: args{ 179 | src: []byte("(?:pod|pods)"), 180 | }, 181 | want: []byte("(pod|pods)"), 182 | }, 183 | } 184 | for _, tt := range tests { 185 | t.Run(tt.name, func(t *testing.T) { 186 | br := BracketsReplacement{ 187 | Opening: tt.fields.Opening, 188 | Closing: tt.fields.Closing, 189 | } 190 | if got := br.replaceSingle(tt.args.src); !reflect.DeepEqual(got, tt.want) { 191 | t.Errorf("BracketsReplacement.replaceSingle() = %v, want %v", got, tt.want) 192 | } 193 | }) 194 | } 195 | } 196 | 197 | func TestBracketsReplacement_getRegExp(t *testing.T) { 198 | type fields struct { 199 | Opening Replacement 200 | Closing Replacement 201 | } 202 | tests := []struct { 203 | name string 204 | fields fields 205 | want string 206 | }{ 207 | { 208 | name: "Positive Test", 209 | fields: fields{ 210 | Opening: Replacement{ 211 | Replacee: "(?:", 212 | Replacer: "(", 213 | }, 214 | Closing: Replacement{ 215 | Replacee: " )", 216 | Replacer: ")", 217 | }, 218 | }, 219 | want: `\(\?\:` + regExp_CharsWithinBrackets + `\ \)`, 220 | }, 221 | } 222 | for _, tt := range tests { 223 | t.Run(tt.name, func(t *testing.T) { 224 | br := BracketsReplacement{ 225 | Opening: tt.fields.Opening, 226 | Closing: tt.fields.Closing, 227 | } 228 | if got := br.getRegExp(); got != tt.want { 229 | t.Errorf("BracketsReplacement.getRegExp() = %v, want %v", got, tt.want) 230 | } 231 | }) 232 | } 233 | } 234 | 235 | func Test_escapeEveryCharacter(t *testing.T) { 236 | type args struct { 237 | s string 238 | } 239 | tests := []struct { 240 | name string 241 | args args 242 | want string 243 | }{ 244 | { 245 | name: "Positive Test", 246 | args: args{ 247 | s: "(?:", 248 | }, 249 | want: `\(\?\:`, 250 | }, 251 | } 252 | for _, tt := range tests { 253 | t.Run(tt.name, func(t *testing.T) { 254 | if got := escapeEveryCharacter(tt.args.s); got != tt.want { 255 | t.Errorf("escapeEveryCharacter() = %v, want %v", got, tt.want) 256 | } 257 | }) 258 | } 259 | } 260 | 261 | func TestReplacement_Replace(t *testing.T) { 262 | type fields struct { 263 | Replacee string 264 | Replacer string 265 | } 266 | type args struct { 267 | src string 268 | } 269 | tests := []struct { 270 | name string 271 | fields fields 272 | args args 273 | want string 274 | }{ 275 | { 276 | name: "Positive Test", 277 | fields: fields{ 278 | Replacee: "replace-me", 279 | Replacer: "replaced-by-me", 280 | }, 281 | args: args{ 282 | src: "replace-me as many times as replace-me appears in a string containing replace-me", 283 | }, 284 | want: "replaced-by-me as many times as replaced-by-me appears in a string containing replaced-by-me", 285 | }, 286 | } 287 | for _, tt := range tests { 288 | t.Run(tt.name, func(t *testing.T) { 289 | r := Replacement{ 290 | Replacee: tt.fields.Replacee, 291 | Replacer: tt.fields.Replacer, 292 | } 293 | if got := r.Replace(tt.args.src); got != tt.want { 294 | t.Errorf("Replacement.Replace() = %v, want %v", got, tt.want) 295 | } 296 | }) 297 | } 298 | } 299 | 300 | func TestReplacements_Replace(t *testing.T) { 301 | type args struct { 302 | src string 303 | } 304 | tests := []struct { 305 | name string 306 | rs Replacements 307 | args args 308 | want string 309 | }{ 310 | { 311 | name: "Positive Test", 312 | rs: Replacements{ 313 | { 314 | Replacee: "replace-me-1", 315 | Replacer: "replaced-by-me-1", 316 | }, 317 | { 318 | Replacee: "replace-me-2", 319 | Replacer: "replaced-by-me-2", 320 | }, 321 | }, 322 | args: args{ 323 | src: `replace-me-1 as me times as replace-me-1 appears in a string containing replace-me-1, 324 | replace-me-2 as me times as replace-me-2 appears in a string containing replace-me-2`, 325 | }, 326 | want: `replaced-by-me-1 as me times as replaced-by-me-1 appears in a string containing replaced-by-me-1, 327 | replaced-by-me-2 as me times as replaced-by-me-2 appears in a string containing replaced-by-me-2`, 328 | }, 329 | } 330 | for _, tt := range tests { 331 | t.Run(tt.name, func(t *testing.T) { 332 | if got := tt.rs.Replace(tt.args.src); got != tt.want { 333 | t.Errorf("Replacements.Replace() = %v, want %v", got, tt.want) 334 | } 335 | }) 336 | } 337 | } 338 | 339 | func TestBracketsReplacements_Replace(t *testing.T) { 340 | type args struct { 341 | src string 342 | } 343 | tests := []struct { 344 | name string 345 | brs BracketsReplacements 346 | args args 347 | want string 348 | }{ 349 | { 350 | name: "Positive Test", 351 | brs: BracketsReplacements{ 352 | { 353 | Opening: Replacement{ 354 | Replacee: `(?:`, Replacer: `[`}, 355 | Closing: Replacement{ 356 | Replacee: ` )?`, Replacer: `] `}, 357 | }, 358 | { 359 | Opening: Replacement{ 360 | Replacee: `(?:`, Replacer: `[`}, 361 | Closing: Replacement{ 362 | Replacee: `)?`, Replacer: `]`}, 363 | }, 364 | { 365 | Opening: Replacement{ 366 | Replacee: `(?:`, Replacer: `(`}, 367 | Closing: Replacement{ 368 | Replacee: `)`, Replacer: `)`}, 369 | }, 370 | { 371 | Opening: Replacement{ 372 | Replacee: `\(`, Replacer: `(`}, 373 | Closing: Replacement{ 374 | Replacee: `\)`, Replacer: `)`}, 375 | }, 376 | }, 377 | args: args{ 378 | src: `(?:<something> )?(?:<something>)? (?:<something>) \(<something>\)`, 379 | }, 380 | want: "[<something>] [<something>] (<something>) (<something>)", 381 | }, 382 | } 383 | for _, tt := range tests { 384 | t.Run(tt.name, func(t *testing.T) { 385 | if got := tt.brs.Replace(tt.args.src); got != tt.want { 386 | t.Errorf("BracketsReplacements.Replace() = %v, want %v", got, tt.want) 387 | } 388 | }) 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 The KeikoProj Authors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /pkg/kube/pod/pod.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package pod 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "reflect" 21 | "strings" 22 | "time" 23 | 24 | "github.com/keikoproj/kubedog/internal/util" 25 | "github.com/keikoproj/kubedog/pkg/kube/common" 26 | "github.com/pkg/errors" 27 | log "github.com/sirupsen/logrus" 28 | corev1 "k8s.io/api/core/v1" 29 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | "k8s.io/apimachinery/pkg/util/wait" 31 | "k8s.io/client-go/kubernetes" 32 | ) 33 | 34 | func PodOperation(kubeClientset kubernetes.Interface, operation, namespace string) error { 35 | return PodOperationWithSelector(kubeClientset, operation, namespace, "") 36 | } 37 | 38 | func PodOperationWithSelector(kubeClientset kubernetes.Interface, operation, namespace, selector string) error { 39 | switch operation { 40 | case "list", "get": 41 | return ListPodsWithSelector(kubeClientset, namespace, selector) 42 | case "delete": 43 | return DeletePodsWithSelector(kubeClientset, namespace, selector) 44 | default: 45 | return errors.Errorf("Unknown pod operation '%s'", operation) 46 | } 47 | } 48 | 49 | func ListPods(kubeClientset kubernetes.Interface, namespace string) error { 50 | return ListPodsWithSelector(kubeClientset, namespace, "") 51 | } 52 | 53 | func ListPodsWithSelector(kubeClientset kubernetes.Interface, namespace, selector string) error { 54 | var readyCountFn = func(conditions []corev1.ContainerStatus) string { 55 | var readyCount = 0 56 | var containerCount = len(conditions) 57 | for _, condition := range conditions { 58 | if condition.Ready { 59 | readyCount++ 60 | } 61 | } 62 | return fmt.Sprintf("%d/%d", readyCount, containerCount) 63 | } 64 | pods, err := GetPodListWithLabelSelector(kubeClientset, namespace, selector) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | if len(pods.Items) == 0 { 70 | return errors.Errorf("No pods matched selector '%s'", selector) 71 | } 72 | tableFormat := "%-64s%-12s%-24s" 73 | log.Infof(tableFormat, "NAME", "READY", "STATUS") 74 | for _, pod := range pods.Items { 75 | log.Infof(tableFormat, pod.Name, readyCountFn(pod.Status.ContainerStatuses), pod.Status.Phase) 76 | } 77 | return nil 78 | } 79 | 80 | func DeletePodsWithSelector(kubeClientset kubernetes.Interface, namespace, selector string) error { 81 | err := DeletePodListWithLabelSelector(kubeClientset, namespace, selector) 82 | if err != nil { 83 | return err 84 | } 85 | log.Infof("Deleted pods with selector '%s' in namespace '%s'", selector, namespace) 86 | return nil 87 | } 88 | 89 | func DeletePodsWithFieldSelector(kubeClientset kubernetes.Interface, namespace, fieldSelector string) error { 90 | err := DeletePodListWithLabelSelectorAndFieldSelector(kubeClientset, namespace, "", fieldSelector) 91 | if err != nil { 92 | return err 93 | } 94 | log.Infof("Deleted pods with field selector '%s' in namespace '%s'", fieldSelector, namespace) 95 | return nil 96 | } 97 | 98 | func PodsWithSelectorHaveRestartCountLessThan(kubeClientset kubernetes.Interface, namespace string, selector string, restartCount int) error { 99 | pods, err := GetPodListWithLabelSelector(kubeClientset, namespace, selector) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | if len(pods.Items) == 0 { 105 | return errors.Errorf("No pods matched selector '%s'", selector) 106 | } 107 | 108 | for _, pod := range pods.Items { 109 | for _, containerStatus := range pod.Status.ContainerStatuses { 110 | log.Infof("Container '%s' of pod '%s' on node '%s' restarted %d times", 111 | containerStatus.Name, pod.Name, pod.Spec.NodeName, containerStatus.RestartCount) 112 | if int(containerStatus.RestartCount) >= restartCount { 113 | return errors.Errorf("Container '%s' of pod '%s' restarted %d times", 114 | containerStatus.Name, pod.Name, containerStatus.RestartCount) 115 | } 116 | } 117 | } 118 | 119 | return nil 120 | } 121 | 122 | func PodsInNamespaceWithLabelSelectorConvergeToFieldSelector(kubeClientset kubernetes.Interface, expBackoff wait.Backoff, namespace, labelSelector, fieldSelector string) error { 123 | return util.RetryOnAnyError(&expBackoff, func() error { 124 | podList, err := GetPodListWithLabelSelector(kubeClientset, namespace, labelSelector) 125 | if err != nil { 126 | return err 127 | } 128 | n := len(podList.Items) 129 | if n == 0 { 130 | return fmt.Errorf("no pods matched label selector '%s'", labelSelector) 131 | } 132 | log.Infof("found '%d' pods with label selector '%s'", n, labelSelector) 133 | 134 | podListWithSelector, err := GetPodListWithLabelSelectorAndFieldSelector(kubeClientset, namespace, labelSelector, fieldSelector) 135 | if err != nil { 136 | return err 137 | } 138 | m := len(podListWithSelector.Items) 139 | if m == 0 { 140 | return fmt.Errorf("no pods matched label selector '%s' and field selector '%s'", labelSelector, fieldSelector) 141 | } 142 | log.Infof("found '%d' pods with label selector '%s' and field selector '%s'", m, labelSelector, fieldSelector) 143 | 144 | message := fmt.Sprintf("'%d/%d' pod(s) with label selector '%s' converged to field selector '%s'", m, n, labelSelector, fieldSelector) 145 | if n != m { 146 | return errors.New(message) 147 | } 148 | podsUIDs := []string{} 149 | podsWithSelectorUIDs := []string{} 150 | for i := range podList.Items { 151 | podsUIDs = append(podsUIDs, string(podList.Items[i].UID)) 152 | podsWithSelectorUIDs = append(podsWithSelectorUIDs, string(podListWithSelector.Items[i].UID)) 153 | } 154 | if !reflect.DeepEqual(podsUIDs, podsWithSelectorUIDs) { 155 | 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) 156 | } 157 | log.Info(message) 158 | return nil 159 | }) 160 | } 161 | 162 | func SomeOrAllPodsInNamespaceWithSelectorHaveStringInLogsSinceTime(kubeClientset kubernetes.Interface, expBackoff wait.Backoff, SomeOrAll, namespace, selector, searchKeyword string, since time.Time) error { 163 | return util.RetryOnAnyError(&expBackoff, func() error { 164 | pods, err := GetPodListWithLabelSelector(kubeClientset, namespace, selector) 165 | if err != nil { 166 | return err 167 | } 168 | if len(pods.Items) == 0 { 169 | return fmt.Errorf("no pods matched selector '%s'", selector) 170 | } 171 | 172 | const ( 173 | somePodsKeyword = "some" 174 | allPodsKeyword = "all" 175 | ) 176 | var podsCount int 177 | for _, pod := range pods.Items { 178 | podCount, err := countStringInPodLogs(kubeClientset, pod, since, searchKeyword) 179 | if err != nil { 180 | return err 181 | } 182 | podsCount += podCount 183 | switch SomeOrAll { 184 | case somePodsKeyword: 185 | if podCount != 0 { 186 | log.Infof("'%s' pods required to have string in logs. pod '%s' has string '%s' in logs", somePodsKeyword, pod.Name, searchKeyword) 187 | return nil 188 | } 189 | case allPodsKeyword: 190 | if podCount == 0 { 191 | return fmt.Errorf("'%s' pods required to have string in logs. pod '%s' does not have string '%s' in logs", allPodsKeyword, pod.Name, searchKeyword) 192 | } 193 | default: 194 | return fmt.Errorf("wrong input as '%s', expected '(%s|%s)'", SomeOrAll, somePodsKeyword, allPodsKeyword) 195 | } 196 | } 197 | if podsCount == 0 { 198 | return fmt.Errorf("pods in namespace '%s' with selector '%s' do not have string '%s' in logs", namespace, selector, searchKeyword) 199 | } 200 | return nil 201 | }) 202 | } 203 | 204 | func SomePodsInNamespaceWithSelectorDontHaveStringInLogsSinceTime(kubeClientset kubernetes.Interface, namespace, selector, searchkeyword string, since time.Time) error { 205 | pods, err := GetPodListWithLabelSelector(kubeClientset, namespace, selector) 206 | if err != nil { 207 | return err 208 | } 209 | 210 | if len(pods.Items) == 0 { 211 | return errors.Errorf("No pods matched selector '%s'", selector) 212 | } 213 | for _, pod := range pods.Items { 214 | count, err := countStringInPodLogs(kubeClientset, pod, since, searchkeyword) 215 | if err != nil { 216 | return err 217 | } 218 | if count == 0 { 219 | return nil 220 | } 221 | } 222 | return fmt.Errorf("pod has '%s' message in the logs", searchkeyword) 223 | } 224 | 225 | func PodsInNamespaceWithSelectorHaveNoErrorsInLogsSinceTime(kubeClientset kubernetes.Interface, namespace string, selector string, since time.Time) error { 226 | pods, err := GetPodListWithLabelSelector(kubeClientset, namespace, selector) 227 | if err != nil { 228 | return err 229 | } 230 | if len(pods.Items) == 0 { 231 | return errors.Errorf("No pods matched selector '%s'", selector) 232 | } 233 | 234 | for _, pod := range pods.Items { 235 | errorStrings := []string{`"level":"error"`, "level=error"} 236 | count, err := countStringInPodLogs(kubeClientset, pod, since, errorStrings...) 237 | if err != nil { 238 | return err 239 | } 240 | if count != 0 { 241 | return errors.Errorf("Pod %s has %d errors", pod.Name, count) 242 | } 243 | } 244 | 245 | return nil 246 | } 247 | 248 | func PodsInNamespaceWithSelectorHaveSomeErrorsInLogsSinceTime(kubeClientset kubernetes.Interface, namespace string, selector string, since time.Time) error { 249 | err := PodsInNamespaceWithSelectorHaveNoErrorsInLogsSinceTime(kubeClientset, namespace, selector, since) 250 | if err == nil { 251 | return fmt.Errorf("logs found from selector %q in namespace %q have errors", selector, namespace) 252 | } 253 | return nil 254 | } 255 | 256 | func PodInNamespaceShouldHaveLabels(kubeClientset kubernetes.Interface, name, namespace, labels string) error { 257 | if err := common.ValidateClientset(kubeClientset); err != nil { 258 | return err 259 | } 260 | 261 | pod, err := kubeClientset.CoreV1().Pods(namespace).Get(context.Background(), name, metav1.GetOptions{}) 262 | if err != nil { 263 | return errors.New("Error fetching pod: " + err.Error()) 264 | } 265 | 266 | inputLabels := make(map[string]string) 267 | slc := strings.Split(labels, ",") 268 | for _, item := range slc { 269 | vals := strings.Split(item, "=") 270 | if len(vals) != 2 { 271 | continue 272 | } 273 | 274 | inputLabels[vals[0]] = vals[1] 275 | } 276 | 277 | for k, v := range inputLabels { 278 | pV, ok := pod.Labels[k] 279 | if !ok { 280 | return errors.New(fmt.Sprintf("Label %s missing in pod/namespace %s", k, name+"/"+namespace)) 281 | } 282 | if v != pV { 283 | return errors.New(fmt.Sprintf("Label value %s doesn't match expected %s for key %s in pod/namespace %s", pV, v, k, name+"/"+namespace)) 284 | } 285 | } 286 | 287 | return nil 288 | } 289 | 290 | func PodsInNamespaceWithSelectorShouldHaveLabels(kubeClientset kubernetes.Interface, namespace, selector, labels string) error { 291 | podList, err := GetPodListWithLabelSelector(kubeClientset, namespace, selector) 292 | if err != nil { 293 | return fmt.Errorf("error getting pods with selector %q: %v", selector, err) 294 | } 295 | 296 | if len(podList.Items) == 0 { 297 | return fmt.Errorf("no pods matched selector '%s'", selector) 298 | } 299 | 300 | for _, pod := range podList.Items { 301 | inputLabels := make(map[string]string) 302 | slc := strings.Split(labels, ",") 303 | for _, item := range slc { 304 | vals := strings.Split(item, "=") 305 | if len(vals) != 2 { 306 | continue 307 | } 308 | 309 | inputLabels[vals[0]] = vals[1] 310 | } 311 | 312 | for k, v := range inputLabels { 313 | pV, ok := pod.Labels[k] 314 | if !ok { 315 | return fmt.Errorf("label %s missing in pod/namespace %s", k, pod.Name+"/"+namespace) 316 | } 317 | if v != pV { 318 | return fmt.Errorf("label value %s doesn't match expected %s for key %s in pod/namespace %s", pV, v, k, pod.Name+"/"+namespace) 319 | } 320 | } 321 | } 322 | 323 | return nil 324 | } 325 | -------------------------------------------------------------------------------- /pkg/kube/kube.go: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package kube 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "path/filepath" 21 | "time" 22 | 23 | "github.com/keikoproj/kubedog/pkg/kube/common" 24 | "github.com/keikoproj/kubedog/pkg/kube/pod" 25 | "github.com/keikoproj/kubedog/pkg/kube/structured" 26 | unstruct "github.com/keikoproj/kubedog/pkg/kube/unstructured" 27 | "github.com/pkg/errors" 28 | log "github.com/sirupsen/logrus" 29 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 | "k8s.io/client-go/dynamic" 31 | "k8s.io/client-go/kubernetes" 32 | "k8s.io/client-go/tools/clientcmd" 33 | ) 34 | 35 | type ClientSet struct { 36 | KubeInterface kubernetes.Interface 37 | DynamicInterface dynamic.Interface 38 | timestamps map[string]time.Time 39 | config configuration 40 | } 41 | 42 | func (kc *ClientSet) SetFilesPath(path string) { 43 | kc.config.filesPath = path 44 | } 45 | 46 | func (kc *ClientSet) SetTemplateArguments(args interface{}) { 47 | kc.config.templateArguments = args 48 | } 49 | 50 | func (kc *ClientSet) SetWaiterInterval(duration time.Duration) { 51 | kc.config.waiterInterval = duration 52 | } 53 | 54 | func (kc *ClientSet) SetWaiterTries(tries int) { 55 | kc.config.waiterTries = tries 56 | } 57 | 58 | func (kc *ClientSet) DiscoverClients() error { 59 | var ( 60 | home, _ = os.UserHomeDir() 61 | kubeconfigPath = filepath.Join(home, ".kube", "config") 62 | ) 63 | 64 | if exported := os.Getenv("KUBECONFIG"); exported != "" { 65 | kubeconfigPath = exported 66 | } 67 | if _, err := os.Stat(kubeconfigPath); os.IsNotExist(err) { 68 | return errors.Errorf("expected kubeconfig to exist for create operation, '%v'", kubeconfigPath) 69 | } 70 | config, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | dynClient, err := dynamic.NewForConfig(config) 76 | if err != nil { 77 | log.Fatal("Unable to construct dynamic client", err) 78 | } 79 | client, err := kubernetes.NewForConfig(config) 80 | if err != nil { 81 | return err 82 | } 83 | _, err = client.Discovery().ServerVersion() 84 | if err != nil { 85 | return err 86 | } 87 | 88 | kc.DynamicInterface = dynClient 89 | kc.KubeInterface = client 90 | 91 | return nil 92 | } 93 | 94 | func (kc *ClientSet) SetTimestamp(timestampName string) error { 95 | now := time.Now() 96 | if kc.timestamps == nil { 97 | kc.timestamps = map[string]time.Time{} 98 | } 99 | kc.timestamps[timestampName] = now 100 | log.Infof("Set timestamp '%s' as '%v'", timestampName, now) 101 | return nil 102 | } 103 | 104 | func (kc *ClientSet) KubernetesClusterShouldBe(state string) error { 105 | switch state { 106 | case common.StateCreated, common.StateUpgraded: 107 | if err := pod.ListPods(kc.KubeInterface, metav1.NamespaceSystem); err != nil { 108 | return errors.Errorf("failed validating cluster create/update, could not get pods: '%v'", err) 109 | } 110 | return nil 111 | case common.StateDeleted: 112 | if err := kc.DiscoverClients(); err == nil { 113 | return errors.New("failed validating cluster delete, cluster is still available") 114 | } 115 | return nil 116 | default: 117 | return fmt.Errorf("unsupported state: '%s'", state) 118 | } 119 | } 120 | 121 | func (kc *ClientSet) DeleteAllTestResources() error { 122 | if err := kc.DiscoverClients(); err != nil { 123 | return err 124 | } 125 | return unstruct.DeleteResourcesAtPath(kc.DynamicInterface, kc.getDiscoveryClient(), kc.config.templateArguments, kc.getWaiterConfig(), kc.getTemplatesPath()) 126 | } 127 | 128 | func (kc *ClientSet) ResourceOperation(operation, resourceFileName string) error { 129 | resource, err := unstruct.GetResource(kc.getDiscoveryClient(), kc.config.templateArguments, kc.getResourcePath(resourceFileName)) 130 | if err != nil { 131 | return err 132 | } 133 | // TODO: use ResourceOperationInNamespace should like ResourceOperation does, ResourceOperation is redundant 134 | return unstruct.ResourceOperation(kc.DynamicInterface, resource, operation) 135 | } 136 | 137 | func (kc *ClientSet) ResourceOperationInNamespace(operation, resourceFileName, namespace string) error { 138 | resource, err := unstruct.GetResource(kc.getDiscoveryClient(), kc.config.templateArguments, kc.getResourcePath(resourceFileName)) 139 | if err != nil { 140 | return err 141 | } 142 | return unstruct.ResourceOperationInNamespace(kc.DynamicInterface, resource, operation, namespace) 143 | } 144 | 145 | func (kc *ClientSet) ResourcesOperation(operation, resourcesFileName string) error { 146 | resources, err := unstruct.GetResources(kc.getDiscoveryClient(), kc.config.templateArguments, kc.getResourcePath(resourcesFileName)) 147 | if err != nil { 148 | return err 149 | } 150 | return unstruct.ResourcesOperation(kc.DynamicInterface, resources, operation) 151 | } 152 | 153 | func (kc *ClientSet) ResourcesOperationInNamespace(operation, resourcesFileName, namespace string) error { 154 | resources, err := unstruct.GetResources(kc.getDiscoveryClient(), kc.config.templateArguments, kc.getResourcePath(resourcesFileName)) 155 | if err != nil { 156 | return err 157 | } 158 | return unstruct.ResourcesOperationInNamespace(kc.DynamicInterface, resources, operation, namespace) 159 | } 160 | 161 | func (kc *ClientSet) ResourceOperationWithResult(operation, resourceFileName, expectedResult string) error { 162 | resource, err := unstruct.GetResource(kc.getDiscoveryClient(), kc.config.templateArguments, kc.getResourcePath(resourceFileName)) 163 | if err != nil { 164 | return err 165 | } 166 | return unstruct.ResourceOperationWithResult(kc.DynamicInterface, resource, operation, expectedResult) 167 | } 168 | 169 | func (kc *ClientSet) ResourceOperationWithResultInNamespace(operation, resourceFileName, namespace, expectedResult string) error { 170 | resource, err := unstruct.GetResource(kc.getDiscoveryClient(), kc.config.templateArguments, kc.getResourcePath(resourceFileName)) 171 | if err != nil { 172 | return err 173 | } 174 | return unstruct.ResourceOperationWithResultInNamespace(kc.DynamicInterface, resource, operation, namespace, expectedResult) 175 | } 176 | 177 | func (kc *ClientSet) ResourceShouldBe(resourceFileName, state string) error { 178 | resource, err := unstruct.GetResource(kc.getDiscoveryClient(), kc.config.templateArguments, kc.getResourcePath(resourceFileName)) 179 | if err != nil { 180 | return err 181 | } 182 | return unstruct.ResourceShouldBe(kc.DynamicInterface, resource, kc.getWaiterConfig(), state) 183 | } 184 | 185 | func (kc *ClientSet) ResourceShouldConvergeToSelector(resourceFileName, selector string) error { 186 | resource, err := unstruct.GetResource(kc.getDiscoveryClient(), kc.config.templateArguments, kc.getResourcePath(resourceFileName)) 187 | if err != nil { 188 | return err 189 | } 190 | return unstruct.ResourceShouldConvergeToSelector(kc.DynamicInterface, resource, kc.getWaiterConfig(), selector) 191 | } 192 | 193 | func (kc *ClientSet) ResourceShouldConvergeToField(resourceFileName, selector string) error { 194 | resource, err := unstruct.GetResource(kc.getDiscoveryClient(), kc.config.templateArguments, kc.getResourcePath(resourceFileName)) 195 | if err != nil { 196 | return err 197 | } 198 | return unstruct.ResourceShouldConvergeToField(kc.DynamicInterface, resource, kc.getWaiterConfig(), selector) 199 | } 200 | 201 | func (kc *ClientSet) ResourceConditionShouldBe(resourceFileName, conditionType, conditionValue string) error { 202 | resource, err := unstruct.GetResource(kc.getDiscoveryClient(), kc.config.templateArguments, kc.getResourcePath(resourceFileName)) 203 | if err != nil { 204 | return err 205 | } 206 | return unstruct.ResourceConditionShouldBe(kc.DynamicInterface, resource, kc.getWaiterConfig(), conditionType, conditionValue) 207 | } 208 | 209 | func (kc *ClientSet) UpdateResourceWithField(resourceFileName, key, value string) error { 210 | resource, err := unstruct.GetResource(kc.getDiscoveryClient(), kc.config.templateArguments, kc.getResourcePath(resourceFileName)) 211 | if err != nil { 212 | return err 213 | } 214 | return unstruct.UpdateResourceWithField(kc.DynamicInterface, resource, key, value) 215 | } 216 | 217 | func (kc *ClientSet) VerifyInstanceGroups() error { 218 | return unstruct.VerifyInstanceGroups(kc.DynamicInterface) 219 | } 220 | 221 | func (kc *ClientSet) PodOperation(operation, namespace string) error { 222 | return pod.PodOperation(kc.KubeInterface, operation, namespace) 223 | } 224 | 225 | func (kc *ClientSet) PodOperationWithSelector(operation, namespace, selector string) error { 226 | return pod.PodOperationWithSelector(kc.KubeInterface, operation, namespace, selector) 227 | } 228 | 229 | func (kc *ClientSet) DeletePodWithFieldSelector(namespace, fieldSelector string) error { 230 | return pod.DeletePodsWithFieldSelector(kc.KubeInterface, namespace, fieldSelector) 231 | } 232 | 233 | func (kc *ClientSet) PodsWithSelectorHaveRestartCountLessThan(namespace, selector string, restartCount int) error { 234 | return pod.PodsWithSelectorHaveRestartCountLessThan(kc.KubeInterface, namespace, selector, restartCount) 235 | } 236 | 237 | func (kc *ClientSet) SomeOrAllPodsInNamespaceWithSelectorHaveStringInLogsSinceTime(someOrAll, namespace, selector, searchKeyword, sinceTime string) error { 238 | timestamp, err := kc.GetTimestamp(sinceTime) 239 | if err != nil { 240 | return err 241 | } 242 | // TODO: refactor SomeOrAllPodsInNamespaceWithSelectorHaveStringInLogsSinceTime to not have the input someOrAll change its behavior, instead have different methods 243 | return pod.SomeOrAllPodsInNamespaceWithSelectorHaveStringInLogsSinceTime(kc.KubeInterface, kc.getExpBackoff(), someOrAll, namespace, selector, searchKeyword, timestamp) 244 | } 245 | 246 | func (kc *ClientSet) SomePodsInNamespaceWithSelectorDontHaveStringInLogsSinceTime(namespace, selector, searchKeyword, sinceTime string) error { 247 | timestamp, err := kc.GetTimestamp(sinceTime) 248 | if err != nil { 249 | return err 250 | } 251 | return pod.SomePodsInNamespaceWithSelectorDontHaveStringInLogsSinceTime(kc.KubeInterface, namespace, selector, searchKeyword, timestamp) 252 | } 253 | 254 | func (kc *ClientSet) PodsInNamespaceWithSelectorHaveNoErrorsInLogsSinceTime(namespace, selector, sinceTime string) error { 255 | timestamp, err := kc.GetTimestamp(sinceTime) 256 | if err != nil { 257 | return err 258 | } 259 | return pod.PodsInNamespaceWithSelectorHaveNoErrorsInLogsSinceTime(kc.KubeInterface, namespace, selector, timestamp) 260 | } 261 | 262 | func (kc *ClientSet) PodsInNamespaceWithSelectorHaveSomeErrorsInLogsSinceTime(namespace, selector, sinceTime string) error { 263 | timestamp, err := kc.GetTimestamp(sinceTime) 264 | if err != nil { 265 | return err 266 | } 267 | return pod.PodsInNamespaceWithSelectorHaveSomeErrorsInLogsSinceTime(kc.KubeInterface, namespace, selector, timestamp) 268 | } 269 | 270 | func (kc *ClientSet) PodsInNamespaceWithLabelSelectorConvergeToFieldSelector(namespace, labelSelector, fieldSelector string) error { 271 | return pod.PodsInNamespaceWithLabelSelectorConvergeToFieldSelector(kc.KubeInterface, kc.getExpBackoff(), namespace, labelSelector, fieldSelector) 272 | } 273 | 274 | func (kc *ClientSet) PodsInNamespaceWithSelectorShouldHaveLabels(namespace, selector, labels string) error { 275 | return pod.PodsInNamespaceWithSelectorShouldHaveLabels(kc.KubeInterface, namespace, selector, labels) 276 | } 277 | 278 | func (kc *ClientSet) PodInNamespaceShouldHaveLabels(name, namespace, labels string) error { 279 | return pod.PodInNamespaceShouldHaveLabels(kc.KubeInterface, name, namespace, labels) 280 | } 281 | 282 | func (kc *ClientSet) SecretOperationFromEnvironmentVariable(operation, name, namespace, environmentVariable string) error { 283 | return structured.SecretOperationFromEnvironmentVariable(kc.KubeInterface, operation, name, namespace, environmentVariable) 284 | } 285 | 286 | func (kc *ClientSet) SecretDelete(name, namespace string) error { 287 | // TODO: use SecretOperationFromEnvironmentVariable directly like SecretDelete does, SecretDelete is redundant 288 | return structured.SecretDelete(kc.KubeInterface, name, namespace) 289 | } 290 | 291 | func (kc *ClientSet) NodesWithSelectorShouldBe(expectedNodes int, selector, state string) error { 292 | return structured.NodesWithSelectorShouldBe(kc.KubeInterface, kc.getWaiterConfig(), expectedNodes, selector, state) 293 | } 294 | 295 | func (kc *ClientSet) ResourceInNamespace(resourceType, name, isOrIsNot, namespace string) error { 296 | switch isOrIsNot { 297 | case "is": 298 | return structured.ResourceInNamespace(kc.KubeInterface, resourceType, name, namespace) 299 | case "is not": 300 | return structured.ResourceNotInNamespace(kc.KubeInterface, resourceType, name, namespace) 301 | default: 302 | return errors.Errorf("paramter isOrIsNot can only be 'is' or 'is not'") 303 | } 304 | } 305 | 306 | func (kc *ClientSet) ScaleDeployment(name, namespace string, replicas int32) error { 307 | return structured.ScaleDeployment(kc.KubeInterface, name, namespace, replicas) 308 | } 309 | 310 | func (kc *ClientSet) ValidatePrometheusVolumeClaimTemplatesName(statefulsetName, namespace, volumeClaimTemplatesName string) error { 311 | return structured.ValidatePrometheusVolumeClaimTemplatesName(kc.KubeInterface, statefulsetName, namespace, volumeClaimTemplatesName) 312 | } 313 | 314 | func (kc *ClientSet) ListNodes() error { 315 | return structured.ListNodes(kc.KubeInterface) 316 | } 317 | 318 | func (kc *ClientSet) DaemonSetIsRunning(name, namespace string) error { 319 | return structured.DaemonSetIsRunning(kc.KubeInterface, kc.getExpBackoff(), name, namespace) 320 | } 321 | 322 | func (kc *ClientSet) DeploymentIsRunning(name, namespace string) error { 323 | return structured.DeploymentIsRunning(kc.KubeInterface, name, namespace) 324 | } 325 | 326 | func (kc *ClientSet) ConfigMapDataHasKeyAndValue(name, namespace, key, value string) error { 327 | return structured.ConfigMapDataHasKeyAndValue(kc.KubeInterface, name, namespace, key, value) 328 | } 329 | 330 | func (kc *ClientSet) PersistentVolExists(name, expectedPhase string) error { 331 | return structured.PersistentVolExists(kc.KubeInterface, name, expectedPhase) 332 | } 333 | 334 | func (kc *ClientSet) PersistentVolClaimExists(name, expectedPhase string, namespace string) error { 335 | return structured.PersistentVolClaimExists(kc.KubeInterface, name, expectedPhase, namespace) 336 | } 337 | 338 | func (kc *ClientSet) ClusterRbacIsFound(resourceType, name string) error { 339 | return structured.ClusterRbacIsFound(kc.KubeInterface, resourceType, name) 340 | } 341 | 342 | func (kc *ClientSet) IngressAvailable(name, namespace string, port int, path string) error { 343 | return structured.IngressAvailable(kc.KubeInterface, kc.getWaiterConfig(), name, namespace, port, path) 344 | } 345 | 346 | func (kc *ClientSet) SendTrafficToIngress(tps int, name, namespace string, port int, path string, duration int, durationUnits string, expectedErrors int) error { 347 | return structured.SendTrafficToIngress(kc.KubeInterface, kc.getWaiterConfig(), tps, name, namespace, port, path, duration, durationUnits, expectedErrors) 348 | } 349 | --------------------------------------------------------------------------------