├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── release-config.yaml ├── spellcheck.yml ├── wordlist.txt └── workflows │ ├── assets │ └── grpcurl.yaml │ ├── draftrelease.yaml │ ├── golangci-lint.yml │ ├── linkcheck.yaml │ ├── lintcharts.yaml │ ├── lintcharts2.yaml │ ├── releaseassets.yaml │ ├── releasecharts.yaml │ ├── spellcheck.yaml │ ├── testcharts.yaml │ ├── testkustomize.yaml │ ├── unittest.yaml │ ├── verifyuserexperience.yaml │ └── versionbump.yaml ├── .gitignore ├── .golangci.yml ├── .lycheeignore ├── ADOPTERS.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MAINTAINERS.md ├── Makefile ├── README.md ├── abn ├── grpc │ ├── abn.pb.go │ ├── abn.proto │ └── abn_grpc.pb.go ├── service.go ├── service_impl.go ├── service_impl_test.go ├── service_test.go └── test_helpers.go ├── action ├── doc.go ├── run.go └── run_test.go ├── base ├── collect_grpc.go ├── collect_grpc_test.go ├── collect_http.go ├── collect_http_test.go ├── doc.go ├── experiment.go ├── experiment_test.go ├── insights_test.go ├── internal │ ├── common.go │ ├── doc.go │ └── helloworld │ │ └── helloworld │ │ ├── doc.go │ │ ├── greeter.pb.go │ │ ├── greeter.proto │ │ ├── greeter_grpc.pb.go │ │ └── greeter_server.go ├── kubedriver.go ├── kubedriver_test.go ├── log │ ├── doc.go │ ├── log.go │ └── log_test.go ├── metrics.go ├── notify.go ├── notify_test.go ├── readiness.go ├── readiness_test.go ├── run.go ├── run_test.go ├── sprigutil.go ├── test_helpers.go ├── test_helpers_driver.go ├── util.go └── util_test.go ├── bump-version-hints.md ├── charts ├── controller │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ │ ├── _helpers.tpl │ │ ├── configmap.yaml │ │ ├── persistentvolumeclaim.yaml │ │ ├── roles.yaml │ │ ├── service.yaml │ │ ├── serviceaccount.yaml │ │ └── statefulset.yaml │ └── values.yaml ├── iter8 │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── templates │ │ ├── _experiment.tpl │ │ ├── _k-job.tpl │ │ ├── _k-role.tpl │ │ ├── _k-rolebinding.tpl │ │ ├── _k-secret.tpl │ │ ├── _k-serviceacccount.tpl │ │ ├── _task-github.tpl │ │ ├── _task-grpc.tpl │ │ ├── _task-http.tpl │ │ ├── _task-ready.tpl │ │ ├── _task-slack.tpl │ │ └── k8s.yaml │ └── values.yaml └── release │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── _configmap.weight-config.tpl │ ├── _deployment-gtw.blue-green.routemap.tpl │ ├── _deployment-gtw.blue-green.tpl │ ├── _deployment-gtw.canary.routemap.tpl │ ├── _deployment-gtw.canary.tpl │ ├── _deployment-gtw.none.routemap.tpl │ ├── _deployment-gtw.none.tpl │ ├── _deployment-gtw.service.tpl │ ├── _deployment-gtw.tpl │ ├── _deployment-istio.blue-green.routemap.tpl │ ├── _deployment-istio.blue-green.tpl │ ├── _deployment-istio.canary.routemap.tpl │ ├── _deployment-istio.canary.tpl │ ├── _deployment-istio.mirror.routemap.tpl │ ├── _deployment-istio.mirror.tpl │ ├── _deployment-istio.none.routemap.tpl │ ├── _deployment-istio.none.tpl │ ├── _deployment-istio.service.tpl │ ├── _deployment-istio.tpl │ ├── _deployment.tpl │ ├── _deployment.version.deployment.tpl │ ├── _deployment.version.service.tpl │ ├── _helpers.tpl │ ├── _kserve.blue-green.routemap.tpl │ ├── _kserve.blue-green.tpl │ ├── _kserve.canary.routemap.tpl │ ├── _kserve.canary.tpl │ ├── _kserve.none.routemap.tpl │ ├── _kserve.none.tpl │ ├── _kserve.service.tpl │ ├── _kserve.tpl │ ├── _kserve.version.isvc.tpl │ ├── _mm-istio.blue-green.routemap.tpl │ ├── _mm-istio.blue-green.tpl │ ├── _mm-istio.canary.routemap.tpl │ ├── _mm-istio.canary.tpl │ ├── _mm-istio.none.routemap.tpl │ ├── _mm-istio.none.tpl │ ├── _mm-istio.service.tpl │ ├── _mm-istio.tpl │ ├── _mm-istio.version.isvc.tpl │ └── release.yaml │ └── values.yaml ├── cmd ├── controllers.go ├── controllers_test.go ├── doc.go ├── docs.go ├── docs_test.go ├── k.go ├── krun.go ├── krun_test.go ├── root.go ├── test_helpers.go ├── version.go └── version_test.go ├── config.yaml ├── controllers ├── allcontrollers.go ├── allcontrollers_test.go ├── config.go ├── config_test.go ├── events.go ├── finalizer.go ├── finalizer_test.go ├── interface.go ├── interface_test.go ├── k8sclient │ ├── fake │ │ ├── simple.go │ │ └── simple_test.go │ ├── interface.go │ └── simple.go ├── podname.go ├── podname_test.go ├── routemap.go ├── routemap_test.go ├── routemaps.go └── routemaps_test.go ├── doc.go ├── docker └── Dockerfile ├── driver ├── common.go ├── common_test.go ├── doc.go ├── kubedriver.go ├── kubedriver_test.go └── test_helpers.go ├── go.mod ├── go.sum ├── grafana ├── abn.json ├── grpc.json └── http.json ├── kustomize └── controller │ ├── clusterScoped │ └── kustomization.yaml │ └── namespaceScoped │ ├── configmap.yaml │ ├── kustomization.yaml │ ├── pvc.yaml │ ├── role.yaml │ ├── rolebinding.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── statefulset.yaml ├── main.go ├── metrics ├── doc.go ├── server.go ├── server_test.go └── test_helpers.go ├── storage ├── badgerdb │ ├── badgerdb.go │ └── badgerdb_test.go ├── client │ ├── client.go │ └── client_test.go ├── interface.go ├── redis │ ├── redis.go │ └── redis_test.go ├── util.go └── util_test.go ├── templates └── notify │ ├── _payload-github.tpl │ └── _payload-slack.tpl └── testdata ├── .gitignore ├── abninputs ├── application.yaml └── config.yaml ├── assertinputs ├── .gitignore └── experiment.yaml ├── controllers ├── config.yaml └── garb.age ├── drivertests ├── .gitignore └── experiment.tpl ├── experiment.tpl ├── experiment.yaml ├── experiment_grpc.yaml ├── output ├── .gitignore ├── gen-cli-values.txt ├── gen-values-file.txt ├── hub-with-destdir.txt ├── hub.txt ├── kassert.txt ├── kdelete.txt ├── klaunch.txt ├── klog.txt ├── krun.txt ├── launch-with-destdir.txt └── launch.txt └── payload └── ukpolice.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: kind/bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Desktop (please complete the following information):** 23 | - OS: [e.g. MacOS] 24 | - Output of the `iter8 version` command 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull request 3 | about: Create a pull request 4 | title: '' 5 | labels: kind/bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | # Pull Request Template 11 | 12 | ## Description 13 | 14 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 15 | 16 | Fixes # (issue) 17 | 18 | ## Type of change 19 | 20 | Please delete options that are not relevant. 21 | 22 | - [ ] Bug fix (non-breaking change which fixes an issue) 23 | - [ ] New feature (non-breaking change which adds functionality) 24 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 25 | - [ ] This change requires a documentation update 26 | 27 | ## How Has This Been Tested? 28 | 29 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 30 | 31 | - [ ] Test A 32 | - [ ] Test B 33 | 34 | **Test Configuration**: 35 | * OS (if applicable) 36 | * Kubernetes version (if applicable) 37 | 38 | ## Checklist: 39 | 40 | - [ ] My code follows the style guidelines of this project 41 | - [ ] I have performed a self-review of my own code 42 | - [ ] I have commented my code, particularly in hard-to-understand areas 43 | - [ ] I have made corresponding changes to the documentation 44 | - [ ] My changes generate no new warnings 45 | - [ ] I have added tests that prove my fix is effective or that my feature works 46 | - [ ] New and existing unit tests pass locally with my changes 47 | - [ ] Any dependent changes have been merged and published in downstream modules 48 | - [ ] I have checked my code and corrected any misspellings 49 | -------------------------------------------------------------------------------- /.github/release-config.yaml: -------------------------------------------------------------------------------- 1 | name-template: 'Version $NEXT_PATCH_VERSION of Iter8' 2 | tag-template: 'v$NEXT_PATCH_VERSION' 3 | tag-prefix: 'v' 4 | categories: 5 | - title: '🚀 Features' 6 | labels: 7 | - 'kind/enhancement' 8 | - title: '🧹 Cleaned or removed features' 9 | label: 'remove' 10 | - title: '🐛 Bug Fixes' 11 | labels: 12 | - 'kind/bug' 13 | - title: '🧰 Maintenance' 14 | label: 'chore' 15 | - title: '📝 Documentation' 16 | labels: 17 | - 'kind/docs' 18 | - title: '🚦 CI' 19 | labels: 20 | - 'area/CI' 21 | change-template: '- #$NUMBER: $TITLE' 22 | template: | 23 | ## What’s Changed 24 | $CHANGES 25 | -------------------------------------------------------------------------------- /.github/spellcheck.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | - name: Markdown 3 | aspell: 4 | lang: en 5 | ignore-case: true 6 | dictionary: 7 | wordlists: 8 | - .github/wordlist.txt # <-- put path to custom dictionary file here 9 | encoding: utf-8 10 | pipeline: 11 | - pyspelling.filters.markdown: 12 | - pyspelling.filters.html: 13 | comments: false 14 | ignores: 15 | - code 16 | - pre 17 | sources: 18 | - 'docs/**/*.md' 19 | - '*.md' 20 | default_encoding: utf-8 -------------------------------------------------------------------------------- /.github/wordlist.txt: -------------------------------------------------------------------------------- 1 | abn 2 | acm 3 | API 4 | apis 5 | ArgoCD 6 | AutoX 7 | backend 8 | backends 9 | benchmarking 10 | Cha 11 | chaosengine 12 | CLI 13 | composable 14 | CRD 15 | CRDs 16 | cronjob 17 | crontab 18 | DCO 19 | default_encoding 20 | declaratively 21 | DevOps 22 | DevSecOps 23 | Dockerfile 24 | DZone 25 | Fortio 26 | frontend 27 | GitOps 28 | gRPC 29 | GitHub 30 | Grafana 31 | GVR 32 | Homebrew 33 | http 34 | httpbin 35 | https 36 | iCalendar 37 | InferenceService 38 | integrations 39 | io 40 | Istio 41 | Istio's 42 | Iter 43 | ITnext 44 | js 45 | JSON 46 | jq 47 | Kalantar 48 | Knative 49 | kubectl 50 | Kubernetes 51 | Kubecon 52 | KServe 53 | Linkerd 54 | LitmusChaos 55 | localhost 56 | minikube 57 | MLOps 58 | modelmesh 59 | namespace 60 | namespaces 61 | NewRelic 62 | Parthasarathy 63 | plotly 64 | png 65 | PRs 66 | protobuf 67 | protoc 68 | quickstart 69 | roadmap 70 | scikit 71 | SDK 72 | sed 73 | Seldon 74 | sexualized 75 | SHA 76 | sklearn 77 | SLO 78 | SLOs 79 | SRE 80 | Srinivasan 81 | subdirectory 82 | Sysdig 83 | tada 84 | Tekton 85 | toc 86 | unary 87 | warmup 88 | warmupDuration 89 | warmupNumRequests 90 | webhook 91 | webhooks 92 | yaml 93 | abnmetrics 94 | auth 95 | argoproj 96 | custommetrics 97 | ctx 98 | deleteiter 99 | dev 100 | encodedmetric 101 | execintosleep 102 | expreport 103 | failured 104 | getRecommendation 105 | GetTrack 106 | GitCommit 107 | githubusercontent 108 | gmail 109 | golang 110 | GoVersion 111 | GOBIN 112 | installbrewbins 113 | installghaction 114 | installiter 115 | ksvc 116 | kustomize 117 | lastupdatetime 118 | lifecycle 119 | linenums 120 | lut 121 | metricname 122 | nofailure 123 | repo 124 | repos 125 | req 126 | rollout 127 | rollouts 128 | setName 129 | setUser 130 | trackToRoute 131 | toJson 132 | verifyUserExperience 133 | versionname 134 | WriteMetric 135 | contentType 136 | numRequests 137 | payloadStr 138 | payloadURL 139 | proto 140 | qps 141 | bool 142 | payloadTemplateURL 143 | tpl 144 | usr 145 | softFailure 146 | struct 147 | versionValues 148 | ProviderSpec 149 | jqExpression 150 | binaryDataURL 151 | binaryDataURL 152 | dataURL 153 | metadataURL 154 | protoURL 155 | irisv 156 | EOF 157 | HOSTNAME 158 | jsonpath 159 | cronjobSchedule 160 | logLevel 161 | serviceAccountName 162 | wget 163 | gz 164 | xvf 165 | IMG 166 | mv 167 | Atin 168 | ChaosNative 169 | Chaudhary 170 | Datagrate 171 | Mert 172 | Shubham 173 | Sood 174 | Toolchains 175 | jetic 176 | Öztürk 177 | reconfigures 178 | spartha 179 | sriumcp 180 | -------------------------------------------------------------------------------- /.github/workflows/draftrelease.yaml: -------------------------------------------------------------------------------- 1 | name: Release drafter 2 | 3 | # Runs when changes are pushed 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | update_release_draft: 12 | runs-on: ubuntu-latest 13 | steps: 14 | # Drafts your next Release notes as Pull Requests are merged into any tracked branch 15 | - uses: release-drafter/release-drafter@v5 16 | with: 17 | config-name: release-config.yaml 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | # Only runs when there are golang code changes 4 | 5 | # Lint golang files 6 | 7 | on: 8 | pull_request: 9 | branches: 10 | - master 11 | paths: 12 | - '**.go' 13 | 14 | permissions: 15 | contents: read 16 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 17 | # pull-requests: read 18 | 19 | jobs: 20 | golangci: 21 | name: lint 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/setup-go@v5 25 | with: 26 | go-version: 1.21 27 | - uses: actions/checkout@v4 28 | - name: golangci-lint 29 | uses: golangci/golangci-lint-action@v3 30 | with: 31 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 32 | version: v1.55.2 33 | 34 | # Optional: working directory, useful for monorepos 35 | # working-directory: somedir 36 | 37 | # Optional: golangci-lint command line arguments. 38 | # args: --issues-exit-code=0 39 | 40 | # Optional: show only new issues if it's a pull request. The default value is `false`. 41 | # only-new-issues: true 42 | 43 | # Optional: if set to true then the all caching functionality will be complete disabled, 44 | # takes precedence over all other caching options. 45 | # skip-cache: true 46 | 47 | # Optional: if set to true then the action don't cache or restore ~/go/pkg. 48 | # skip-pkg-cache: true 49 | 50 | # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. 51 | # skip-build-cache: true -------------------------------------------------------------------------------- /.github/workflows/linkcheck.yaml: -------------------------------------------------------------------------------- 1 | name: Link checker 2 | 3 | # Only runs when there are markdown changes and intermittently 4 | 5 | # Check links across markdown files 6 | 7 | on: 8 | pull_request: 9 | branches: 10 | - master 11 | paths: 12 | - '**.md' 13 | schedule: 14 | - cron: "0 0 1 * *" 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 26 | - uses: actions/checkout@v4 27 | 28 | - name: Link checker 29 | id: lychee 30 | uses: lycheeverse/lychee-action@v1.8.0 31 | env: 32 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 33 | with: 34 | fail: true 35 | args: -v '**/*.md' -------------------------------------------------------------------------------- /.github/workflows/lintcharts.yaml: -------------------------------------------------------------------------------- 1 | name: Lint Helm charts 2 | 3 | # Only runs when charts have changed 4 | 5 | # Lint Helm charts 6 | 7 | on: 8 | pull_request: 9 | branches: 10 | - master 11 | paths: 12 | - charts/** 13 | 14 | jobs: 15 | # Get the paths for the Helm charts to lint 16 | get_paths: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Get the paths for Helm charts to lint 25 | id: set-matrix 26 | run: | 27 | # Get paths (in string form) 28 | stringPaths=$(find -maxdepth 2 -path './charts/*') 29 | 30 | # Check paths (length greater than 0) 31 | stringPathsLength=$(echo ${#stringPaths}) 32 | if (( stringPathsLength == 0 )); 33 | then 34 | echo "No paths to check" 35 | exit 1 36 | fi 37 | 38 | # Serialize paths into JSON array 39 | paths=$(jq -ncR '[inputs]' <<< "$stringPaths") 40 | 41 | # Output serialized paths 42 | echo "matrix=$paths" >> $GITHUB_OUTPUT 43 | echo $paths 44 | 45 | outputs: 46 | matrix: ${{ steps.set-matrix.outputs.matrix }} 47 | 48 | # Lint Helm charts based on paths provided by previous job 49 | lint: 50 | name: Test changed-files 51 | needs: get_paths 52 | runs-on: ubuntu-latest 53 | strategy: 54 | matrix: 55 | version: ${{ fromJson(needs.get_paths.outputs.matrix) }} 56 | steps: 57 | - uses: actions/checkout@v4 58 | with: 59 | fetch-depth: 0 60 | 61 | - name: Get modified files in the ${{ matrix.version }} folder 62 | id: modified-files 63 | uses: tj-actions/changed-files@v41 64 | with: 65 | files: ${{ matrix.version }} 66 | 67 | - name: Lint Helm charts in the ${{ matrix.version }} folder 68 | uses: stackrox/kube-linter-action@v1 69 | if: steps.modified-files.outputs.any_modified == 'true' 70 | with: 71 | directory: ${{ matrix.version }} -------------------------------------------------------------------------------- /.github/workflows/lintcharts2.yaml: -------------------------------------------------------------------------------- 1 | name: Additional Helm chart linting 2 | # Like lintcharts.yaml, the other lint Helm chart workflow, this workflow uses kube-linter 3 | # kube-linter checks Helm templates but it does not check what is contained in {{ define ... }} blocks 4 | # This workflow builds on the other workflow by producing Kubernetes YAML files from the templates and running kube-linter on those files 5 | # See iter8-tools/iter8#1452 6 | 7 | # Only runs when charts have changed 8 | 9 | # Lint Helm charts 10 | # Use templates to create Kubernetes YAML files and lint them 11 | 12 | on: 13 | pull_request: 14 | branches: 15 | - master 16 | paths: 17 | - charts/** 18 | 19 | jobs: 20 | http: 21 | name: Lint HTTP performance test 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - name: Check out code 26 | uses: actions/checkout@v4 27 | 28 | - name: Get modified files in the charts/iter8 folder 29 | id: modified-files 30 | uses: tj-actions/changed-files@v41 31 | with: 32 | files: charts/iter8 33 | 34 | - uses: azure/setup-helm@v3 35 | if: steps.modified-files.outputs.any_modified == 'true' 36 | with: 37 | token: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Create Kubernetes YAML file 40 | if: steps.modified-files.outputs.any_modified == 'true' 41 | run: | 42 | helm template charts/iter8 \ 43 | --set tasks={http} \ 44 | --set http.url=http://httpbin.default/get >> iter8.yaml 45 | 46 | - name: Lint Kubernetes YAML file 47 | if: steps.modified-files.outputs.any_modified == 'true' 48 | uses: stackrox/kube-linter-action@v1 49 | with: 50 | directory: iter8.yaml 51 | 52 | grpc: 53 | name: Lint gRPC performance test 54 | runs-on: ubuntu-latest 55 | 56 | steps: 57 | - name: Check out code 58 | uses: actions/checkout@v4 59 | 60 | - name: Get modified files in the charts/iter8 folder 61 | id: modified-files 62 | uses: tj-actions/changed-files@v41 63 | with: 64 | files: charts/iter8 65 | 66 | - uses: azure/setup-helm@v3 67 | if: steps.modified-files.outputs.any_modified == 'true' 68 | with: 69 | token: ${{ secrets.GITHUB_TOKEN }} 70 | 71 | - name: Create Kubernetes YAML file 72 | if: steps.modified-files.outputs.any_modified == 'true' 73 | run: | 74 | helm template charts/iter8 \ 75 | --set tasks={grpc} \ 76 | --set grpc.host="hello.default:50051" \ 77 | --set grpc.call="helloworld.Greeter.SayHello" \ 78 | --set grpc.protoURL="https://raw.githubusercontent.com/grpc/grpc-go/master/examples/helloworld/helloworld/helloworld.proto" >> iter8.yaml 79 | 80 | - name: Lint Kubernetes YAML file 81 | if: steps.modified-files.outputs.any_modified == 'true' 82 | uses: stackrox/kube-linter-action@v1 83 | with: 84 | directory: iter8.yaml 85 | -------------------------------------------------------------------------------- /.github/workflows/releaseassets.yaml: -------------------------------------------------------------------------------- 1 | name: Release binaries and Docker image 2 | 3 | # Runs when a release is published 4 | 5 | # Build and publish binaries and release Docker image 6 | # 7 | # NOTE: completion of this task will trigger verifyuserexperience.yaml 8 | # which will test the released image (with released charts) 9 | 10 | on: 11 | release: 12 | types: [published] 13 | 14 | jobs: 15 | build-and-push: 16 | name: Push Iter8 image to Docker Hub 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - name: Get version 23 | run: | 24 | tagref=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') 25 | # Strip "v" prefix from tagref 26 | echo "VERSION=$(echo $tagref | sed -e 's/^v//')" >> $GITHUB_ENV 27 | echo "MAJOR_MINOR_VERSION=$(echo $tagref | sed -e 's/^v//' -e 's,\([0-9]*\.[0-9]*\)\.\([0-9]*\),\1,')" >> $GITHUB_ENV 28 | - name: Get owner 29 | run: | 30 | ownerrepo=${{ github.repository }} 31 | owner=$(echo $ownerrepo | cut -f1 -d/) 32 | if [[ "$owner" == "iter8-tools" ]]; then 33 | owner=iter8 34 | fi 35 | echo "OWNER=$owner" >> $GITHUB_ENV 36 | - uses: docker/setup-buildx-action@v3 37 | - uses: docker/login-action@v3 38 | with: 39 | username: ${{ secrets.DOCKERHUB_USERNAME }} 40 | password: ${{ secrets.DOCKERHUB_SECRET }} 41 | - uses: docker/build-push-action@v5 42 | with: 43 | file: docker/Dockerfile 44 | platforms: linux/amd64,linux/arm64 45 | tags: ${{ env.OWNER }}/iter8:${{ env.VERSION }},${{ env.OWNER }}/iter8:${{ env.MAJOR_MINOR_VERSION }},${{ env.OWNER }}/iter8:latest 46 | push: true 47 | build-args: | 48 | TAG=v${{ env.VERSION }} 49 | -------------------------------------------------------------------------------- /.github/workflows/releasecharts.yaml: -------------------------------------------------------------------------------- 1 | name: Release charts 2 | 3 | # Only runs when charts are pushed 4 | 5 | # Release charts 6 | # 7 | # NOTE: completion of this task will trigger verifyuserexperience.yaml 8 | # which will test the released charts (with released image) 9 | 10 | on: 11 | push: 12 | branches: 13 | - master 14 | paths: 15 | - charts/** 16 | 17 | jobs: 18 | release-charts: 19 | permissions: 20 | contents: write 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Configure Git 29 | run: | 30 | git config user.name "$GITHUB_ACTOR" 31 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 32 | 33 | - name: Install Helm 34 | uses: azure/setup-helm@v3 35 | with: 36 | token: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | - name: Run chart-releaser 39 | uses: helm/chart-releaser-action@v1.5.0 40 | with: 41 | config: config.yaml 42 | env: 43 | CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 44 | -------------------------------------------------------------------------------- /.github/workflows/spellcheck.yaml: -------------------------------------------------------------------------------- 1 | name: Spell check markdown 2 | 3 | # Runs during pull request 4 | 5 | # Spell check markdown 6 | 7 | on: 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | spell-check: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - run: | 20 | pwd 21 | ls -l 22 | - uses: rojopolis/spellcheck-github-actions@0.35.0 23 | with: 24 | config_path: .github/spellcheck.yml 25 | -------------------------------------------------------------------------------- /.github/workflows/unittest.yaml: -------------------------------------------------------------------------------- 1 | name: Unit test 2 | 3 | # Runs during pull request 4 | 5 | # Always needs to pass in order for PR to be accepted 6 | 7 | on: 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | unit-test: 14 | name: unit-test 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Install Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: 1.21 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v4 24 | 25 | - name: Test and compute coverage 26 | run: make coverage # includes vet and lint 27 | 28 | - name: Enforce coverage 29 | run: | 30 | export COVERAGE=$(go tool cover -func coverage.out | grep total | awk '{print substr($3, 1, length($3)-1)}') 31 | echo "code coverage is at ${COVERAGE}" 32 | if [ 1 -eq "$(echo "${COVERAGE} > 76.0" | bc)" ]; then \ 33 | echo "all good... coverage is above 76.0%"; 34 | else \ 35 | echo "not good... coverage is not above 76.0%"; 36 | exit 1 37 | fi 38 | -------------------------------------------------------------------------------- /.github/workflows/versionbump.yaml: -------------------------------------------------------------------------------- 1 | name: Version bump check 2 | 3 | # Only runs when charts have changed 4 | 5 | # Check if the version number of changed charts have been bumped 6 | 7 | on: 8 | pull_request: 9 | branches: 10 | - master 11 | paths: 12 | - charts/** 13 | 14 | jobs: 15 | # Get the paths for the Helm charts to version check 16 | get_paths: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | with: 22 | fetch-depth: 0 23 | 24 | - name: Get the paths for Helm charts to version check 25 | id: set-matrix 26 | run: | 27 | # Get paths (in string form) 28 | stringPaths=$(find -maxdepth 2 -path './charts/*') 29 | 30 | # Check paths (length greater than 0) 31 | stringPathsLength=$(echo ${#stringPaths}) 32 | if (( stringPathsLength == 0 )); 33 | then 34 | echo "No paths to check" 35 | exit 1 36 | fi 37 | 38 | # Serialize paths into JSON array 39 | paths=$(jq -ncR '[inputs]' <<< "$stringPaths") 40 | echo $paths 41 | 42 | # Output serialized paths 43 | echo "matrix=$paths" >> $GITHUB_OUTPUT 44 | 45 | outputs: 46 | matrix: ${{ steps.set-matrix.outputs.matrix }} 47 | 48 | # Version check Helm charts based on paths provided by previous job 49 | version_check: 50 | name: Version check 51 | needs: get_paths 52 | runs-on: ubuntu-latest 53 | strategy: 54 | matrix: 55 | version: ${{ fromJson(needs.get_paths.outputs.matrix) }} 56 | steps: 57 | - uses: actions/checkout@v4 58 | with: 59 | fetch-depth: 0 60 | 61 | - name: Get modified files in the ${{ matrix.version }} folder 62 | id: modified-files 63 | uses: tj-actions/changed-files@v41 64 | with: 65 | files: ${{ matrix.version }} 66 | 67 | - name: Run step if any file(s) in the ${{ matrix.version }} folder was modified 68 | if: steps.modified-files.outputs.any_modified == 'true' 69 | run: | 70 | # Remove ./ prefix from raw matrix version (i.e. ./charts/iter8 -> charts/iter8) 71 | version=$(echo ${{ matrix.version }} | sed s/".\/"//) 72 | 73 | # Get chart file 74 | chartFile="$version/Chart.yaml" 75 | 76 | # Get git diff of the Chart.yaml between the master branch and PR branch 77 | gitDiff=$(git diff origin/master..HEAD -- $chartFile) 78 | echo $gitDiff 79 | 80 | # Addition in Chart.yaml 81 | addChart="+++ b/$add$chartFile" 82 | echo $addChart 83 | 84 | # Addition of version in Chart.yaml 85 | addVersion="+version:" 86 | echo $addVersion 87 | 88 | if [[ "$gitDiff" == *"$addChart"* ]] && [[ "$gitDiff" == *$addVersion* ]]; 89 | then 90 | echo "version in $chartFile has been modified" 91 | else 92 | echo "version in $chartFile needs to be modified" 93 | exit 1 94 | fi -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | coverage.out 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build/generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | # Python 110 | *.py[ocd] 111 | *__pycache__/ 112 | *venv* 113 | *.venv* 114 | 115 | # material (built from npm) 116 | material 117 | 118 | # site (built from mkdocs) 119 | site 120 | 121 | # backup pptx 122 | ~$*.pptx 123 | 124 | # vscode 125 | .vscode 126 | 127 | # Helm chart.lock 128 | Chart.lock 129 | 130 | # Iter8 binary and yamls and reports 131 | **/experiment.yaml 132 | !testdata/experiment.yaml 133 | **/result.yaml 134 | **/report.html 135 | **/*.metrics.yaml 136 | !testdata/metrics/*.metrics.yaml 137 | 138 | bin/ 139 | _dist/ 140 | docker/iter8 141 | **/ghz.proto 142 | **/ghz-call-data.json 143 | **/ghz-call-data.bin 144 | **/ghz-call-metadata.json 145 | 146 | !testdata/charts 147 | !testdata/charts/iter8 148 | 149 | # iter8lib tgz 150 | iter8lib*.tgz 151 | 152 | # data 153 | payload.dat 154 | 155 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Refer to golangci-lint's example config file for more options and information: 2 | # https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml 3 | 4 | run: 5 | timeout: 10m 6 | modules-download-mode: readonly 7 | 8 | linters: 9 | enable: 10 | - errcheck 11 | - gosimple 12 | - ineffassign 13 | - typecheck 14 | - unused 15 | - goimports 16 | - revive 17 | - govet 18 | - staticcheck 19 | - gosec 20 | - misspell 21 | 22 | issues: 23 | exclude-use-default: false 24 | max-issues-per-linter: 0 25 | max-same-issues: 0 -------------------------------------------------------------------------------- /.lycheeignore: -------------------------------------------------------------------------------- 1 | http://localhost:8000/ -------------------------------------------------------------------------------- /ADOPTERS.md: -------------------------------------------------------------------------------- 1 | # Adopters 2 | 3 | If you are starting to use Iter8, we would love to see you in the list below. Please raise a PR to add yourself to this list. 4 | 5 | | Organization / Project / Company | Contact(s) | 6 | | --- | --- | 7 | | IBM Cloud (DevOps Toolchains) | [Michael Kalantar](https://github.com/kalantar), [Srinivasan Parthasarathy](https://github.com/sriumcp) | 8 | | IBM Research Cloud Innovation Lab | [Atin Sood](https://github.com/atinsood)| 9 | | IBM Cloud (Code Engine) | [Doug Davis](https://github.com/duglin) | 10 | | ChaosNative (LitmusChaos) | [Shubham Chaudhary](https://github.com/ispeakc0de) | 11 | | Seldon Core | [Clive Cox](https://github.com/cliveseldon) | 12 | | Datagrate, Inc. (jetic.io) | [Mert Öztürk](https://github.com/mertdotcc) | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing or otherwise unacceptable behavior may be reported by contacting the project team at iter8tools@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it seems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This document contains a list of maintainers for Iter8. 4 | 5 | | Maintainer | GitHub ID | Email | 6 | |---------------------------| --------------------------------------- | ------------------- | 7 | | Michael Kalantar | [kalantar](https://github.com/kalantar) | kalantar@us.ibm.com | 8 | | Alan Cha | [Ala-Cha](https://github.com/Alan-Cha) | Alan.Cha1@ibm.com | 9 | | Srinivasan Parthasarathy | [sriumcp](https://github.com/sriumcp) | spartha@us.ibm.com | 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BINDIR := $(CURDIR)/bin 2 | INSTALL_PATH ?= /usr/local/bin 3 | BINNAME ?= iter8 4 | 5 | GOBIN = $(shell go env GOBIN) 6 | ifeq ($(GOBIN),) 7 | GOBIN = $(shell go env GOPATH)/bin 8 | endif 9 | 10 | # go options 11 | TAGS := 12 | LDFLAGS := -w -s 13 | GOFLAGS := 14 | 15 | # Rebuild the binary if any of these files change 16 | SRC := $(shell find . -type f -name '*.(go|proto|tpl)' -print) go.mod go.sum 17 | 18 | # Required for globs to work correctly 19 | SHELL = /usr/bin/env bash 20 | 21 | GIT_COMMIT = $(shell git rev-parse HEAD) 22 | GIT_TAG = $(shell git describe --tags --dirty) 23 | 24 | ifdef VERSION 25 | BINARY_VERSION = $(VERSION) 26 | endif 27 | BINARY_VERSION ?= ${GIT_TAG} 28 | 29 | # Only set Version if GIT_TAG or VERSION is set 30 | ifneq ($(BINARY_VERSION),) 31 | LDFLAGS += -X github.com/iter8-tools/iter8/base.Version=${BINARY_VERSION} 32 | endif 33 | 34 | 35 | LDFLAGS += -X github.com/iter8-tools/iter8/cmd.gitCommit=${GIT_COMMIT} 36 | 37 | .PHONY: all 38 | all: build 39 | 40 | # ------------------------------------------------------------------------------ 41 | # build 42 | 43 | .PHONY: build 44 | build: $(BINDIR)/$(BINNAME) 45 | 46 | $(BINDIR)/$(BINNAME): $(SRC) 47 | GO111MODULE=on go build $(GOFLAGS) -trimpath -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME) ./ 48 | 49 | # ------------------------------------------------------------------------------ 50 | # install 51 | 52 | .PHONY: install 53 | install: build 54 | @install "$(BINDIR)/$(BINNAME)" "$(INSTALL_PATH)/$(BINNAME)" 55 | 56 | # ------------------------------------------------------------------------------ 57 | # dependencies 58 | 59 | .PHONY: clean 60 | clean: 61 | @rm -rf '$(BINDIR)' 62 | 63 | # ------------------------------------------------------------------------------ 64 | # test 65 | 66 | .PHONY: fmt 67 | fmt: ## Run go fmt against code. 68 | go fmt ./... 69 | 70 | .PHONY: vet 71 | vet: ## Run go vet against code 72 | go vet ./... 73 | 74 | .PHONY: golangci-lint 75 | golangci-lint: 76 | golangci-lint run ./... 77 | 78 | .PHONY: lint 79 | lint: vet golangci-lint 80 | 81 | .PHONY: test 82 | test: fmt vet ## Run tests. 83 | go test -v ./... -coverprofile=coverage.out 84 | 85 | .PHONY: coverage 86 | coverage: test 87 | @echo "test coverage: $(shell go tool cover -func coverage.out | grep total | awk '{print substr($$3, 1, length($$3)-1)}')" 88 | 89 | .PHONY: htmlcov 90 | htmlcov: coverage 91 | go tool cover -html=coverage.out 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Iter8: Kubernetes Release Optimizer 2 | 3 | [![Iter8 release](https://img.shields.io/github/v/release/iter8-tools/iter8?sort=semver)](https://github.com/iter8-tools/iter8/releases) 4 | [![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/github.com/iter8-tools/iter8) 5 | 6 | Iter8 is the Kubernetes release optimizer built for DevOps, MLOps, SRE and data science teams. Iter8 makes it easy to ensure that Kubernetes apps and ML models perform well and maximize business value. 7 | 8 | Iter8 supports the following use-cases: 9 | 10 | 1. Progressive release with automated traffic management 11 | 2. A/B/n testing with a client SDK and business metrics 12 | 3. Performance testing for HTTP and gRPC endpoints 13 | 14 | Any Kubernetes resource type, including CRDs can be used with Iter8. 15 | 16 | ## :rocket: Features 17 | 18 | Iter8 introduces a set of tasks which can be composed in order to conduct tests. 19 | 20 |

21 | 22 |

23 | 24 | Iter8 packs a number of powerful features that facilitate Kubernetes application and ML model testing. They include the following: 25 | 26 | 1. **Use any resource types.** Iter8 is easily extensible so that an application being tested can be composed of any resource types including CRDs. 27 | 2. **Client SDK.** A client SDK enables application frontend components to reliably associate business metrics with the contributing version of the backend thereby enabling A/B/n testing of backends. 28 | 3. **Composable test tasks.** Performance test tasks include load generation and metrics storage simplifying setup. 29 | 30 | Please see [https://iter8.tools](https://iter8.tools) for the complete documentation. 31 | 32 | ## :maple_leaf: Issues 33 | Iter8 issues are tracked [here](https://github.com/iter8-tools/iter8/issues). 34 | 35 | ## :tada: Contributing 36 | We welcome PRs! 37 | 38 | See [here](CONTRIBUTING.md) for information about ways to contribute, finding an issue, asking for help, pull-request lifecycle, and more. 39 | 40 | ## :hibiscus: Credits 41 | Iter8 is primarily written in `Go` and builds on a few awesome open source projects including: 42 | 43 | - [Helm](https://helm.sh) 44 | - [Istio](https://istio.io) 45 | - [Kubernetes Gateway API](https://gateway-api.sigs.k8s.io/) 46 | - [Fortio](https://github.com/fortio/fortio) 47 | - [ghz](https://ghz.sh) 48 | - [Grafana](https://grafana.com/) 49 | -------------------------------------------------------------------------------- /abn/grpc/abn.proto: -------------------------------------------------------------------------------- 1 | // protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative abn/grpc/abn.proto 2 | // python -m grpc_tools.protoc -I../../../iter8-tools/iter8/abn/grpc --python_out=. --grpc_python_out=. ../../../iter8-tools/iter8/abn/grpc/abn.proto 3 | 4 | syntax = "proto3"; 5 | 6 | option go_package = "github.com/iter8-tools/iter8/abn/grpc"; 7 | 8 | import "google/protobuf/empty.proto"; 9 | package main; 10 | 11 | // for more information, see https://github.com/iter8-tools/iter8/issues/1257 12 | 13 | service ABN { 14 | // Identify a version (index) the caller should send a request to. 15 | // Should be called for each request (transaction). 16 | rpc Lookup(Application) returns(VersionRecommendation) {} 17 | 18 | // Write a metric value to metrics database. 19 | // The metric value is explicitly associated with a list of transactions that contributed to its computation. 20 | // The user is expected to identify these transactions. 21 | rpc WriteMetric(MetricValue) returns (google.protobuf.Empty) {} 22 | } 23 | 24 | message Application { 25 | // name of (backend) application or service 26 | // This value is used to identify the Kubernetes objects that make up the service 27 | // Kubernetes objects that comprise the service should have the label app.kubernetes.io/name set to name 28 | string name = 1; 29 | // User or user session identifier 30 | string user = 2; 31 | } 32 | 33 | message VersionRecommendation { 34 | // versionNumber index of an application version 35 | int32 versionNumber = 1; 36 | } 37 | 38 | message MetricValue { 39 | // Metric name 40 | string name = 1; 41 | // Metric value 42 | string value = 2; 43 | // name of application 44 | string application = 3; 45 | // User or user session identifier 46 | string user = 4; 47 | } 48 | 49 | // https://developers.google.com/protocol-buffers/docs/proto3 -------------------------------------------------------------------------------- /abn/service_impl_test.go: -------------------------------------------------------------------------------- 1 | package abn 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dgraph-io/badger/v4" 7 | "github.com/google/uuid" 8 | util "github.com/iter8-tools/iter8/base" 9 | "github.com/iter8-tools/iter8/storage/badgerdb" 10 | storageclient "github.com/iter8-tools/iter8/storage/client" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | // tests that we get the same result for the same inputs 15 | func TestLookupInternal(t *testing.T) { 16 | var err error 17 | // set up test metrics db for recording users 18 | tempDirPath := t.TempDir() 19 | storageclient.MetricsClient, err = badgerdb.GetClient(badger.DefaultOptions(tempDirPath), badgerdb.AdditionalOptions{}) 20 | assert.NoError(t, err) 21 | 22 | // setup: add desired routemaps to allRoutemaps 23 | testRM := testRoutemaps{ 24 | allroutemaps: setupRoutemaps(t, *getTestRM("default", "test")), 25 | } 26 | allRoutemaps = &testRM 27 | 28 | tries := 20 // needs to be big enough to find at least one problem; this is probably overkill 29 | // do lookup tries times 30 | versionNumbers := make([]int, tries) 31 | for i := 0; i < tries; i++ { 32 | _, v, err := lookupInternal("default/test", "user") 33 | assert.NoError(t, err) 34 | versionNumbers[i] = v 35 | } 36 | 37 | tr := versionNumbers[0] 38 | for i := 1; i < tries; i++ { 39 | assert.Equal(t, tr, versionNumbers[i]) 40 | } 41 | } 42 | 43 | func TestWeights(t *testing.T) { 44 | var err error 45 | 46 | // set up test metrics db for recording users 47 | tempDirPath := t.TempDir() 48 | storageclient.MetricsClient, err = badgerdb.GetClient(badger.DefaultOptions(tempDirPath), badgerdb.AdditionalOptions{}) 49 | assert.NoError(t, err) 50 | 51 | // setup: add desired routemaps to allRoutemaps 52 | testRM := testRoutemaps{ 53 | allroutemaps: setupRoutemaps(t, *getWeightedTestRM("default", "test", []uint32{3, 1})), 54 | } 55 | allRoutemaps = &testRM 56 | 57 | tries := 100 58 | versionNumbers := make([]int, tries) 59 | for i := 0; i < tries; i++ { 60 | _, v, err := lookupInternal("default/test", uuid.NewString()) 61 | assert.NoError(t, err) 62 | versionNumbers[i] = v 63 | } 64 | 65 | // expect 3/4 will be for version 0 (weight 3); ie, 75 66 | // expect 1/4 will be for version 1 (weight 1); ie, 25 67 | // compute number for version 1 by summing versionNumbers 68 | // assert less than 30 (bigger than 25) 69 | // there is a slight possibility of test failure 70 | 71 | sum := 0 72 | for i := 1; i < tries; i++ { 73 | sum += versionNumbers[i] 74 | } 75 | assert.Less(t, sum, 30) 76 | } 77 | 78 | func getWeightedTestRM(namespace, name string, weights []uint32) *testroutemap { 79 | copyWeights := make([]uint32, len(weights)) 80 | versions := make([]testversion, len(weights)) 81 | for i := range weights { 82 | copyWeights[i] = weights[i] 83 | versions[i] = testversion{signature: util.StringPointer(uuid.NewString())} 84 | } 85 | 86 | return &testroutemap{ 87 | namespace: namespace, 88 | name: name, 89 | versions: versions, 90 | normalizedWeights: copyWeights, 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /abn/test_helpers.go: -------------------------------------------------------------------------------- 1 | package abn 2 | 3 | import "github.com/iter8-tools/iter8/controllers" 4 | 5 | type testroutemapsByName map[string]*testroutemap 6 | type testroutemaps struct { 7 | nsRoutemap map[string]testroutemapsByName 8 | } 9 | 10 | func (s *testroutemaps) GetRoutemapFromNamespaceName(namespace string, name string) controllers.RoutemapInterface { 11 | rmByName, ok := s.nsRoutemap[namespace] 12 | if ok { 13 | return rmByName[name] 14 | } 15 | return nil 16 | } 17 | 18 | type testversion struct { 19 | signature *string 20 | } 21 | 22 | func (v *testversion) GetSignature() *string { 23 | return v.signature 24 | } 25 | 26 | type testroutemap struct { 27 | name string 28 | namespace string 29 | versions []testversion 30 | normalizedWeights []uint32 31 | } 32 | 33 | func (s *testroutemap) RLock() {} 34 | 35 | func (s *testroutemap) RUnlock() {} 36 | 37 | func (s *testroutemap) GetNamespace() string { 38 | return s.namespace 39 | } 40 | 41 | func (s *testroutemap) GetName() string { 42 | return s.name 43 | } 44 | 45 | func (s *testroutemap) Weights() []uint32 { 46 | return s.normalizedWeights 47 | } 48 | 49 | func (s *testroutemap) GetVersions() []controllers.VersionInterface { 50 | result := make([]controllers.VersionInterface, len(s.versions)) 51 | for i := range s.versions { 52 | v := s.versions[i] 53 | result[i] = controllers.VersionInterface(&v) 54 | } 55 | return result 56 | } 57 | 58 | type testRoutemaps struct { 59 | allroutemaps testroutemaps 60 | } 61 | 62 | func (cm *testRoutemaps) GetAllRoutemaps() controllers.RoutemapsInterface { 63 | return &cm.allroutemaps 64 | } 65 | -------------------------------------------------------------------------------- /action/doc.go: -------------------------------------------------------------------------------- 1 | // Package action contains the logic for each action that Iter8 can perform. 2 | // 3 | // This is a library for calling top-level Iter8 actions like 'launch' and 'assert'. 4 | // Actions approximately match the command line invocations that the Iter8 CLI uses. 5 | package action 6 | -------------------------------------------------------------------------------- /action/run.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "github.com/iter8-tools/iter8/base" 5 | "github.com/iter8-tools/iter8/driver" 6 | ) 7 | 8 | // RunOpts are the options used for running an experiment 9 | type RunOpts struct { 10 | // Rundir is the directory of the local experiment.yaml file 11 | RunDir string 12 | 13 | // KubeDriver enables Kubernetes experiment run 14 | *driver.KubeDriver 15 | } 16 | 17 | // NewRunOpts initializes and returns run opts 18 | func NewRunOpts(kd *driver.KubeDriver) *RunOpts { 19 | return &RunOpts{ 20 | RunDir: ".", 21 | KubeDriver: kd, 22 | } 23 | } 24 | 25 | // KubeRun runs a Kubernetes experiment 26 | func (rOpts *RunOpts) KubeRun() error { 27 | // initialize kube driver 28 | if err := rOpts.KubeDriver.InitKube(); err != nil { 29 | return err 30 | } 31 | 32 | return base.RunExperiment(rOpts.KubeDriver) 33 | } 34 | -------------------------------------------------------------------------------- /action/run_test.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "testing" 11 | 12 | "fortio.org/fortio/fhttp" 13 | "github.com/iter8-tools/iter8/base" 14 | "github.com/iter8-tools/iter8/driver" 15 | "github.com/stretchr/testify/assert" 16 | "helm.sh/helm/v3/pkg/cli" 17 | corev1 "k8s.io/api/core/v1" 18 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 | ) 20 | 21 | const ( 22 | myName = "myName" 23 | myNamespace = "myNamespace" 24 | ) 25 | 26 | func TestKubeRun(t *testing.T) { 27 | // define METRICS_SERVER_URL 28 | metricsServerURL := "http://iter8.default:8080" 29 | err := os.Setenv(base.MetricsServerURL, metricsServerURL) 30 | assert.NoError(t, err) 31 | 32 | // create and configure HTTP endpoint for testing 33 | mux, addr := fhttp.DynamicHTTPServer(false) 34 | url := fmt.Sprintf("http://127.0.0.1:%d/get", addr.Port) 35 | var verifyHandlerCalled bool 36 | mux.HandleFunc("/get", base.GetTrackingHandler(&verifyHandlerCalled)) 37 | 38 | // mock metrics server 39 | base.StartHTTPMock(t) 40 | metricsServerCalled := false 41 | base.MockMetricsServer(base.MockMetricsServerInput{ 42 | MetricsServerURL: metricsServerURL, 43 | ExperimentResultCallback: func(req *http.Request) { 44 | metricsServerCalled = true 45 | 46 | // check query parameters 47 | assert.Equal(t, myName, req.URL.Query().Get("test")) 48 | assert.Equal(t, myNamespace, req.URL.Query().Get("namespace")) 49 | 50 | // check payload 51 | body, err := io.ReadAll(req.Body) 52 | assert.NoError(t, err) 53 | assert.NotNil(t, body) 54 | 55 | // check payload content 56 | bodyExperimentResult := base.ExperimentResult{} 57 | err = json.Unmarshal(body, &bodyExperimentResult) 58 | assert.NoError(t, err) 59 | assert.NotNil(t, body) 60 | 61 | // no experiment failure 62 | assert.False(t, bodyExperimentResult.Failure) 63 | }, 64 | }) 65 | 66 | _ = os.Chdir(t.TempDir()) 67 | 68 | // create experiment.yaml 69 | base.CreateExperimentYaml(t, base.CompletePath("../testdata", base.ExperimentTemplateFile), url, base.ExperimentFile) 70 | 71 | // fix rOpts 72 | rOpts := NewRunOpts(driver.NewFakeKubeDriver(cli.New())) 73 | 74 | // read experiment from file created above 75 | byteArray, _ := os.ReadFile(base.ExperimentFile) 76 | _, _ = rOpts.Clientset.CoreV1().Secrets("default").Create(context.TODO(), &corev1.Secret{ 77 | ObjectMeta: metav1.ObjectMeta{ 78 | Name: "default", 79 | Namespace: "default", 80 | }, 81 | StringData: map[string]string{base.ExperimentFile: string(byteArray)}, 82 | }, metav1.CreateOptions{}) 83 | 84 | err = rOpts.KubeRun() 85 | assert.NoError(t, err) 86 | // sanity check -- handler was called 87 | assert.True(t, verifyHandlerCalled) 88 | assert.True(t, metricsServerCalled) 89 | } 90 | -------------------------------------------------------------------------------- /base/doc.go: -------------------------------------------------------------------------------- 1 | // Package base defines Iter8's experiment, task and metric data structures. 2 | // It contains the core logic for running an experiment. 3 | package base 4 | -------------------------------------------------------------------------------- /base/insights_test.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestTrackVersionStr(t *testing.T) { 10 | scenarios := map[string]struct { 11 | in Insights 12 | expectedStr string 13 | }{ 14 | "VersionNames is nil": {in: Insights{}, expectedStr: "version 0"}, 15 | "Version and Track empty": {in: Insights{VersionNames: []VersionInfo{}}, expectedStr: "version 0"}, 16 | "Track is empty": {in: Insights{VersionNames: []VersionInfo{{Version: "version"}}}, expectedStr: "version"}, 17 | "Version is empty": {in: Insights{VersionNames: []VersionInfo{{Track: "track"}}}, expectedStr: "track"}, 18 | "Version and Track not empty": {in: Insights{VersionNames: []VersionInfo{{Track: "track", Version: "version"}}}, expectedStr: "track (version)"}, 19 | } 20 | 21 | for l, s := range scenarios { 22 | t.Run(l, func(t *testing.T) { 23 | assert.Equal(t, s.expectedStr, s.in.TrackVersionStr(0)) 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /base/internal/common.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | /* 4 | Credit: This file is sourced from https://github.com/bojand/ghz and modified for reuse in Iter8 5 | */ 6 | 7 | import ( 8 | "net" 9 | 10 | "google.golang.org/grpc" 11 | "google.golang.org/grpc/reflection" 12 | 13 | "github.com/iter8-tools/iter8/base/internal/helloworld/helloworld" 14 | ) 15 | 16 | // LocalHostPort is the localhost:12345 combo used for testing 17 | const LocalHostPort = "localhost:12345" 18 | 19 | // StartServer starts the server. 20 | // 21 | // For testing only. 22 | func StartServer(_ bool) (*helloworld.Greeter, *grpc.Server, error) { 23 | lis, err := net.Listen("tcp", LocalHostPort) 24 | if err != nil { 25 | return nil, nil, err 26 | } 27 | 28 | var opts []grpc.ServerOption 29 | 30 | stats := helloworld.NewHWStats() 31 | 32 | opts = append(opts, grpc.StatsHandler(stats)) 33 | 34 | s := grpc.NewServer(opts...) 35 | 36 | gs := helloworld.NewGreeter() 37 | helloworld.RegisterGreeterServer(s, gs) 38 | reflection.Register(s) 39 | 40 | gs.Stats = stats 41 | 42 | go func() { 43 | _ = s.Serve(lis) 44 | }() 45 | 46 | return gs, s, err 47 | } 48 | -------------------------------------------------------------------------------- /base/internal/doc.go: -------------------------------------------------------------------------------- 1 | // Package internal provides gRPC code used for testing load-test-grpc 2 | package internal 3 | -------------------------------------------------------------------------------- /base/internal/helloworld/helloworld/doc.go: -------------------------------------------------------------------------------- 1 | // Package helloworld implements the helloworld grpc service. 2 | // This package is used for testing the load-test-grpc task 3 | package helloworld 4 | -------------------------------------------------------------------------------- /base/internal/helloworld/helloworld/greeter.proto: -------------------------------------------------------------------------------- 1 | // Credit: This file is from https://github.com/bojand/ghz/ 2 | // this file is used for test purposes 3 | syntax = "proto3"; 4 | 5 | option go_package = "github.com/iter8-tools/iter8/base/internal/helloworld/helloworld"; 6 | 7 | package helloworld; 8 | 9 | service Greeter { 10 | rpc SayHello (HelloRequest) returns (HelloReply) {} 11 | rpc SayHelloCS (stream HelloRequest) returns (HelloReply) {} 12 | rpc SayHellos (HelloRequest) returns (stream HelloReply) {} 13 | rpc SayHelloBidi (stream HelloRequest) returns (stream HelloReply) {} 14 | } 15 | 16 | // The request message containing the user's name. 17 | message HelloRequest { 18 | string name = 1; 19 | } 20 | 21 | // The response message containing the greetings 22 | message HelloReply { 23 | string message = 1; 24 | } -------------------------------------------------------------------------------- /base/kubedriver.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "errors" 5 | 6 | // Import to initialize client auth plugins. 7 | _ "k8s.io/client-go/plugin/pkg/client/auth" 8 | 9 | "github.com/iter8-tools/iter8/base/log" 10 | 11 | "helm.sh/helm/v3/pkg/cli" 12 | 13 | "k8s.io/client-go/dynamic" 14 | ) 15 | 16 | var ( 17 | kd = NewKubeDriver(cli.New()) 18 | ) 19 | 20 | // KubeDriver embeds Kube configuration, and 21 | // enables interaction with a Kubernetes cluster through Kube APIs 22 | type KubeDriver struct { 23 | // EnvSettings provides generic Kubernetes options 24 | *cli.EnvSettings 25 | // dynamicClient enables unstructured interaction with a Kubernetes cluster 26 | dynamicClient dynamic.Interface 27 | } 28 | 29 | // NewKubeDriver creates and returns a new KubeDriver 30 | func NewKubeDriver(s *cli.EnvSettings) *KubeDriver { 31 | kd := &KubeDriver{ 32 | EnvSettings: s, 33 | dynamicClient: nil, 34 | } 35 | return kd 36 | } 37 | 38 | // initKube initializes the Kubernetes clientset 39 | func (kd *KubeDriver) initKube() (err error) { 40 | if kd.dynamicClient == nil { 41 | // get REST config 42 | restConfig, err := kd.EnvSettings.RESTClientGetter().ToRESTConfig() 43 | if err != nil { 44 | e := errors.New("unable to get Kubernetes REST config") 45 | log.Logger.WithStackTrace(err.Error()).Error(e) 46 | return e 47 | } 48 | kd.dynamicClient, err = dynamic.NewForConfig(restConfig) 49 | if err != nil { 50 | e := errors.New("unable to get Kubernetes dynamic client") 51 | log.Logger.WithStackTrace(err.Error()).Error(e) 52 | return e 53 | } 54 | } 55 | 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /base/kubedriver_test.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "helm.sh/helm/v3/pkg/cli" 8 | ) 9 | 10 | func TestInitKube(t *testing.T) { 11 | kubeDriver := NewKubeDriver(cli.New()) 12 | err := kubeDriver.initKube() 13 | 14 | assert.NoError(t, err) 15 | } 16 | -------------------------------------------------------------------------------- /base/log/doc.go: -------------------------------------------------------------------------------- 1 | // Package log enables logging for Iter8. 2 | package log 3 | -------------------------------------------------------------------------------- /base/log/log.go: -------------------------------------------------------------------------------- 1 | // Package log provides primitives for logging. 2 | package log 3 | 4 | import ( 5 | "bufio" 6 | "strings" 7 | 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // Level is the log level for Iter8 CLI 12 | var Level = logrus.InfoLevel 13 | 14 | // Iter8Logger inherits all methods from logrus logger. 15 | // Provides additional methods for standardized Iter8 logging. 16 | type Iter8Logger struct { 17 | *logrus.Logger 18 | } 19 | 20 | // StackTrace is the trace from external components like a shell scripts run by an Iter8 task. 21 | type StackTrace struct { 22 | // prefix is the string with which external traces are prefixed 23 | prefix string 24 | // Trace is the raw trace 25 | Trace string 26 | } 27 | 28 | // Logger to be used in all of Iter8. 29 | var Logger *Iter8Logger 30 | 31 | // init initializes the logger. 32 | func init() { 33 | Logger = &Iter8Logger{logrus.New()} 34 | Logger.SetFormatter(&logrus.TextFormatter{ 35 | TimestampFormat: "2006-01-02 15:04:05", 36 | FullTimestamp: true, 37 | DisableQuote: true, 38 | DisableSorting: true, 39 | }) 40 | 41 | Logger.SetLevel(Level) 42 | } 43 | 44 | // WithStackTrace yields a log entry with a formatted stack trace field embedded in it. 45 | func (l *Iter8Logger) WithStackTrace(t string) *logrus.Entry { 46 | return l.WithField("stack-trace", &StackTrace{ 47 | prefix: "::Trace:: ", 48 | Trace: t, 49 | }) 50 | } 51 | 52 | // WithIndentedTrace yields a log entry with a formatted indent embedded in it. 53 | func (l *Iter8Logger) WithIndentedTrace(t string) *logrus.Entry { 54 | return l.WithField("indented-trace", &StackTrace{ 55 | prefix: " ", 56 | Trace: t, 57 | }) 58 | } 59 | 60 | // String processes stack traces by prefixing each line of the trace with prefix. 61 | // This enables other tools like grep to easily filter out these traces if needed. 62 | func (st *StackTrace) String() string { 63 | out := "below ... \n" 64 | scanner := bufio.NewScanner(strings.NewReader(st.Trace)) 65 | for scanner.Scan() { 66 | out += st.prefix + scanner.Text() + "\n" 67 | } 68 | out = strings.TrimSuffix(out, "\n") 69 | return out 70 | } 71 | -------------------------------------------------------------------------------- /base/log/log_test.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestStackTrace(t *testing.T) { 11 | Logger.WithIndentedTrace("hello there") 12 | Logger.WithStackTrace("hello there") 13 | 14 | st := StackTrace{ 15 | prefix: "::Trace:: ", 16 | Trace: fmt.Sprintln("a") + fmt.Sprintln("b"), 17 | } 18 | assert.Contains(t, st.String(), "::Trace:: a") 19 | assert.Contains(t, st.String(), "::Trace:: b") 20 | 21 | } 22 | -------------------------------------------------------------------------------- /base/metrics.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | 10 | log "github.com/iter8-tools/iter8/base/log" 11 | ) 12 | 13 | const ( 14 | // MetricsServerURL is the URL of the metrics server 15 | MetricsServerURL = "METRICS_SERVER_URL" 16 | 17 | // TestResultPath is the path to the PUT /testResult endpoint 18 | TestResultPath = "/testResult" 19 | 20 | // AbnDashboard is the path to the GET /abnDashboard endpoint 21 | AbnDashboard = "/abnDashboard" 22 | // HTTPDashboardPath is the path to the GET /httpDashboard endpoint 23 | HTTPDashboardPath = "/httpDashboard" 24 | // GRPCDashboardPath is the path to the GET /grpcDashboard endpoint 25 | GRPCDashboardPath = "/grpcDashboard" 26 | ) 27 | 28 | // callMetricsService is a general function that can be used to send data to the metrics service 29 | func callMetricsService(method, metricsServerURL, path string, queryParams map[string]string, payload interface{}) error { 30 | // handle URL and URL parameters 31 | u, err := url.ParseRequestURI(metricsServerURL + path) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | params := url.Values{} 37 | for paramKey, paramValue := range queryParams { 38 | params.Add(paramKey, paramValue) 39 | } 40 | u.RawQuery = params.Encode() 41 | urlStr := fmt.Sprintf("%v", u) 42 | 43 | log.Logger.Trace(fmt.Sprintf("call metrics service URL: %s", urlStr)) 44 | 45 | // handle payload 46 | dataBytes, err := json.Marshal(payload) 47 | if err != nil { 48 | log.Logger.Error("cannot JSON marshal data for metrics server request: ", err) 49 | return err 50 | } 51 | 52 | // create request 53 | req, err := http.NewRequest(method, urlStr, bytes.NewBuffer(dataBytes)) 54 | if err != nil { 55 | log.Logger.Error("cannot create new HTTP request metrics server: ", err) 56 | return err 57 | } 58 | 59 | req.Header.Set("Content-Type", "application/json") 60 | 61 | log.Logger.Trace("sending request") 62 | 63 | // send request 64 | client := &http.Client{} 65 | resp, err := client.Do(req) 66 | if err != nil { 67 | log.Logger.Error("could not send request to metrics server: ", err) 68 | return err 69 | } 70 | defer func() { 71 | err = resp.Body.Close() 72 | if err != nil { 73 | log.Logger.Error("could not close response body: ", err) 74 | } 75 | }() 76 | 77 | log.Logger.Trace("sent request") 78 | 79 | return nil 80 | } 81 | 82 | // PutExperimentResultToMetricsService sends the test result to the metrics service 83 | func PutExperimentResultToMetricsService(metricsServerURL, namespace, experiment string, experimentResult *ExperimentResult) error { 84 | return callMetricsService(http.MethodPut, metricsServerURL, TestResultPath, map[string]string{ 85 | "namespace": namespace, 86 | "test": experiment, 87 | }, experimentResult) 88 | } 89 | -------------------------------------------------------------------------------- /base/run.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | 8 | log "github.com/iter8-tools/iter8/base/log" 9 | ) 10 | 11 | const ( 12 | // RunTaskName is the name of the run task which performs running of a shell script 13 | RunTaskName = "run" 14 | ) 15 | 16 | var ( 17 | // tempDirEnv is a temporary directory 18 | tempDirEnv = fmt.Sprintf("TEMP_DIR=%v", os.TempDir()) 19 | ) 20 | 21 | // runTask enables running a shell script 22 | type runTask struct { 23 | // TaskMeta has fields common to all tasks 24 | TaskMeta 25 | } 26 | 27 | // initializeDefaults sets default values for task inputs 28 | func (t *runTask) initializeDefaults() {} 29 | 30 | // validateInputs for this task 31 | func (t *runTask) validateInputs() error { 32 | return nil 33 | } 34 | 35 | // getCommand gets the executable command 36 | func (t *runTask) getCommand() *exec.Cmd { 37 | cmdStr := *t.TaskMeta.Run 38 | // create command to be executed 39 | // #nosec 40 | cmd := exec.Command("/bin/bash", "-c", cmdStr) 41 | // append the environment variable for temp dir 42 | cmd.Env = append(os.Environ(), tempDirEnv) 43 | return cmd 44 | } 45 | 46 | // run the command 47 | func (t *runTask) run(_ *Experiment) error { 48 | err := t.validateInputs() 49 | if err != nil { 50 | return err 51 | } 52 | 53 | t.initializeDefaults() 54 | 55 | cmd := t.getCommand() 56 | out, err := cmd.CombinedOutput() 57 | if err != nil { 58 | log.Logger.WithStackTrace(err.Error()).Error("combined execution failed") 59 | log.Logger.WithStackTrace(string(out)).Error("combined output from command") 60 | return err 61 | } 62 | log.Logger.WithStackTrace(string(out)).Trace("combined output from command") 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /base/run_test.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestRunRun(t *testing.T) { 11 | _ = os.Chdir(t.TempDir()) 12 | // valid run task... should succeed 13 | rt := &runTask{ 14 | TaskMeta: TaskMeta{ 15 | Run: StringPointer("echo hello"), 16 | }, 17 | } 18 | 19 | exp := &Experiment{ 20 | Spec: []Task{rt}, 21 | Result: &ExperimentResult{}, 22 | } 23 | exp.initResults(1) 24 | err := rt.run(exp) 25 | assert.NoError(t, err) 26 | } 27 | -------------------------------------------------------------------------------- /base/sprigutil.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // Note: the following code snippets are from sprig library 9 | // https://github.com/Masterminds/sprig 10 | 11 | // The following copyright notice is from the sprig library. 12 | // This copyright applies to the code in this file. 13 | // It is included as required by the MIT License under which sprig is released. 14 | 15 | /* 16 | Copyright (C) 2013-2020 Masterminds 17 | 18 | Permission is hereby granted, free of charge, to any person obtaining a copy 19 | of this software and associated documentation files (the "Software"), to deal 20 | in the Software without restriction, including without limitation the rights 21 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 22 | copies of the Software, and to permit persons to whom the Software is 23 | furnished to do so, subject to the following conditions: 24 | 25 | The above copyright notice and this permission notice shall be included in 26 | all copies or substantial portions of the Software. 27 | 28 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 34 | THE SOFTWARE. 35 | */ 36 | 37 | // Uniq deduplicates a list 38 | // We have switched from uniq to Uniq, since we want to use it in other packages 39 | func Uniq(list interface{}) []interface{} { 40 | l, err := mustUniq(list) 41 | if err != nil { 42 | panic(err) 43 | } 44 | 45 | return l 46 | } 47 | 48 | // mustUniq deduplicates a list and returns an error if the type doesn't permit equality checks 49 | // this function has been modified from the sprig implementation, in order to use the 50 | // the two valued inList function 51 | func mustUniq(list interface{}) ([]interface{}, error) { 52 | tp := reflect.TypeOf(list).Kind() 53 | switch tp { 54 | case reflect.Slice, reflect.Array: 55 | l2 := reflect.ValueOf(list) 56 | 57 | l := l2.Len() 58 | dest := []interface{}{} 59 | var item interface{} 60 | for i := 0; i < l; i++ { 61 | item = l2.Index(i).Interface() 62 | if ok, _ := inList(dest, item); !ok { 63 | dest = append(dest, item) 64 | } 65 | } 66 | 67 | return dest, nil 68 | default: 69 | return nil, fmt.Errorf("cannot find uniq on type %s", tp) 70 | } 71 | } 72 | 73 | // inList checks if needle is present in haystack 74 | // this function has been modified from the sprig implementation, in order to return index also 75 | func inList(haystack []interface{}, needle interface{}) (bool, int) { 76 | for i, h := range haystack { 77 | if reflect.DeepEqual(needle, h) { 78 | return true, i 79 | } 80 | } 81 | return false, -1 82 | } 83 | -------------------------------------------------------------------------------- /base/test_helpers_driver.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "helm.sh/helm/v3/pkg/cli" 5 | 6 | "k8s.io/apimachinery/pkg/runtime" 7 | dynamicfake "k8s.io/client-go/dynamic/fake" 8 | ) 9 | 10 | // initKubeFake initialize the Kube clientset with a fake 11 | func initKubeFake(kd *KubeDriver, _ ...runtime.Object) { 12 | kd.dynamicClient = dynamicfake.NewSimpleDynamicClient(runtime.NewScheme()) 13 | } 14 | 15 | // NewFakeKubeDriver creates and returns a new KubeDriver with fake clients 16 | func NewFakeKubeDriver(s *cli.EnvSettings, objects ...runtime.Object) *KubeDriver { 17 | kd := &KubeDriver{ 18 | EnvSettings: s, 19 | } 20 | initKubeFake(kd, objects...) 21 | return kd 22 | } 23 | -------------------------------------------------------------------------------- /base/util_test.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type config struct { 11 | Property *int `json:"property,omitempty"` 12 | } 13 | 14 | func TestReadConfigDefaultProperty(t *testing.T) { 15 | configEnvironnentVariable := "CONFIG" 16 | defaultPropertyValue := 8888 17 | 18 | file, err := os.CreateTemp("/tmp", "test") 19 | assert.NoError(t, err) 20 | assert.NotNil(t, file) 21 | defer func() { 22 | err := os.Remove(file.Name()) 23 | assert.NoError(t, err) 24 | }() 25 | 26 | err = os.Setenv(configEnvironnentVariable, file.Name()) 27 | assert.NoError(t, err) 28 | conf := &config{} 29 | err = ReadConfig(configEnvironnentVariable, conf, func() { 30 | if nil == conf.Property { 31 | conf.Property = IntPointer(defaultPropertyValue) 32 | } 33 | }) 34 | assert.NoError(t, err) 35 | 36 | assert.Equal(t, defaultPropertyValue, *conf.Property) 37 | } 38 | 39 | func TestReadConfigNoEnvVar(t *testing.T) { 40 | configEnvironnentVariable := "CONFIG" 41 | defaultPropertyValue := 8888 42 | 43 | // don't set environment variable 44 | conf := &config{} 45 | err := ReadConfig(configEnvironnentVariable, conf, func() { 46 | if nil == conf.Property { 47 | conf.Property = IntPointer(defaultPropertyValue) 48 | } 49 | }) 50 | assert.Error(t, err) 51 | } 52 | 53 | func TestReadConfigNoFile(t *testing.T) { 54 | configEnvironnentVariable := "CONFIG" 55 | defaultPropertyValue := 8888 56 | 57 | err := os.Setenv(configEnvironnentVariable, "/tmp/noexistant") 58 | assert.NoError(t, err) 59 | conf := &config{} 60 | err = ReadConfig(configEnvironnentVariable, conf, func() { 61 | if nil == conf.Property { 62 | conf.Property = IntPointer(defaultPropertyValue) 63 | } 64 | }) 65 | assert.Error(t, err) 66 | } 67 | 68 | func TestSplitApplication(t *testing.T) { 69 | ns, n := SplitApplication("namespace/name") 70 | assert.Equal(t, "namespace", ns) 71 | assert.Equal(t, "name", n) 72 | 73 | ns, n = SplitApplication("namespace/name/ignored") 74 | assert.Equal(t, "namespace", ns) 75 | assert.Equal(t, "name", n) 76 | 77 | ns, n = SplitApplication("name") 78 | assert.Equal(t, "default", ns) 79 | assert.Equal(t, "name", n) 80 | } 81 | 82 | type testType struct { 83 | S string 84 | I int 85 | Nested struct { 86 | S string 87 | I int 88 | } 89 | } 90 | 91 | func TestToYAML(t *testing.T) { 92 | obj := testType{ 93 | S: "hello world", 94 | I: 3, 95 | Nested: struct { 96 | S string 97 | I int 98 | }{ 99 | S: "nested", 100 | }, 101 | } 102 | 103 | objString := ToYAML(obj) 104 | assert.Equal(t, `I: 3 105 | Nested: 106 | I: 0 107 | S: nested 108 | S: hello world`, string(objString)) 109 | } 110 | -------------------------------------------------------------------------------- /bump-version-hints.md: -------------------------------------------------------------------------------- 1 | These instructions are a guide to making a new major or minor (not patch) release. The challenge is that charts refer to the image version. But this version is only created when the release is published. Consequently the following sequence of steps is needed: 2 | 3 | 1. Modify only golang code (no version bump) 4 | 1. Do not change `MajorMinor` or `Version` in `base/util.go` 5 | 6 | Make new major/minor release 7 | 8 | 2. Bump version references in `/charts` changes and bump `/charts/iter8` chart version (no changes to `/testdata`) and bump Kustomize files and bump verifyUserExperience workflow 9 | 1. The charts are modified to use the new image 10 | 2. The chart versions should be bumped to match the major/minor version (this is required for the `iter8` chart) but is desirable for all 11 | 12 | Merging the chart changes triggers a automatic chart releases 13 | 14 | 3. Version bump golang and `/testdata` and other workflows 15 | 1. Bump `MajorMinor` or `Version` in `base/util.go` 16 | 2. Bump explicit version references in remaining workflows 17 | 3. Bump Dockerfile 18 | 5. Changes to `/testdata` is only a version bump in charts (`iter8.tools/version`) 19 | 20 | Make a release (new patch version) 21 | 22 | *** 23 | 24 | At this point the documentation can be updated to refer to the new version. This usually takes 2 commits (or one with failed link checking) 25 | 26 | Some things to change in the docs: 27 | 28 | * `iter8.tools/version` in Kubernetes manifests samples 29 | * `--version` for any `helm upgrade` and `helm template` commands 30 | * `getting-started/delete.md.md` and `getting-started/install.md` 31 | * Reference to `values.yaml` 32 | -------------------------------------------------------------------------------- /charts/controller/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | 25 | # generated files need to be ignored 26 | experiment.yaml -------------------------------------------------------------------------------- /charts/controller/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: controller 3 | version: 1.1.0 4 | description: Iter8 controller controller 5 | type: application 6 | keywords: 7 | - Iter8 8 | - controller 9 | - experiment 10 | home: https://iter8.tools 11 | sources: 12 | - https://github.com/iter8-tools/iter8 13 | maintainers: 14 | - name: Alan Cha 15 | email: alan.cha1@ibm.com 16 | - name: Iter8 17 | email: iter8-tools@gmail.com 18 | icon: https://github.com/iter8-tools/iter8/raw/master/mkdocs/docs/images/favicon.png 19 | appVersion: v1.1 20 | -------------------------------------------------------------------------------- /charts/controller/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{- define "iter8-controller.name" -}} 2 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 3 | {{- end -}} 4 | 5 | {{- define "iter8-controller.labels" -}} 6 | labels: 7 | app.kubernetes.io/name: {{ template "iter8-controller.name" . }} 8 | helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} 9 | app.kubernetes.io/managed-by: {{ .Release.Service }} 10 | app.kubernetes.io/instance: {{ .Release.Name }} 11 | app.kubernetes.io/version: {{ .Chart.AppVersion }} 12 | {{- end -}} 13 | -------------------------------------------------------------------------------- /charts/controller/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ .Release.Name }} 5 | data: 6 | config.yaml: | 7 | {{ omit .Values "metrics" "abn" | toYaml | indent 4 | trim }} 8 | metrics.yaml: | 9 | {{ toYaml .Values.metrics | indent 4 | trim }} 10 | abn.yaml: | 11 | {{ toYaml .Values.abn | indent 4 | trim }} -------------------------------------------------------------------------------- /charts/controller/templates/persistentvolumeclaim.yaml: -------------------------------------------------------------------------------- 1 | 2 | {{- if or (not .Values.metrics) (not .Values.metrics.implementation) (eq "badgerdb" .Values.metrics.implementation) }} 3 | apiVersion: v1 4 | kind: PersistentVolumeClaim 5 | metadata: 6 | name: {{ .Release.Name }} 7 | {{ template "iter8-controller.labels" . }} 8 | spec: 9 | accessModes: 10 | - ReadWriteOnce 11 | resources: 12 | requests: 13 | storage: {{ default "50Mi" .Values.metrics.badgerdb.storage }} 14 | storageClassName: {{ default "standard" .Values.metrics.badgerdb.storageClassName }} 15 | {{- end }} 16 | -------------------------------------------------------------------------------- /charts/controller/templates/roles.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | {{- if .Values.clusterScoped }} 3 | kind: ClusterRole 4 | {{- else }} 5 | kind: Role 6 | {{- end }} 7 | metadata: 8 | name: {{ $.Release.Name }} 9 | {{ template "iter8-controller.labels" $ }} 10 | rules: 11 | {{- range $typeName, $type := .Values.resourceTypes }} 12 | {{- if not $type.Resource }} 13 | {{- fail (print "resourceType \"" (print $typeName "\" does not have a resource parameter")) }} 14 | {{- end }} 15 | - apiGroups: ["{{- $type.Group -}}"] 16 | resources: ["{{- $type.Resource -}}"] 17 | verbs: ["get", "list", "watch", "patch", "update", "create"] 18 | {{- end }} 19 | - apiGroups: [""] 20 | resources: ["events"] 21 | verbs: ["get", "create"] 22 | - apiGroups: [""] 23 | resources: ["pods"] 24 | verbs: ["get"] 25 | --- 26 | apiVersion: rbac.authorization.k8s.io/v1 27 | {{- if .Values.clusterScoped }} 28 | kind: ClusterRoleBinding 29 | {{- else }} 30 | kind: RoleBinding 31 | {{- end }} 32 | metadata: 33 | name: {{ $.Release.Name }} 34 | {{ template "iter8-controller.labels" $ }} 35 | subjects: 36 | - kind: ServiceAccount 37 | name: {{ $.Release.Name }} 38 | namespace: {{ $.Release.Namespace }} 39 | roleRef: 40 | {{- if .Values.clusterScoped }} 41 | kind: ClusterRole 42 | {{- else }} 43 | kind: Role 44 | {{- end }} 45 | name: {{ $.Release.Name }} 46 | apiGroup: rbac.authorization.k8s.io -------------------------------------------------------------------------------- /charts/controller/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ .Release.Name }} 5 | spec: 6 | selector: 7 | app.kubernetes.io/name: {{ template "iter8-controller.name" . }} 8 | ports: 9 | - name: grpc 10 | port: {{ .Values.abn.port }} 11 | targetPort: {{ .Values.abn.port }} 12 | - name: http 13 | port: {{ .Values.metrics.port }} 14 | targetPort: {{ .Values.metrics.port }} -------------------------------------------------------------------------------- /charts/controller/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ .Release.Name }} 5 | {{ template "iter8-controller.labels" . }} -------------------------------------------------------------------------------- /charts/controller/templates/statefulset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: {{ .Release.Name }} 5 | {{ template "iter8-controller.labels" . }} 6 | spec: 7 | serviceName: {{ .Release.Name }} 8 | replicas: {{ default 1 .Values.replicas | int }} 9 | selector: 10 | matchLabels: 11 | app.kubernetes.io/name: {{ template "iter8-controller.name" . }} 12 | template: 13 | metadata: 14 | labels: 15 | app.kubernetes.io/name: {{ template "iter8-controller.name" . }} 16 | spec: 17 | terminationGracePeriodSeconds: 10 18 | serviceAccountName: {{ .Release.Name }} 19 | containers: 20 | - name: iter8-controller 21 | image: {{ .Values.image }} 22 | imagePullPolicy: Always 23 | command: ["/bin/iter8"] 24 | args: ["controllers", "-l", "{{ .Values.logLevel }}"] 25 | env: 26 | - name: CONFIG_FILE 27 | value: /config/config.yaml 28 | - name: METRICS_CONFIG_FILE 29 | value: /config/metrics.yaml 30 | - name: ABN_CONFIG_FILE 31 | value: /config/abn.yaml 32 | - name: METRICS_DIR 33 | value: /metrics 34 | - name: POD_NAME 35 | valueFrom: 36 | fieldRef: 37 | fieldPath: metadata.name 38 | - name: POD_NAMESPACE 39 | valueFrom: 40 | fieldRef: 41 | fieldPath: metadata.namespace 42 | volumeMounts: 43 | - name: config 44 | mountPath: "/config" 45 | readOnly: true 46 | {{- if or (not .Values.metrics) (not .Values.metrics.implementation) (eq "badgerdb" .Values.metrics.implementation) }} 47 | - name: metrics 48 | mountPath: {{ default "/metrics" .Values.metrics.badgerdb.dir }} 49 | {{- end }} 50 | resources: 51 | {{ toYaml .Values.resources | indent 10 | trim }} 52 | securityContext: 53 | readOnlyRootFilesystem: true 54 | allowPrivilegeEscalation: false 55 | capabilities: 56 | drop: 57 | - ALL 58 | runAsNonRoot: true 59 | runAsUser: 1001040000 60 | volumes: 61 | - name: config 62 | configMap: 63 | name: {{ .Release.Name }} 64 | {{- if or (not .Values.metrics) (not .Values.metrics.implementation) (eq "badgerdb" .Values.metrics.implementation) }} 65 | - name: metrics 66 | persistentVolumeClaim: 67 | claimName: {{ .Release.Name }} 68 | {{- end }} -------------------------------------------------------------------------------- /charts/controller/values.yaml: -------------------------------------------------------------------------------- 1 | ### Controller image 2 | image: iter8/iter8:1.1 3 | replicas: 1 4 | 5 | ### default resync time for controller 6 | defaultResync: 15m 7 | 8 | ### flag indicating whether installed as cluster scoped or (default) namespace scoped 9 | # clusterScoped: true 10 | 11 | ### list of resource types to watch. For each resource type, an Iter8 shortname is mapped to a group, version, and resource. 12 | ### to add types to watch, any shortname can be used 13 | ### Where a condition is identified, it identifies the status condition on an object that should be inspected to determine 14 | ### if the resource is "ready". 15 | resourceTypes: 16 | svc: 17 | Group: "" 18 | Version: v1 19 | Resource: services 20 | service: 21 | Group: "" 22 | Version: v1 23 | Resource: services 24 | cm: 25 | Group: "" 26 | Version: v1 27 | Resource: configmaps 28 | deploy: 29 | Group: apps 30 | Version: v1 31 | Resource: deployments 32 | conditions: 33 | - Available 34 | isvc: 35 | Group: serving.kserve.io 36 | Version: v1beta1 37 | Resource: inferenceservices 38 | conditions: 39 | - Ready 40 | vs: 41 | Group: networking.istio.io 42 | Version: v1beta1 43 | Resource: virtualservices 44 | httproute: 45 | Group: gateway.networking.k8s.io 46 | Version: v1beta1 47 | Resource: httproutes 48 | 49 | ### log level. Must be one of trace, debug, info, warning, error 50 | logLevel: info 51 | 52 | ### Resource limits 53 | resources: 54 | requests: 55 | memory: "64Mi" 56 | cpu: "250m" 57 | limits: 58 | memory: "128Mi" 59 | cpu: "500m" 60 | 61 | ### A/B/n 62 | abn: 63 | # port for Iter8 gRPC service 64 | port: 50051 65 | 66 | ### Metrics 67 | metrics: 68 | # port on which HTTP service (for Grafana) should be exposed 69 | port: 8080 70 | # implementation technology for metrics storage 71 | # Valid values are badgerdb (default) and redis 72 | # The set of properties used to configure the metrics store depend on the 73 | # implementation selected. 74 | implementation: badgerdb 75 | # default properties specific to BadgerDB 76 | badgerdb: 77 | # storage that should be created to support badger db 78 | storage: 50Mi 79 | storageClassName: standard 80 | # location to mount storage 81 | dir: /metrics 82 | # default properties specific to Redis 83 | redis: 84 | address: redis:6379 85 | # password: (default - none) 86 | # username: (default - none) 87 | # db: (default 0) 88 | 89 | 90 | -------------------------------------------------------------------------------- /charts/iter8/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /charts/iter8/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: iter8 3 | version: 1.1.0 4 | description: Iter8 experiment chart 5 | type: application 6 | home: https://iter8.tools 7 | sources: 8 | - https://github.com/iter8-tools/hub 9 | maintainers: 10 | - name: Srinivasan Parthasarathy 11 | email: spartha@us.ibm.com 12 | url: https://researcher.watson.ibm.com/researcher/view.php?person=us-spartha 13 | - name: Michael Kalantar 14 | email: kalantar@us.ibm.com 15 | icon: https://github.com/iter8-tools/iter8/raw/master/mkdocs/docs/images/favicon.png 16 | -------------------------------------------------------------------------------- /charts/iter8/README.md: -------------------------------------------------------------------------------- 1 | # Iter8 test runner chart 2 | 3 | This chart enables Iter8 tests. -------------------------------------------------------------------------------- /charts/iter8/templates/_experiment.tpl: -------------------------------------------------------------------------------- 1 | {{- define "experiment" -}} 2 | {{- if not .Values.tasks }} 3 | {{- fail ".Values.tasks is empty" }} 4 | {{- end }} 5 | metadata: 6 | name: {{ .Release.Name }} 7 | namespace: {{ .Release.Namespace }} 8 | spec: 9 | {{- range .Values.tasks }} 10 | {{- if eq "grpc" . }} 11 | {{- include "task.grpc" $.Values.grpc -}} 12 | {{- else if eq "http" . }} 13 | {{- include "task.http" $.Values.http -}} 14 | {{- else if eq "ready" . }} 15 | {{- include "task.ready" $ -}} 16 | {{- else if eq "slack" . }} 17 | {{- include "task.slack" $.Values.slack -}} 18 | {{- else if eq "github" . }} 19 | {{- include "task.github" $.Values.github -}} 20 | {{- else }} 21 | {{- fail "task name must be one of grpc, http, ready, github, or slack" -}} 22 | {{- end }} 23 | {{- end }} 24 | result: 25 | startTime: {{ now | toJson }} 26 | numCompletedTasks: 0 27 | failure: false 28 | iter8Version: {{ .Values.majorMinor }} 29 | {{- end }} -------------------------------------------------------------------------------- /charts/iter8/templates/_k-job.tpl: -------------------------------------------------------------------------------- 1 | {{- define "k.job" -}} 2 | apiVersion: batch/v1 3 | kind: Job 4 | metadata: 5 | name: {{ .Release.Name }}-{{ .Release.Revision }}-job 6 | annotations: 7 | iter8.tools/test: {{ .Release.Name }} 8 | iter8.tools/revision: {{ .Release.Revision | quote }} 9 | spec: 10 | template: 11 | metadata: 12 | labels: 13 | iter8.tools/test: {{ .Release.Name }} 14 | annotations: 15 | sidecar.istio.io/inject: "false" 16 | spec: 17 | serviceAccountName: {{ default (printf "%s-iter8-sa" .Release.Name) .Values.serviceAccountName }} 18 | containers: 19 | - name: iter8 20 | image: {{ .Values.iter8Image }} 21 | imagePullPolicy: Always 22 | command: 23 | - "/bin/sh" 24 | - "-c" 25 | - | 26 | iter8 k run --namespace {{ .Release.Namespace }} --test {{ .Release.Name }} -l {{ .Values.logLevel }} 27 | resources: 28 | {{ toYaml .Values.resources | indent 10 | trim }} 29 | securityContext: 30 | allowPrivilegeEscalation: false 31 | capabilities: 32 | drop: 33 | - ALL 34 | runAsNonRoot: true 35 | runAsUser: 1001040000 36 | env: 37 | - name: METRICS_SERVER_URL 38 | value: "{{ .Values.metricsServerURL }}" 39 | restartPolicy: Never 40 | backoffLimit: 0 41 | {{- end }} 42 | -------------------------------------------------------------------------------- /charts/iter8/templates/_k-role.tpl: -------------------------------------------------------------------------------- 1 | {{- define "k.role" -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: {{ .Release.Name }} 6 | annotations: 7 | iter8.tools/test: {{ .Release.Name }} 8 | rules: 9 | - apiGroups: [""] 10 | resourceNames: [{{ .Release.Name | quote }}] 11 | resources: ["secrets"] 12 | verbs: ["get", "update"] 13 | {{- if .Values.ready }} 14 | --- 15 | {{- $namespace := coalesce $.Values.ready.namespace $.Release.Namespace }} 16 | apiVersion: rbac.authorization.k8s.io/v1 17 | kind: Role 18 | metadata: 19 | name: {{ .Release.Name }}-ready 20 | {{- if $namespace }} 21 | namespace: {{ $namespace }} 22 | {{- end }} {{- /* if $namespace */}} 23 | annotations: 24 | iter8.tools/test: {{ .Release.Name }} 25 | rules: 26 | {{- $typesToCheck := omit .Values.ready "timeout" "namespace" }} 27 | {{- range $type, $name := $typesToCheck }} 28 | {{- $definition := get $.Values.resourceTypes $type }} 29 | {{- if not $definition }} 30 | {{- cat "no type definition for: " $type | fail }} 31 | {{- else }} 32 | - apiGroups: [ {{ get $definition "Group" | quote }} ] 33 | resourceNames: [ {{ $name | quote }} ] 34 | resources: [ {{ get $definition "Resource" | quote }} ] 35 | verbs: [ "get" ] 36 | {{- end }} {{- /* if not $definition */}} 37 | {{- end }} {{- /* range $type, $name */}} 38 | {{- end }} {{- /* {{- if .Values.ready */}} 39 | {{- end }} {{- /* {{- if .Values.ready */}} 40 | -------------------------------------------------------------------------------- /charts/iter8/templates/_k-rolebinding.tpl: -------------------------------------------------------------------------------- 1 | {{- define "k.rolebinding" -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: RoleBinding 4 | metadata: 5 | name: {{ .Release.Name }} 6 | annotations: 7 | iter8.tools/test: {{ .Release.Name }} 8 | subjects: 9 | - kind: ServiceAccount 10 | name: {{ .Release.Name }}-iter8-sa 11 | namespace: {{ .Release.Namespace }} 12 | roleRef: 13 | kind: Role 14 | name: {{ .Release.Name }} 15 | apiGroup: rbac.authorization.k8s.io 16 | {{- if .Values.ready }} 17 | --- 18 | {{- $namespace := coalesce .Values.ready.namespace .Release.Namespace }} 19 | {{- if $namespace }} 20 | apiVersion: rbac.authorization.k8s.io/v1 21 | kind: RoleBinding 22 | metadata: 23 | name: {{ .Release.Name }}-ready 24 | namespace: {{ $namespace }} 25 | annotations: 26 | iter8.tools/test: {{ .Release.Name }} 27 | subjects: 28 | - kind: ServiceAccount 29 | name: {{ .Release.Name }}-iter8-sa 30 | namespace: {{ .Release.Namespace }} 31 | roleRef: 32 | kind: Role 33 | name: {{ .Release.Name }}-ready 34 | apiGroup: rbac.authorization.k8s.io 35 | {{- end }} 36 | {{- end }} 37 | {{- end }} 38 | -------------------------------------------------------------------------------- /charts/iter8/templates/_k-secret.tpl: -------------------------------------------------------------------------------- 1 | {{- define "k.secret" -}} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: {{ .Release.Name }} 6 | annotations: 7 | iter8.tools/test: {{ .Release.Name }} 8 | stringData: 9 | experiment.yaml: | 10 | {{ include "experiment" . | indent 4 }} 11 | {{- end }} -------------------------------------------------------------------------------- /charts/iter8/templates/_k-serviceacccount.tpl: -------------------------------------------------------------------------------- 1 | {{- define "k.serviceaccount" -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ .Release.Name }}-iter8-sa 6 | {{- end }} 7 | -------------------------------------------------------------------------------- /charts/iter8/templates/_task-github.tpl: -------------------------------------------------------------------------------- 1 | {{- define "task.github" -}} 2 | {{- /* Validate values */ -}} 3 | {{- if not . }} 4 | {{- fail "github notify values object is nil" }} 5 | {{- end }} 6 | {{- if not .owner }} 7 | {{- fail "please set a value for the owner parameter" }} 8 | {{- end }} 9 | {{- if not .repo }} 10 | {{- fail "please set a value for the repo parameter" }} 11 | {{- end }} 12 | {{- if not .token }} 13 | {{- fail "please set a value for the token parameter" }} 14 | {{- end }} 15 | # task: send a GitHub notification 16 | - task: notify 17 | with: 18 | url: https://api.github.com/repos/{{ .owner }}/{{ .repo }}/dispatches 19 | method: POST 20 | headers: 21 | Authorization: token {{ .token }} 22 | Accept: application/vnd.github+json 23 | payloadTemplateURL: {{ default "https://raw.githubusercontent.com/iter8-tools/iter8/v0.16.5/templates/notify/_payload-github.tpl" .payloadTemplateURL }} 24 | softFailure: {{ default true .softFailure }} 25 | {{ end }} -------------------------------------------------------------------------------- /charts/iter8/templates/_task-http.tpl: -------------------------------------------------------------------------------- 1 | {{- define "task.http" -}} 2 | {{- /* Validate values */ -}} 3 | {{- if not . }} 4 | {{- fail "http values object is nil" }} 5 | {{- end }} 6 | {{/* url must be defined or a url must be defined for each endpoint */}} 7 | {{- if not .url }} 8 | {{- if .endpoints }} 9 | {{- range $endpointID, $endpoint := .endpoints }} 10 | {{- if not $endpoint.url }} 11 | {{- fail (print "endpoint \"" (print $endpointID "\" does not have a url parameter")) }} 12 | {{- end }} 13 | {{- end }} 14 | {{- else }} 15 | {{- fail "please set the url parameter or the endpoints parameter" }} 16 | {{- end }} 17 | {{- end }} 18 | {{- /**************************/ -}} 19 | {{- /* Perform the various setup steps before the main task */ -}} 20 | {{- $vals := mustDeepCopy . }} 21 | {{- if $vals.payloadURL }} 22 | # task: download payload from payload URL 23 | - run: | 24 | curl -o /tmp/payload.dat {{ $vals.payloadURL }} 25 | {{- $_ := set $vals "payloadFile" "/tmp/payload.dat" }} 26 | {{- end }} 27 | {{- /**************************/ -}} 28 | {{- /* Repeat above for each endpoint */ -}} 29 | {{- range $endpointID, $endpoint := $vals.endpoints }} 30 | {{- if $endpoint.payloadURL }} 31 | {{- $payloadFile := print "/tmp/" $endpointID "_payload.dat" }} 32 | # task: download payload from payload URL for endpoint 33 | - run: | 34 | curl -o {{ $payloadFile }} {{ $endpoint.payloadURL }} 35 | {{- $_ := set $endpoint "payloadFile" $payloadFile }} 36 | {{- end }} 37 | {{- end }} 38 | {{- /**************************/ -}} 39 | {{- /* Warmup task if requested */ -}} 40 | {{- if or .warmupNumRequests .warmupDuration }} 41 | {{- $warmupVals := mustDeepCopy $vals }} 42 | {{- if .warmupNumRequests }} 43 | {{- $_ := set $warmupVals "numRequests" .warmupNumRequests }} 44 | {{- else }} 45 | {{- $_ := set $warmupVals "duration" .warmupDuration}} 46 | {{- end }} 47 | {{- /* replace warmup options a boolean */ -}} 48 | {{- $_ := unset $warmupVals "warmupDuration" }} 49 | {{- $_ := unset $warmupVals "warmupNumRequests" }} 50 | {{- $_ := set $warmupVals "warmup" true }} 51 | # task: generate warmup HTTP requests 52 | # collect Iter8's built-in HTTP latency and error-related metrics 53 | - task: http 54 | with: 55 | {{ toYaml $warmupVals | indent 4 }} 56 | {{- end }} 57 | {{- /* warmup done */ -}} 58 | {{- /**************************/ -}} 59 | {{- /* Main task */ -}} 60 | {{- /* remove warmup options if present */ -}} 61 | {{- $_ := unset . "warmupDuration" }} 62 | {{- $_ := unset . "warmupNumRequests" }} 63 | # task: generate HTTP requests for app 64 | # collect Iter8's built-in HTTP latency and error-related metrics 65 | - task: http 66 | with: 67 | {{ toYaml $vals | indent 4 }} 68 | {{- end }} -------------------------------------------------------------------------------- /charts/iter8/templates/_task-ready.tpl: -------------------------------------------------------------------------------- 1 | {{- define "task.ready" }} 2 | {{- if .Values.ready }} 3 | {{- $typesToCheck := omit .Values.ready "timeout" "namespace" }} 4 | {{- range $type, $name := $typesToCheck }} 5 | {{- $definition := get $.Values.resourceTypes $type }} 6 | {{- if not $definition }} 7 | {{- cat "no type definition for: " $type | fail }} 8 | {{- else }} 9 | # task: test for existence and readiness of a resource 10 | - task: ready 11 | with: 12 | name: {{ $name | quote }} 13 | group: {{ get $definition "Group" | quote }} 14 | version: {{ get $definition "Version" | quote }} 15 | resource: {{ get $definition "Resource" | quote }} 16 | {{- if (hasKey $definition "conditions") }} 17 | conditions: 18 | {{ toYaml (get $definition "conditions") | indent 4 }} 19 | {{- end }} {{- /* if (hasKey $definition "conditions") */}} 20 | {{- $namespace := coalesce $.Values.ready.namespace $.Release.Namespace }} 21 | {{- if $namespace }} 22 | namespace: {{ $namespace }} 23 | {{- end }} {{- /* if $namespace */}} 24 | {{- if $.Values.ready.timeout }} 25 | timeout: {{ $.Values.ready.timeout }} 26 | {{- end }} {{- /* if $.Values.ready.timeout */}} 27 | {{- end }} {{- /* if not $definition */}} 28 | {{- end }} {{- /* range $type, $name */}} 29 | {{- end }} {{- /* {{- if .Values.ready */}} 30 | {{- end }} {{- /* define "task.ready" */}} 31 | -------------------------------------------------------------------------------- /charts/iter8/templates/_task-slack.tpl: -------------------------------------------------------------------------------- 1 | {{- define "task.slack" -}} 2 | {{- /* Validate values */ -}} 3 | {{- if not . }} 4 | {{- fail "slack notify values object is nil" }} 5 | {{- end }} 6 | {{- if not .url }} 7 | {{- fail "please set a value for the url parameter" }} 8 | {{- end }} 9 | # task: send a Slack notification 10 | - task: notify 11 | with: 12 | url: {{ .url }} 13 | method: POST 14 | payloadTemplateURL: {{ default "https://raw.githubusercontent.com/iter8-tools/iter8/v0.16.5/templates/notify/_payload-slack.tpl" .payloadTemplateURL }} 15 | softFailure: {{ default true .softFailure }} 16 | {{ end }} -------------------------------------------------------------------------------- /charts/iter8/templates/k8s.yaml: -------------------------------------------------------------------------------- 1 | {{ include "k.secret" . }} 2 | {{- if not .Values.serviceAccountName }} 3 | --- 4 | {{ include "k.role" . }} 5 | --- 6 | {{ include "k.serviceaccount" . }} 7 | --- 8 | {{ include "k.rolebinding" . }} 9 | {{- end}} 10 | --- 11 | {{ include "k.job" . }} 12 | -------------------------------------------------------------------------------- /charts/iter8/values.yaml: -------------------------------------------------------------------------------- 1 | ### iter8Image default iter8 image used for running Kubernetes experiments 2 | iter8Image: iter8/iter8:1.1 3 | 4 | ### majorMinor is the minor version of Iter8 5 | majorMinor: v1.1 6 | 7 | logLevel: info 8 | 9 | ### resources are the resource limits for the pods 10 | resources: 11 | requests: 12 | memory: "64Mi" 13 | cpu: "250m" 14 | limits: 15 | memory: "128Mi" 16 | cpu: "500m" 17 | 18 | ### metricsServerURL is the URL to the Metrics server 19 | metricsServerURL: http://iter8.default:8080 20 | 21 | ### list of resource types and conditions used to evalutate object readiness. 22 | resourceTypes: 23 | svc: 24 | Group: "" 25 | Version: v1 26 | Resource: services 27 | service: 28 | Group: "" 29 | Version: v1 30 | Resource: services 31 | cm: 32 | Group: "" 33 | Version: v1 34 | Resource: configmaps 35 | deploy: 36 | Group: apps 37 | Version: v1 38 | Resource: deployments 39 | conditions: 40 | - Available 41 | isvc: 42 | Group: serving.kserve.io 43 | Version: v1beta1 44 | Resource: inferenceservices 45 | conditions: 46 | - Ready 47 | vs: 48 | Group: networking.istio.io 49 | Version: v1beta1 50 | Resource: virtualservices -------------------------------------------------------------------------------- /charts/release/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | 25 | # generated files need to be ignored 26 | experiment.yaml -------------------------------------------------------------------------------- /charts/release/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: release 3 | version: 1.1.0 4 | description: Iter8 supported application release 5 | type: application 6 | keywords: 7 | - Iter8 8 | - traffic 9 | - blue-green 10 | - canary 11 | - mirroring 12 | home: https://iter8.tools 13 | sources: 14 | - https://github.com/iter8-tools/iter8 15 | maintainers: 16 | - name: Michael Kalantar 17 | email: kalantar@us.ibm.com 18 | - name: Iter8 19 | email: iter8-tools@gmail.com 20 | icon: https://github.com/iter8-tools/iter8/raw/master/mkdocs/docs/images/favicon.png 21 | appVersion: v1.1 22 | -------------------------------------------------------------------------------- /charts/release/templates/_configmap.weight-config.tpl: -------------------------------------------------------------------------------- 1 | {{- define "configmap.weight-config" }} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: {{ .VERSION_NAME }}-weight-config 6 | labels: 7 | iter8.tools/watch: "true" 8 | {{- if .weight }} 9 | annotations: 10 | iter8.tools/weight: "{{ .weight }}" 11 | {{- end }} {{- /* if .weight */}} 12 | {{- end }} {{- /* define "configmap.weight-config" */}} 13 | -------------------------------------------------------------------------------- /charts/release/templates/_deployment-gtw.blue-green.routemap.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment-gtw.blue-green.routemap" }} 2 | 3 | {{- $APP_NAME := (include "application.name" .) }} 4 | {{- $APP_NAMESPACE := (include "application.namespace" .) }} 5 | {{- $versions := include "normalize.versions.deployment" . | mustFromJson }} 6 | {{- $APP_PORT := pluck "port" (dict "port" 80) $.Values.application | first }} 7 | 8 | apiVersion: v1 9 | kind: ConfigMap 10 | {{- template "routemap.metadata" . }} 11 | data: 12 | strSpec: | 13 | versions: 14 | {{- range $i, $v := $versions }} 15 | - resources: 16 | - gvrShort: svc 17 | name: {{ template "svc.name" $v }} 18 | namespace: {{ template "svc.namespace" $v }} 19 | - gvrShort: deploy 20 | name: {{ template "deploy.name" $v }} 21 | namespace: {{ template "deploy.namespace" $v }} 22 | - gvrShort: cm 23 | name: {{ $v.VERSION_NAME }}-weight-config 24 | namespace: {{ $v.VERSION_NAMESPACE }} 25 | weight: {{ $v.weight }} 26 | {{- end }} {{- /* range $i, $v := $versions */}} 27 | routingTemplates: 28 | {{ .Values.application.strategy }}: 29 | gvrShort: httproute 30 | template: | 31 | apiVersion: gateway.networking.k8s.io/v1beta1 32 | kind: HTTPRoute 33 | metadata: 34 | name: {{ $APP_NAME }} 35 | namespace: {{ $APP_NAMESPACE }} 36 | spec: 37 | hostnames: 38 | - {{ $APP_NAME }} 39 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }} 40 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }}.svc.cluster.local 41 | parentRefs: 42 | - group: "" 43 | kind: Service 44 | name: {{ $APP_NAME }} 45 | port: {{ $APP_PORT }} 46 | {{- if .Values.gateway }} 47 | - name: {{ .Values.gateway }} 48 | {{- end }} 49 | rules: 50 | - backendRefs: 51 | {{- range $i, $v := $versions }} 52 | - group: "" 53 | kind: Service 54 | name: {{ template "svc.name" $v }} 55 | port: {{ $v.port }} 56 | {{- if gt (len $versions) 1 }} 57 | {{ `{{- if gt (index .Weights 1) 0 }}` }} 58 | weight: {{ `{{ index .Weights ` }}{{ print $i }}{{ ` }}` }} 59 | {{ `{{- end }}` }} 60 | {{- end }} 61 | filters: 62 | - type: ResponseHeaderModifier 63 | responseHeaderModifier: 64 | add: 65 | - name: app-version 66 | value: {{ template "svc.name" $v }} 67 | {{- end }} {{- /* range $i, $v := $versions */}} 68 | {{- end }} {{- /* define "env.deployment-gtw.blue-green.routemap" */}} 69 | -------------------------------------------------------------------------------- /charts/release/templates/_deployment-gtw.blue-green.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment-gtw.blue-green" }} 2 | 3 | {{- /* prepare versions for simpler processing */}} 4 | {{- $versions := include "normalize.versions.deployment" . | mustFromJson }} 5 | 6 | {{- /* weight-config ConfigMaps */}} 7 | {{- range $i, $v := $versions }} 8 | {{ include "configmap.weight-config" $v }} 9 | --- 10 | {{- end }} {{- /* range $i, $v := $versions */}} 11 | 12 | {{- /* routemap */}} 13 | {{ include "env.deployment-gtw.blue-green.routemap" . }} 14 | 15 | {{- end }} {{- /* define "env.deployment-gtw.blue-green" */}} -------------------------------------------------------------------------------- /charts/release/templates/_deployment-gtw.canary.routemap.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment-gtw.canary.routemap" }} 2 | 3 | {{- $APP_NAME := (include "application.name" .) }} 4 | {{- $APP_NAMESPACE := (include "application.namespace" .) }} 5 | {{- $versions := include "normalize.versions.deployment" . | mustFromJson }} 6 | {{- $APP_PORT := pluck "port" (dict "port" 80) $.Values.application | first }} 7 | 8 | apiVersion: v1 9 | kind: ConfigMap 10 | {{- template "routemap.metadata" . }} 11 | data: 12 | strSpec: | 13 | versions: 14 | {{- range $i, $v := $versions }} 15 | - resources: 16 | - gvrShort: svc 17 | name: {{ template "svc.name" $v }} 18 | namespace: {{ template "svc.namespace" $v }} 19 | - gvrShort: deploy 20 | name: {{ template "deploy.name" $v }} 21 | namespace: {{ template "deploy.namespace" $v }} 22 | {{- end }} {{- /* range $i, $v := $versions */}} 23 | routingTemplates: 24 | {{ .Values.application.strategy }}: 25 | gvrShort: httproute 26 | template: | 27 | apiVersion: gateway.networking.k8s.io/v1beta1 28 | kind: HTTPRoute 29 | metadata: 30 | name: {{ $APP_NAME }} 31 | namespace: {{ $APP_NAMESPACE }} 32 | spec: 33 | hostnames: 34 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }} 35 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }}.svc 36 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }}.svc.cluster.local 37 | parentRefs: 38 | - group: "" 39 | kind: Service 40 | name: {{ $APP_NAME }} 41 | port: {{ $APP_PORT }} 42 | {{- if .Values.gateway }} 43 | - name: {{ .Values.gateway }} 44 | {{- end }} 45 | rules: 46 | # non-primary versions 47 | {{- range $i, $v := (rest $versions) }} 48 | - matches: 49 | {{- toYaml $v.matches | nindent 14 }} 50 | backendRefs: 51 | - group: "" 52 | kind: Service 53 | name: {{ template "svc.name" $v }} 54 | port: {{ $v.port }} 55 | filters: 56 | - type: ResponseHeaderModifier 57 | responseHeaderModifier: 58 | add: 59 | - name: app-version 60 | value: {{ template "svc.name" $v }} 61 | {{- end }} {{- /* range $i, $v := (rest $versions) */}} 62 | # primary version (default) 63 | {{- $v := (index $versions 0) }} 64 | - backendRefs: 65 | - group: "" 66 | kind: Service 67 | name: {{ template "svc.name" $v }} 68 | port: {{ $v.port }} 69 | filters: 70 | - type: ResponseHeaderModifier 71 | responseHeaderModifier: 72 | add: 73 | - name: app-version 74 | value: {{ template "svc.name" $v }} 75 | {{- end }} {{- /* define "env.deployment-gtw.canary.routemap" */}} 76 | -------------------------------------------------------------------------------- /charts/release/templates/_deployment-gtw.canary.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment-gtw.canary" }} 2 | 3 | {{- /* routemap */}} 4 | {{ include "env.deployment-gtw.canary.routemap" . }} 5 | 6 | {{- end }} {{- /* define "env.deployment-gtw.canary" */}} -------------------------------------------------------------------------------- /charts/release/templates/_deployment-gtw.none.routemap.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment-gtw.none.routemap" }} 2 | 3 | {{- $versions := include "normalize.versions.deployment" . | mustFromJson }} 4 | 5 | apiVersion: v1 6 | kind: ConfigMap 7 | {{- template "routemap.metadata" . }} 8 | data: 9 | strSpec: | 10 | versions: 11 | {{- range $i, $v := $versions }} 12 | - resources: 13 | - gvrShort: svc 14 | name: {{ template "svc.name" $v }} 15 | namespace: {{ template "svc.namespace" $v }} 16 | - gvrShort: deploy 17 | name: {{ template "deploy.name" $v }} 18 | namespace: {{ template "deploy.namespace" $v }} 19 | {{- end }} 20 | 21 | {{- end }} {{- /* define "env.deployment-gtw.none.routemap" */}} 22 | -------------------------------------------------------------------------------- /charts/release/templates/_deployment-gtw.none.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment-gtw.none" }} 2 | 3 | {{- /* routemap */}} 4 | {{ include "env.deployment-gtw.none.routemap" . }} 5 | 6 | {{- end }} {{- /* define "env.deployment-gtw.none" */}} -------------------------------------------------------------------------------- /charts/release/templates/_deployment-gtw.service.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment-gtw.service" }} 2 | 3 | {{- $APP_NAME := (include "application.name" .) }} 4 | {{- $APP_NAMESPACE := (include "application.namespace" .) }} 5 | 6 | apiVersion: v1 7 | kind: Service 8 | metadata: 9 | name: {{ $APP_NAME }} 10 | namespace: {{ $APP_NAMESPACE }} 11 | spec: 12 | selector: 13 | app: {{ $APP_NAME }} 14 | ports: 15 | - port: 80 16 | {{- end }} {{- /* define "env.deployment-gtw.service" */}} -------------------------------------------------------------------------------- /charts/release/templates/_deployment-gtw.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment-gtw" }} 2 | 3 | {{- /* Prepare versions for simpler processing */}} 4 | {{- $versions := include "normalize.versions.deployment" . | mustFromJson }} 5 | 6 | {{- range $i, $v := $versions }} 7 | {{- /* Deployment */}} 8 | {{ include "env.deployment.version.deployment" $v }} 9 | --- 10 | {{- /* Service */}} 11 | {{ include "env.deployment.version.service" $v }} 12 | --- 13 | {{- end }} {{- /* range $i, $v := $versions */}} 14 | 15 | {{- /* Service */}} 16 | {{ include "env.deployment-gtw.service" . }} 17 | --- 18 | 19 | {{- /* routemap (and other strategy specific objects) */}} 20 | {{- if not .Values.application.strategy }} 21 | {{ include "env.deployment-gtw.none" . }} 22 | {{- else if eq "none" .Values.application.strategy }} 23 | {{ include "env.deployment-gtw.none" . }} 24 | {{- else if eq "blue-green" .Values.application.strategy }} 25 | {{ include "env.deployment-gtw.blue-green" . }} 26 | {{- else if eq "canary" .Values.application.strategy }} 27 | {{ include "env.deployment-gtw.canary" . }} 28 | {{- end }} {{- /* if eq ... .Values.application.strategy */}} 29 | 30 | {{- end }} {{- /* define "env.deployment-gtw" */}} -------------------------------------------------------------------------------- /charts/release/templates/_deployment-istio.blue-green.routemap.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment-istio.blue-green.routemap" }} 2 | 3 | {{- $APP_NAME := (include "application.name" .) }} 4 | {{- $APP_NAMESPACE := (include "application.namespace" .) }} 5 | {{- $versions := include "normalize.versions.deployment" . | mustFromJson }} 6 | 7 | apiVersion: v1 8 | kind: ConfigMap 9 | {{- template "routemap.metadata" . }} 10 | data: 11 | strSpec: | 12 | versions: 13 | {{- range $i, $v := $versions }} 14 | - resources: 15 | - gvrShort: svc 16 | name: {{ template "svc.name" $v }} 17 | namespace: {{ template "svc.namespace" $v }} 18 | - gvrShort: deploy 19 | name: {{ template "deploy.name" $v }} 20 | namespace: {{ template "deploy.namespace" $v }} 21 | - gvrShort: cm 22 | name: {{ $v.VERSION_NAME }}-weight-config 23 | namespace: {{ $v.VERSION_NAMESPACE }} 24 | weight: {{ $v.weight }} 25 | {{- end }} {{- /* range $i, $v := $versions */}} 26 | routingTemplates: 27 | {{ .Values.application.strategy }}: 28 | gvrShort: vs 29 | template: | 30 | apiVersion: networking.istio.io/v1beta1 31 | kind: VirtualService 32 | metadata: 33 | name: {{ $APP_NAME }} 34 | namespace: {{ $APP_NAMESPACE }} 35 | spec: 36 | gateways: 37 | {{- if .Values.gateway }} 38 | - {{ .Values.gateway }} 39 | {{- end }} 40 | - mesh 41 | hosts: 42 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }} 43 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }}.svc 44 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }}.svc.cluster.local 45 | http: 46 | - name: {{ $APP_NAME }} 47 | route: 48 | # primary version 49 | {{- $v := (index $versions 0) }} 50 | - destination: 51 | host: {{ template "svc.name" $v }}.{{ $APP_NAMESPACE }}.svc.cluster.local 52 | port: 53 | number: {{ $v.port }} 54 | {{- if gt (len $versions) 1 }} 55 | {{ `{{- if gt (index .Weights 1) 0 }}` }} 56 | weight: {{ `{{ index .Weights 0 }}` }} 57 | {{ `{{- end }}` }} 58 | {{- end }} 59 | headers: 60 | response: 61 | add: 62 | app-version: {{ template "svc.name" $v }} 63 | # other versions 64 | {{- range $i, $v := (rest $versions) }} 65 | {{ `{{- if gt (index .Weights ` }}{{ print (add1 $i) }}{{ `) 0 }}` }} 66 | - destination: 67 | host: {{ template "svc.name" $v }}.{{ $APP_NAMESPACE }}.svc.cluster.local 68 | port: 69 | number: {{ $v.port }} 70 | weight: {{ `{{ index .Weights ` }}{{ print (add1 $i) }}{{ ` }}` }} 71 | headers: 72 | response: 73 | add: 74 | app-version: {{ template "svc.name" $v }} 75 | {{ `{{- end }}` }} 76 | {{- end }} 77 | {{- end }} {{- /* define "env.deployment-istio.blue-green.routemap" */}} 78 | -------------------------------------------------------------------------------- /charts/release/templates/_deployment-istio.blue-green.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment-istio.blue-green" }} 2 | 3 | {{- /* prepare versions for simpler processing */}} 4 | {{- $versions := include "normalize.versions.deployment" . | mustFromJson }} 5 | 6 | {{- /* weight-config ConfigMaps */}} 7 | {{- range $i, $v := $versions }} 8 | {{ include "configmap.weight-config" $v }} 9 | --- 10 | {{- end }} {{- /* range $i, $v := $versions */}} 11 | 12 | {{- /* routemap */}} 13 | {{ include "env.deployment-istio.blue-green.routemap" . }} 14 | 15 | {{- end }} {{- /* define "env.deployment-istio.blue-green" */}} -------------------------------------------------------------------------------- /charts/release/templates/_deployment-istio.canary.routemap.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment-istio.canary.routemap" }} 2 | 3 | {{- $APP_NAME := (include "application.name" .) }} 4 | {{- $APP_NAMESPACE := (include "application.namespace" .) }} 5 | {{- $versions := include "normalize.versions.deployment" . | mustFromJson }} 6 | 7 | apiVersion: v1 8 | kind: ConfigMap 9 | {{- template "routemap.metadata" . }} 10 | data: 11 | strSpec: | 12 | versions: 13 | {{- range $i, $v := $versions }} 14 | - resources: 15 | - gvrShort: svc 16 | name: {{ template "svc.name" $v }} 17 | namespace: {{ template "svc.namespace" $v }} 18 | - gvrShort: deploy 19 | name: {{ template "deploy.name" $v }} 20 | namespace: {{ template "deploy.namespace" $v }} 21 | {{- end }} 22 | routingTemplates: 23 | {{ .Values.application.strategy }}: 24 | gvrShort: vs 25 | template: | 26 | apiVersion: networking.istio.io/v1beta1 27 | kind: VirtualService 28 | metadata: 29 | name: {{ $APP_NAME }} 30 | namespace: {{ $APP_NAMESPACE }} 31 | spec: 32 | gateways: 33 | {{- if .Values.gateway }} 34 | - {{ .Values.gateway }} 35 | {{- end }} 36 | - mesh 37 | hosts: 38 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }} 39 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }}.svc 40 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }}.svc.cluster.local 41 | http: 42 | # non-primary versions 43 | {{- range $i, $v := (rest $versions) }} 44 | {{- /* continue only if candidate is ready (weight > 0) */}} 45 | {{ `{{- if gt (index .Weights ` }}{{ print (add1 $i) }}{{ `) 0 }}` }} 46 | - name: {{ template "svc.name" $v }} 47 | match: 48 | {{- /* A match may have several ORed clauses */}} 49 | {{- range $j, $m := $v.match }} 50 | {{- /* include any other header requirements */}} 51 | {{- if (hasKey $m "headers") }} 52 | - headers: 53 | {{ toYaml (pick $m "headers").headers | indent 18 }} 54 | {{- end }} {{- /* if (hasKey $m "headers") */}} 55 | {{- /* include any other (non-header) requirements */}} 56 | {{- if gt (omit $m "headers" | keys | len) 0 }} 57 | {{ toYaml (omit $m "headers") | indent 16 }} 58 | {{- end }} {{- /* if gt (omit $m "headers" | keys | len) 0 */}} 59 | {{- end }} {{- /* range $j, $m := $v.match */}} 60 | route: 61 | - destination: 62 | host: {{ template "svc.name" $v }}.{{ $APP_NAMESPACE }}.svc.cluster.local 63 | port: 64 | number: {{ $v.port }} 65 | headers: 66 | response: 67 | add: 68 | app-version: {{ template "svc.name" $v }} 69 | {{ `{{- end }}` }} 70 | {{- end }} {{- /* range $i, $v := (rest $versions) */}} 71 | # primary version (default) 72 | {{- $v := (index $versions 0) }} 73 | - name: {{ template "svc.name" $v }} 74 | route: 75 | - destination: 76 | host: {{ template "svc.name" $v }}.{{ $APP_NAMESPACE }}.svc.cluster.local 77 | port: 78 | number: {{ $v.port }} 79 | headers: 80 | response: 81 | add: 82 | app-version: {{ template "svc.name" $v }} 83 | 84 | {{- end }} {{- /* define "env.deployment-istio.canary.routemap" */}} 85 | -------------------------------------------------------------------------------- /charts/release/templates/_deployment-istio.canary.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment-istio.canary" }} 2 | 3 | {{- /* routemap */}} 4 | {{ include "env.deployment-istio.canary.routemap" . }} 5 | 6 | {{- end }} {{- /* define "env.deployment-istio.canary" */}} -------------------------------------------------------------------------------- /charts/release/templates/_deployment-istio.mirror.routemap.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment-istio.mirror.routemap" }} 2 | 3 | {{- $APP_NAME := (include "application.name" .) }} 4 | {{- $APP_NAMESPACE := (include "application.namespace" .) }} 5 | {{- $versions := include "normalize.versions.deployment" . | mustFromJson }} 6 | 7 | apiVersion: v1 8 | kind: ConfigMap 9 | {{- template "routemap.metadata" . }} 10 | data: 11 | strSpec: | 12 | versions: 13 | {{- range $i, $v := $versions }} 14 | - resources: 15 | - gvrShort: svc 16 | name: {{ template "svc.name" $v }} 17 | namespace: {{ template "svc.namespace" $v }} 18 | - gvrShort: deploy 19 | name: {{ template "deploy.name" $v }} 20 | namespace: {{ template "deploy.namespace" $v }} 21 | - gvrShort: cm 22 | name: {{ $v.VERSION_NAME }}-weight-config 23 | namespace: {{ $v.VERSION_NAMESPACE }} 24 | weight: {{ $v.weight }} 25 | {{- end }} {{- /* range $i, $v := $versions */}} 26 | routingTemplates: 27 | {{ .Values.application.strategy }}: 28 | gvrShort: vs 29 | template: | 30 | apiVersion: networking.istio.io/v1beta1 31 | kind: VirtualService 32 | metadata: 33 | name: {{ $APP_NAME }} 34 | namespace: {{ $APP_NAMESPACE }} 35 | spec: 36 | gateways: 37 | {{- if .Values.gateway }} 38 | - {{ .Values.gateway }} 39 | {{- end }} 40 | - mesh 41 | hosts: 42 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }} 43 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }}.svc 44 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }}.svc.cluster.local 45 | http: 46 | - name: {{ $APP_NAME }} 47 | route: 48 | # primary version 49 | {{- $v := (index $versions 0) }} 50 | - destination: 51 | host: {{ template "svc.name" $v }}.{{ $APP_NAMESPACE }}.svc.cluster.local 52 | port: 53 | number: {{ $v.port }} 54 | {{- if gt (len $versions) 1 }} 55 | {{ `{{- if gt (index .Weights 1) 0 }}` }} 56 | weight: {{ `{{ index .Weights 0 }}` }} 57 | {{ `{{- end }}` }} 58 | {{- end }} 59 | headers: 60 | response: 61 | add: 62 | app-version: {{ template "svc.name" $v }} 63 | # other versions 64 | {{- range $i, $v := (rest $versions) }} 65 | {{ `{{- if gt (index .Weights ` }}{{ print (add1 $i) }}{{ `) 0 }}` }} 66 | mirror: 67 | host: {{ template "svc.name" $v }}.{{ $APP_NAMESPACE }}.svc.cluster.local 68 | port: 69 | number: {{ $v.port }} 70 | mirrorPercentage: 71 | value: {{ `{{ index .Weights ` }}{{ print (add1 $i) }}{{ ` }}` }} 72 | {{ `{{- end }}` }} 73 | {{- end }} 74 | {{- end }} {{- /* define "env.deployment-istio.mirror.routemap" */}} 75 | -------------------------------------------------------------------------------- /charts/release/templates/_deployment-istio.mirror.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment-istio.mirror" }} 2 | 3 | {{- /* prepare versions for simpler processing */}} 4 | {{- $versions := include "normalize.versions.deployment" . | mustFromJson }} 5 | 6 | {{- /* weight-config ConfigMaps except for primary */}} 7 | {{- range $i, $v := (rest $versions) }} 8 | {{ include "configmap.weight-config" $v }} 9 | --- 10 | {{- end }} {{- /* range $i, $v := $versions */}} 11 | 12 | {{- /* routemap */}} 13 | {{ include "env.deployment-istio.mirror.routemap" . }} 14 | 15 | {{- end }} {{- /* define "env.deployment-istio.mirror" */}} -------------------------------------------------------------------------------- /charts/release/templates/_deployment-istio.none.routemap.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment-istio.none.routemap" }} 2 | 3 | {{- $versions := include "normalize.versions.deployment" . | mustFromJson }} 4 | 5 | apiVersion: v1 6 | kind: ConfigMap 7 | {{- template "routemap.metadata" . }} 8 | data: 9 | strSpec: | 10 | versions: 11 | {{- range $i, $v := $versions }} 12 | - resources: 13 | - gvrShort: svc 14 | name: {{ template "svc.name" $v }} 15 | namespace: {{ template "svc.namespace" $v }} 16 | - gvrShort: deploy 17 | name: {{ template "deploy.name" $v }} 18 | namespace: {{ template "deploy.namespace" $v }} 19 | {{- end }} 20 | 21 | {{- end }} {{- /* define "env.deployment-istio.none.routemap" */}} 22 | -------------------------------------------------------------------------------- /charts/release/templates/_deployment-istio.none.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment-istio.none" }} 2 | 3 | {{- /* routemap */}} 4 | {{ include "env.deployment-istio.none.routemap" . }} 5 | 6 | {{- end }} {{- /* define "env.deployment-istio.none" */}} -------------------------------------------------------------------------------- /charts/release/templates/_deployment-istio.service.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment-istio.service" }} 2 | 3 | {{- $APP_NAME := (include "application.name" .) }} 4 | {{- $APP_NAMESPACE := (include "application.namespace" .) }} 5 | 6 | apiVersion: v1 7 | kind: Service 8 | metadata: 9 | name: {{ $APP_NAME }} 10 | namespace: {{ $APP_NAMESPACE }} 11 | spec: 12 | externalName: istio-ingressgateway.istio-system.svc.cluster.local 13 | sessionAffinity: None 14 | type: ExternalName 15 | {{- end }} {{- /* define "env.deployment-istio.service" */}} -------------------------------------------------------------------------------- /charts/release/templates/_deployment-istio.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment-istio" }} 2 | 3 | {{- /* Prepare versions for simpler processing */}} 4 | {{- $versions := include "normalize.versions.deployment" . | mustFromJson }} 5 | 6 | {{- range $i, $v := $versions }} 7 | {{- /* Deployment */}} 8 | {{ include "env.deployment.version.deployment" $v }} 9 | --- 10 | {{- /* Service */}} 11 | {{ include "env.deployment.version.service" $v }} 12 | --- 13 | {{- end }} {{- /* range $i, $v := $versions */}} 14 | 15 | {{- /* Service */}} 16 | {{ include "env.deployment-istio.service" . }} 17 | --- 18 | 19 | {{- /* routemap (and other strategy specific objects) */}} 20 | {{- if not .Values.application.strategy }} 21 | {{ include "env.deployment-istio.none" . }} 22 | {{- else if eq "none" .Values.application.strategy }} 23 | {{ include "env.deployment-istio.none" . }} 24 | {{- else if eq "blue-green" .Values.application.strategy }} 25 | {{ include "env.deployment-istio.blue-green" . }} 26 | {{- else if eq "canary" .Values.application.strategy }} 27 | {{ include "env.deployment-istio.canary" . }} 28 | {{- else if eq "mirror" .Values.application.strategy }} 29 | {{ include "env.deployment-istio.mirror" . }} 30 | {{- end }} {{- /* if eq ... .Values.application.strategy */}} 31 | 32 | {{- end }} {{- /* define "env.deployment-istio" */}} -------------------------------------------------------------------------------- /charts/release/templates/_deployment.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment" }} 2 | 3 | {{- /* Prepare versions for simpler processing */}} 4 | {{- $versions := include "normalize.versions.deployment" . | mustFromJson }} 5 | 6 | {{- range $i, $v := $versions }} 7 | 8 | {{- /* Deployment */}} 9 | {{ include "env.deployment.version.deployment" $v }} 10 | --- 11 | {{- /* Service */}} 12 | {{ include "env.deployment.version.service" $v }} 13 | --- 14 | {{- end }} {{- /* range $i, $v := $versions */}} 15 | 16 | {{- /* routemap (and other strategy specific objects) */}} 17 | {{- if not .Values.application.strategy }} 18 | {{ include "env.deployment-istio.none" . }} 19 | {{- else if eq "none" .Values.application.strategy }} 20 | {{ include "env.deployment-istio.none" . }} 21 | {{- else }} 22 | {{- printf "unknown or invalid application strategy (%s) for environment (%s)" .Values.application.strategy .Values.environment | fail }} 23 | {{- end }} {{- /* if eq ... .Values.application.strategy */}} 24 | 25 | {{- end }} {{- /* define "env.deployment" */}} -------------------------------------------------------------------------------- /charts/release/templates/_deployment.version.deployment.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment.version.deployment" }} 2 | 3 | {{- /* compute basic metadata */}} 4 | {{- $metadata := include "application.version.metadata" . | mustFromJson }} 5 | 6 | apiVersion: apps/v1 7 | kind: Deployment 8 | {{- if .deploymentSpecification }} 9 | metadata: 10 | {{- if .deploymentSpecification.metadata }} 11 | {{ toYaml (merge .deploymentSpecification.metadata $metadata) | nindent 2 | trim }} 12 | {{- else }} 13 | {{ toYaml $metadata | nindent 2 | trim }} 14 | {{- end }} {{- /* if .deploymentSpecification.metadata */}} 15 | spec: 16 | {{ toYaml .deploymentSpecification.spec | nindent 2 | trim }} 17 | {{- else }} 18 | {{- if not .image }} {{- /* require .image */}} 19 | {{- print "missing field: image required when deploymentSpecification absent" | fail }} 20 | {{- end }} {{- /* if not .image */}} 21 | {{- if not .port }} {{- /* require .port */}} 22 | {{- print "missing field: port required when deploymentSpecification absent" | fail }} 23 | {{- end }} {{- /* if not .port */}} 24 | metadata: 25 | {{ toYaml $metadata | nindent 2 | trim }} 26 | spec: 27 | selector: 28 | matchLabels: 29 | app: {{ .VERSION_NAME }} 30 | template: 31 | metadata: 32 | labels: 33 | app: {{ .VERSION_NAME }} 34 | spec: 35 | containers: 36 | - name: {{ .VERSION_NAME }} 37 | image: {{ .image }} 38 | ports: 39 | - containerPort: {{ .port }} 40 | {{- end }} {{- /* if .deploymentSpecification */}} 41 | 42 | {{- end }} {{- /* define "env.deployment.version.deployment" */}} 43 | -------------------------------------------------------------------------------- /charts/release/templates/_deployment.version.service.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.deployment.version.service" }} 2 | 3 | {{- /* compute basic metadata */}} 4 | {{- $metadata := include "application.version.metadata" . | mustFromJson }} 5 | 6 | apiVersion: v1 7 | kind: Service 8 | {{- if .serviceSpecification }} 9 | metadata: 10 | {{- if .serviceSpecification.metadata }} 11 | {{ toYaml (merge .serviceSpecification.metadata $metadata) | nindent 2 | trim }} 12 | {{- else }} 13 | {{ toYaml $metadata | nindent 2 | trim }} 14 | {{- end }} {{- /* if .serviceSpecification.metadata */}} 15 | spec: 16 | {{ toYaml .serviceSpecification.spec | nindent 2 | trim }} 17 | {{- else }} 18 | {{- if not .port }} {{- /* require .port */}} 19 | {{- print "missing field: port required when serviceSpecification absent" | fail }} 20 | {{- end }} {{- /* if not .port */}} 21 | metadata: 22 | {{ toYaml $metadata | nindent 2 | trim }} 23 | spec: 24 | selector: 25 | app: {{ .VERSION_NAME }} 26 | ports: 27 | - port: {{ .port }} 28 | {{- end }} {{- /* if .serviceSpecification */}} 29 | {{- end }} {{- /* define "env.deployment.version.service" */}} 30 | -------------------------------------------------------------------------------- /charts/release/templates/_kserve.blue-green.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.kserve.blue-green" }} 2 | 3 | {{- /* prepare versions for simpler processing */}} 4 | {{- $versions := include "normalize.versions.kserve" . | mustFromJson }} 5 | 6 | {{- /* weight-config ConfigMaps */}} 7 | {{- range $i, $v := $versions }} 8 | {{ include "configmap.weight-config" $v }} 9 | --- 10 | {{- end }} {{- /* range $i, $v := $versions */}} 11 | 12 | {{- /* routemap */}} 13 | {{ include "env.kserve.blue-green.routemap" . }} 14 | 15 | {{- end }} {{- /* define "env.kserve.blue-green" */}} -------------------------------------------------------------------------------- /charts/release/templates/_kserve.canary.routemap.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.kserve.canary.routemap" }} 2 | 3 | {{- $APP_NAME := (include "application.name" .) }} 4 | {{- $APP_NAMESPACE := (include "application.namespace" .) }} 5 | {{- $versions := include "normalize.versions.kserve" . | mustFromJson }} 6 | 7 | apiVersion: v1 8 | kind: ConfigMap 9 | {{- template "routemap.metadata" . }} 10 | data: 11 | strSpec: | 12 | versions: 13 | {{- range $i, $v := $versions }} 14 | - resources: 15 | - gvrShort: isvc 16 | name: {{ template "isvc.name" $v }} 17 | namespace: {{ template "isvc.namespace" $v }} 18 | weight: {{ $v.weight }} 19 | {{- end }} {{- /* range $i, $v := $versions */}} 20 | routingTemplates: 21 | {{ .Values.application.strategy }}: 22 | gvrShort: vs 23 | template: | 24 | apiVersion: networking.istio.io/v1beta1 25 | kind: VirtualService 26 | metadata: 27 | name: {{ $APP_NAME }} 28 | namespace: {{ $APP_NAMESPACE }} 29 | spec: 30 | gateways: 31 | - knative-serving/knative-ingress-gateway 32 | - knative-serving/knative-local-gateway 33 | - mesh 34 | hosts: 35 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }} 36 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }}.svc 37 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }}.svc.cluster.local 38 | http: 39 | # non-primary versions 40 | {{- /* For candidate versions, ensure mm-model header is required in all matches */}} 41 | {{- range $i, $v := (rest $versions) }} 42 | {{- /* continue only if candidate is ready (weight > 0) */}} 43 | {{ `{{- if gt (index .Weights ` }}{{ print (add1 $i) }}{{ `) 0 }}` }} 44 | - name: {{ template "isvc.name" $v }} 45 | match: 46 | {{- /* A match may have several ORd clauses */}} 47 | {{- range $j, $m := $v.match }} 48 | {{- /* include any other header requirements */}} 49 | {{- if (hasKey $m "headers") }} 50 | - headers: 51 | {{ toYaml (pick $m "headers").headers | indent 18 }} 52 | {{- end }} 53 | {{- /* include any other (non-header) requirements */}} 54 | {{- if gt (omit $m "headers" | keys | len) 0 }} 55 | {{ toYaml (omit $m "headers") | indent 16 }} 56 | {{- end }} 57 | {{- end }} 58 | rewrite: 59 | uri: /v2/models/{{ template "isvc.name" $v }}/infer 60 | route: 61 | - destination: 62 | host: knative-local-gateway.istio-system.svc.cluster.local 63 | headers: 64 | request: 65 | set: 66 | Host: {{ template "isvc.name" $v }}-{{ template "kserve.host" $ }} 67 | response: 68 | add: 69 | app-version: {{ template "isvc.name" $v }} 70 | {{ `{{- end }}` }} 71 | {{- end }} 72 | # primary version (default) 73 | {{- $v := (index $versions 0) }} 74 | - name: {{ template "isvc.name" $v }} 75 | rewrite: 76 | uri: /v2/models/{{ template "isvc.name" $v }}/infer 77 | route: 78 | - destination: 79 | host: knative-local-gateway.istio-system.svc.cluster.local 80 | headers: 81 | request: 82 | set: 83 | Host: {{ template "isvc.name" $v }}-{{ template "kserve.host" $ }} 84 | response: 85 | add: 86 | app-version: {{ template "isvc.name" $v }} 87 | 88 | {{- end }} {{- /* define "env.kserve.canary.routemap" */}} 89 | -------------------------------------------------------------------------------- /charts/release/templates/_kserve.canary.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.kserve.canary" }} 2 | 3 | {{- /* routemap */}} 4 | {{ include "env.kserve.canary.routemap" . }} 5 | 6 | {{- end }} {{- /* define "env.kserve.canary" */}} -------------------------------------------------------------------------------- /charts/release/templates/_kserve.none.routemap.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.kserve.none.routemap" }} 2 | 3 | {{- $versions := include "normalize.versions.kserve" . | mustFromJson }} 4 | 5 | apiVersion: v1 6 | kind: ConfigMap 7 | {{- template "routemap.metadata" . }} 8 | data: 9 | strSpec: | 10 | versions: 11 | {{- range $i, $v := $versions }} 12 | - resources: 13 | - gvrShort: isvc 14 | name: {{ template "isvc.name" $v }} 15 | namespace: {{ template "isvc.namespace" $v }} 16 | {{- end }} {{- /* range $i, $v := $versions */}} 17 | 18 | {{- end }} {{- /* define "env.kserve.none.routemap" */}} 19 | -------------------------------------------------------------------------------- /charts/release/templates/_kserve.none.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.kserve.none" }} 2 | 3 | {{- /* routemap */}} 4 | {{ include "env.kserve.none.routemap" . }} 5 | 6 | {{- end }} {{- /* define "env.kserve.none" */}} -------------------------------------------------------------------------------- /charts/release/templates/_kserve.service.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.kserve.service" }} 2 | 3 | {{- $APP_NAME := (include "application.name" .) }} 4 | {{- $APP_NAMESPACE := (include "application.namespace" .) }} 5 | 6 | apiVersion: v1 7 | kind: Service 8 | metadata: 9 | name: {{ $APP_NAME }} 10 | namespace: {{ $APP_NAMESPACE }} 11 | spec: 12 | externalName: knative-local-gateway.istio-system.svc.cluster.local 13 | sessionAffinity: None 14 | type: ExternalName 15 | {{- end }} {{- /* define "env.kserve.service" */}} -------------------------------------------------------------------------------- /charts/release/templates/_kserve.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.kserve" }} 2 | 3 | {{- /* Prepare versions for simpler processing */}} 4 | {{- $versions := include "normalize.versions.kserve" . | mustFromJson }} 5 | 6 | {{- range $i, $v := $versions }} 7 | {{- /* InferenceService */}} 8 | {{ include "env.kserve.version.isvc" $v }} 9 | --- 10 | {{- end }} {{- /* range $i, $v := $versions */}} 11 | 12 | {{- /* Service */}} 13 | {{ include "env.kserve.service" . }} 14 | --- 15 | 16 | {{- /* routemap (and other strategy specific objects) */}} 17 | {{- if not .Values.application.strategy }} 18 | {{ include "env.kserve.none" . }} 19 | {{- else if eq "none" .Values.application.strategy }} 20 | {{ include "env.kserve.none" . }} 21 | {{- else if eq "blue-green" .Values.application.strategy }} 22 | {{ include "env.kserve.blue-green" . }} 23 | {{- else if eq "canary" .Values.application.strategy }} 24 | {{ include "env.kserve.canary" . }} 25 | {{- end }} {{- /* if eq ... .Values.application.strategy */}} 26 | 27 | {{- end }} {{- /* define "env.kserve" */}} -------------------------------------------------------------------------------- /charts/release/templates/_kserve.version.isvc.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.kserve.version.isvc" }} 2 | 3 | {{- /* compute basic metadata */}} 4 | {{- $metadata := include "application.version.metadata" . | mustFromJson }} 5 | {{- /* add annotation serving.kserve.io/deploymentMode */}} 6 | 7 | {{- /* define InferenceServcie */}} 8 | apiVersion: serving.kserve.io/v1beta1 9 | kind: InferenceService 10 | {{- if .inferenceServiceSpecification }} 11 | metadata: 12 | {{- if .inferenceServiceSpecification.metadata }} 13 | {{ toYaml (merge .inferenceServiceSpecification.metadata $metadata) | nindent 2 | trim }} 14 | {{- else }} 15 | {{ toYaml $metadata | nindent 2 | trim }} 16 | {{- end }} {{- /* if .inferenceServiceSpecification.metadata */}} 17 | spec: 18 | {{ toYaml .inferenceServiceSpecification.spec | nindent 2 | trim }} 19 | {{- else }} 20 | {{- if not .storageUri }} {{- /* require .storageUri */}} 21 | {{- print "missing field: storageUri required when inferenceServiceSpecification absent" | fail }} 22 | {{- end }} {{- /* if not .storageUri */}} 23 | metadata: 24 | {{ toYaml $metadata | nindent 2 | trim }} 25 | spec: 26 | predictor: 27 | minReplicas: 1 28 | model: 29 | modelFormat: 30 | name: {{ .modelFormat }} 31 | runtime: {{ .runtime }} 32 | storageUri: {{ .storageUri }} 33 | {{- if .protocolVersion }} 34 | protocolVersion: {{ .protocolVersion }} 35 | {{- end }} {{- /* if .protocolVersion */}} 36 | {{- if .ports }} 37 | ports: 38 | {{ toYaml .ports | nindent 6 | trim }} 39 | {{- end }} {{- /* if .ports */}} 40 | {{- end }} {{- /* if .inferenceServiceSpecification */}} 41 | 42 | {{- end }} {{- /* define "env.kserve.version.isvc" */}} 43 | -------------------------------------------------------------------------------- /charts/release/templates/_mm-istio.blue-green.routemap.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.mm-istio.blue-green.routemap" }} 2 | 3 | {{- $APP_NAME := (include "application.name" .) }} 4 | {{- $APP_NAMESPACE := (include "application.namespace" .) }} 5 | {{- $versions := include "normalize.versions.kserve-mm" . | mustFromJson }} 6 | 7 | apiVersion: v1 8 | kind: ConfigMap 9 | {{- template "routemap.metadata" . }} 10 | data: 11 | strSpec: | 12 | versions: 13 | {{- range $i, $v := $versions }} 14 | - resources: 15 | - gvrShort: isvc 16 | name: {{ template "isvc.name" $v }} 17 | namespace: {{ template "isvc.namespace" $v }} 18 | - gvrShort: cm 19 | name: {{ $v.VERSION_NAME }}-weight-config 20 | namespace: {{ $v.VERSION_NAMESPACE }} 21 | weight: {{ $v.weight }} 22 | {{- end }} {{- /* range $i, $v := $versions */}} 23 | routingTemplates: 24 | {{ .Values.application.strategy }}: 25 | gvrShort: vs 26 | template: | 27 | apiVersion: networking.istio.io/v1beta1 28 | kind: VirtualService 29 | metadata: 30 | name: {{ $APP_NAME }} 31 | namespace: {{ $APP_NAMESPACE }} 32 | spec: 33 | gateways: 34 | {{- if .Values.gateway }} 35 | - {{ .Values.gateway }} 36 | {{- end }} 37 | - mesh 38 | hosts: 39 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }} 40 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }}.svc 41 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }}.svc.cluster.local 42 | http: 43 | - route: 44 | # primary model version 45 | {{- $v := (index $versions 0) }} 46 | - destination: 47 | host: {{ template "mm.serviceHost" }} 48 | port: 49 | number: {{ template "mm.servicePort" . }} 50 | {{- if gt (len $versions) 1 }} 51 | {{ `{{- if gt (index .Weights 1) 0 }}` }} 52 | weight: {{ `{{ index .Weights 0 }}` }} 53 | {{ `{{- end }}` }} 54 | {{- end }} {{- /* if gt (len $versions) 1 */}} 55 | headers: 56 | request: 57 | set: 58 | mm-vmodel-id: {{ template "isvc.name" $v }} 59 | remove: 60 | - branch 61 | response: 62 | add: 63 | app-version: {{ template "isvc.name" $v }} 64 | # non-primary model versions 65 | {{- range $i, $v := (rest $versions) }} 66 | - destination: 67 | host: {{ template "mm.serviceHost" $ }} 68 | port: 69 | number: {{ template "mm.servicePort" $ }} 70 | weight: {{ `{{ index .Weights ` }}{{ print (add1 $i) }}{{ ` }}` }} 71 | headers: 72 | request: 73 | set: 74 | mm-vmodel-id: {{ template "isvc.name" $v }} 75 | response: 76 | add: 77 | app-version: {{ template "isvc.name" $v }} 78 | {{- end }} {{- /* {{- range $i, $v := (rest $versions) }} */}} 79 | 80 | {{- end }} {{- /* define "env.mm-istio.blue-green.routemap" */}} 81 | -------------------------------------------------------------------------------- /charts/release/templates/_mm-istio.blue-green.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.mm-istio.blue-green" }} 2 | 3 | {{- /* ServiceEntry */}} 4 | {{ include "env.mm-istio.service" . }} 5 | --- 6 | 7 | {{- /* prepare versions for simpler processing */}} 8 | {{- $versions := include "normalize.versions.kserve-mm" . | mustFromJson }} 9 | 10 | {{- /* weight-config ConfigMaps */}} 11 | {{- range $i, $v := $versions }} 12 | {{ include "configmap.weight-config" $v }} 13 | --- 14 | {{- end }} {{- /* range $i, $v := $versions */}} 15 | 16 | {{- /* routemap */}} 17 | {{ include "env.mm-istio.blue-green.routemap" . }} 18 | 19 | {{- end }} {{- /* define "env.mm-istio.blue-green" */}} -------------------------------------------------------------------------------- /charts/release/templates/_mm-istio.canary.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.mm-istio.canary" }} 2 | 3 | {{- /* ServiceEntry */}} 4 | {{ include "env.mm-istio.service" . }} 5 | --- 6 | 7 | {{- /* routemap */}} 8 | {{ include "env.mm-istio.canary.routemap" . }} 9 | 10 | {{- end }} {{- /* define "env.mm-istio.canary" */}} -------------------------------------------------------------------------------- /charts/release/templates/_mm-istio.none.routemap.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.mm-istio.none.routemap" }} 2 | 3 | {{- $versions := include "normalize.versions.kserve-mm" . | mustFromJson }} 4 | 5 | apiVersion: v1 6 | kind: ConfigMap 7 | {{- template "routemap.metadata" . }} 8 | data: 9 | strSpec: | 10 | versions: 11 | {{- range $i, $v := $versions }} 12 | - resources: 13 | - gvrShort: isvc 14 | name: {{ template "isvc.name" $v }} 15 | namespace: {{ template "isvc.namespace" $v }} 16 | {{- end }} 17 | 18 | {{- end }} {{- /* define "env.mm-istio.none.routemap" */}} 19 | -------------------------------------------------------------------------------- /charts/release/templates/_mm-istio.none.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.mm-istio.none" }} 2 | 3 | {{- /* routemap */}} 4 | {{ include "env.mm-istio.none.routemap" . }} 5 | 6 | {{- end }} {{- /* define "env.mm-istio.none" */}} -------------------------------------------------------------------------------- /charts/release/templates/_mm-istio.service.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.mm-istio.service" }} 2 | 3 | {{- $APP_NAME := (include "application.name" .) }} 4 | {{- $APP_NAMESPACE := (include "application.namespace" .) }} 5 | 6 | apiVersion: networking.istio.io/v1beta1 7 | kind: ServiceEntry 8 | metadata: 9 | name: {{ $APP_NAME }} 10 | namespace: {{ $APP_NAMESPACE }} 11 | spec: 12 | hosts: 13 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }} 14 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }}.svc 15 | - {{ $APP_NAME }}.{{ $APP_NAMESPACE }}.svc.cluster.local 16 | location: MESH_INTERNAL 17 | ports: 18 | - number: {{ template "mm.servicePort" . }} 19 | name: http 20 | protocol: HTTP 21 | resolution: DNS 22 | workloadSelector: 23 | labels: 24 | modelmesh-service: modelmesh-serving 25 | {{- end }} {{- /* define "env.mm-istio.service" */}} -------------------------------------------------------------------------------- /charts/release/templates/_mm-istio.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.mm-istio" }} 2 | 3 | {{- /* Prepare versions for simpler processing */}} 4 | {{- $versions := include "normalize.versions.kserve-mm" . | mustFromJson }} 5 | 6 | {{- /* InferenceServices */}} 7 | {{- range $i, $v := $versions }} 8 | {{ include "env.mm-istio.version.isvc" $v }} 9 | --- 10 | {{- end }} {{- /* range $i, $v := $versions */}} 11 | 12 | {{- /* routemap (and other strategy specific objects) */}} 13 | {{- if not .Values.application.strategy }} 14 | {{ include "env.mm-istio.none" . }} 15 | {{- else if eq "none" .Values.application.strategy }} 16 | {{ include "env.mm-istio.none" . }} 17 | {{- else if eq "blue-green" .Values.application.strategy }} 18 | {{ include "env.mm-istio.blue-green" . }} 19 | {{- else if eq "canary" .Values.application.strategy }} 20 | {{ include "env.mm-istio.canary" . }} 21 | {{- end }} {{- /* if eq ... .Values.application.strategy */}} 22 | 23 | {{- end }} {{- /* define "env.mm-istio" */}} -------------------------------------------------------------------------------- /charts/release/templates/_mm-istio.version.isvc.tpl: -------------------------------------------------------------------------------- 1 | {{- define "env.mm-istio.version.isvc" }} 2 | 3 | {{- /* compute basic metadata */}} 4 | {{- $metadata := include "application.version.metadata" . | mustFromJson }} 5 | {{- /* add annotation serving.kserve.io/deploymentMode */}} 6 | {{- $metadata := set $metadata "annotations" (merge $metadata.annotations (dict "serving.kserve.io/deploymentMode" "ModelMesh")) }} 7 | 8 | {{- /* define InferenceServcie */}} 9 | apiVersion: serving.kserve.io/v1beta1 10 | kind: InferenceService 11 | {{- if .inferenceServiceSpecification }} 12 | metadata: 13 | {{- if .inferenceServiceSpecification.metadata }} 14 | {{ toYaml (merge .inferenceServiceSpecification.metadata $metadata) | nindent 2 | trim }} 15 | {{- else }} 16 | {{ toYaml $metadata | nindent 2 | trim }} 17 | {{- end }} {{- /* if .inferenceServiceSpecification.metadata */}} 18 | spec: 19 | {{ toYaml .inferenceServiceSpecification.spec | nindent 2 | trim }} 20 | {{- else }} 21 | {{- if not .modelFormat }} {{- /* require .modelFormat */}} 22 | {{- print "missing field: modelFormat required when inferenceServiceSpecification absent" | fail }} 23 | {{- end }} {{- /* if not .modelFormat */}} 24 | {{- if not .storageUri }} {{- /* require .storageUri */}} 25 | {{- print "missing field: storageUri required when inferenceServiceSpecification absent" | fail }} 26 | {{- end }} {{- /* if not .storageUri */}} 27 | metadata: 28 | {{ toYaml $metadata | nindent 2 | trim }} 29 | spec: 30 | predictor: 31 | model: 32 | modelFormat: 33 | name: {{ .modelFormat }} 34 | storageUri: {{ .storageUri }} 35 | {{- end }} {{- /* if .inferenceServiceSpecification */}} 36 | 37 | {{- end }} {{- /* define "env.mm-istio.version.isvc" */}} 38 | -------------------------------------------------------------------------------- /charts/release/templates/release.yaml: -------------------------------------------------------------------------------- 1 | {{- /* Verify that .Values.environment is valid */}} 2 | {{- if not .Values.environment }} 3 | {{- printf "environment is required" | fail }} 4 | {{- end }} {{- /* if not .Values.environment */}} 5 | 6 | {{- /* Verify that .Values.application is valid */}} 7 | {{- if not .Values.application }} 8 | {{- printf "application is required" | fail }} 9 | {{- end }} {{- /* if not .Values.application */}} 10 | 11 | {{- /* Different processing based on .Values.environment */}} 12 | {{- if eq "deployment" .Values.environment }} 13 | {{- include "env.deployment" . }} 14 | {{- else if eq "deployment-gtw" .Values.environment }} 15 | {{- include "env.deployment-gtw" . }} 16 | {{- else if eq "deployment-istio" .Values.environment }} 17 | {{- include "env.deployment-istio" . }} 18 | {{- else if eq "kserve-modelmesh-istio" .Values.environment }} 19 | {{- include "env.mm-istio" . }} 20 | {{- else if has .Values.environment (list "kserve" "kserve-0.11") }} 21 | {{- include "env.kserve" . }} 22 | {{- else if eq "kserve-0.10" .Values.environment }} 23 | {{- include "env.kserve-10" . }} 24 | {{- else }} 25 | {{- printf "Unknown environment: '%s'" .Values.environment | fail }} 26 | {{- end }} {{- /* if eq ,,, .Values.environment */}} 27 | -------------------------------------------------------------------------------- /charts/release/values.yaml: -------------------------------------------------------------------------------- 1 | # iter8Version is the minor version of Iter8 2 | # should be specified as the value of the iter8.tools/version label on all routemaps 3 | iter8Version: v1.1 4 | 5 | # default Istio Gateway name 6 | istioGateway: my-gateway 7 | -------------------------------------------------------------------------------- /cmd/controllers.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/iter8-tools/iter8/abn" 10 | "github.com/iter8-tools/iter8/base/log" 11 | "github.com/iter8-tools/iter8/controllers" 12 | "github.com/iter8-tools/iter8/controllers/k8sclient" 13 | "github.com/iter8-tools/iter8/metrics" 14 | "github.com/spf13/cobra" 15 | "google.golang.org/grpc" 16 | ) 17 | 18 | // controllersDesc is the description of controllers cmd 19 | const controllersDesc = ` 20 | Start Iter8 controllers. 21 | 22 | $ iter8 controllers 23 | ` 24 | 25 | // newControllersCmd creates the Iter8 controllers 26 | // when invoking this function for real, set stopCh to nil 27 | // this will block the controller from exiting until an os.Interrupt; 28 | // when invoking this function in a unit test, set stopCh to ctx.Done() 29 | // this will exit the controller when cancel() is called by the parent function; 30 | func newControllersCmd(stopCh <-chan struct{}, client k8sclient.Interface) *cobra.Command { 31 | cmd := &cobra.Command{ 32 | Use: "controllers", 33 | Short: "Start Iter8 controllers", 34 | Long: controllersDesc, 35 | Args: cobra.NoArgs, 36 | SilenceUsage: true, 37 | RunE: func(_ *cobra.Command, _ []string) error { 38 | // createSigCh indicates if we should create sigCh (channel) that fires on interrupt 39 | createSigCh := false 40 | 41 | ctx, cancel := context.WithCancel(context.Background()) 42 | defer cancel() 43 | // if stopCh is nil, create sigCh to exit this func on interrupt, 44 | // and use ctx.Done() to clean up controllers when exiting; 45 | // otherwise, simply use stopCh for both 46 | if stopCh == nil { 47 | stopCh = ctx.Done() 48 | createSigCh = true 49 | } 50 | 51 | if client == nil { 52 | var err error 53 | client, err = k8sclient.New(settings) 54 | if err != nil { 55 | log.Logger.Error("could not obtain Kube client... ") 56 | return err 57 | } 58 | } 59 | 60 | if err := controllers.Start(stopCh, client); err != nil { 61 | log.Logger.Error("controllers did not start... ") 62 | return err 63 | } 64 | log.Logger.Debug("started controllers... ") 65 | 66 | // launch gRPC server to respond to frontend requests 67 | go func() { 68 | err := abn.LaunchGRPCServer([]grpc.ServerOption{}, stopCh) 69 | if err != nil { 70 | log.Logger.Error("cound not start A/B/n service") 71 | } 72 | }() 73 | 74 | // launch metrics HTTP server to respond to support Grafana visualization 75 | go func() { 76 | err := metrics.Start(stopCh) 77 | if err != nil { 78 | log.Logger.Error("count not start A/B/n metrics service") 79 | } 80 | }() 81 | 82 | // if createSigCh, then block until there is an os.Interrupt 83 | if createSigCh { 84 | sigCh := make(chan os.Signal, 1) 85 | signal.Notify(sigCh, syscall.SIGTERM, os.Interrupt) 86 | <-sigCh 87 | log.Logger.Warn("SIGTERM... ") 88 | } 89 | 90 | return nil 91 | }, 92 | } 93 | return cmd 94 | } 95 | -------------------------------------------------------------------------------- /cmd/controllers_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | "time" 8 | 9 | "github.com/iter8-tools/iter8/base" 10 | "github.com/iter8-tools/iter8/controllers/k8sclient/fake" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestControllers(t *testing.T) { 15 | // set pod name 16 | _ = os.Setenv("POD_NAME", "pod-0") 17 | // set pod namespace 18 | _ = os.Setenv("POD_NAMESPACE", "default") 19 | // set config file 20 | _ = os.Setenv("CONFIG_FILE", base.CompletePath("../", "testdata/controllers/config.yaml")) 21 | 22 | kubeClient = fake.New(nil, nil) 23 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 24 | defer cancel() 25 | cmd := newControllersCmd(ctx.Done(), kubeClient) 26 | err := cmd.RunE(cmd, nil) 27 | assert.NoError(t, err) 28 | } 29 | -------------------------------------------------------------------------------- /cmd/doc.go: -------------------------------------------------------------------------------- 1 | // Package cmd defines the Iter8 CLI commands and their flags. 2 | package cmd 3 | -------------------------------------------------------------------------------- /cmd/docs.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/spf13/cobra/doc" 10 | "golang.org/x/text/cases" 11 | "golang.org/x/text/language" 12 | 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // docsDesc is the description for the docs command 17 | const docsDesc = ` 18 | Generate markdown documentation for Iter8 CLI commands. Documentation will be generated for all commands that are not hidden. 19 | ` 20 | 21 | // newDocsCmd creates the docs command 22 | func newDocsCmd() *cobra.Command { 23 | docsDir := "" 24 | cmd := &cobra.Command{ 25 | Use: "docs", 26 | Short: "Generate markdown documentation for Iter8 CLI", 27 | Long: docsDesc, 28 | Hidden: true, 29 | SilenceUsage: true, 30 | RunE: func(cmd *cobra.Command, args []string) error { 31 | standardLinks := func(s string) string { return s } 32 | 33 | hdrFunc := func(filename string) string { 34 | base := filepath.Base(filename) 35 | name := strings.TrimSuffix(base, path.Ext(base)) 36 | caser := cases.Title(language.English) 37 | title := caser.String(strings.Replace(name, "_", " ", -1)) 38 | tpl := `--- 39 | template: main.html 40 | title: "%s" 41 | hide: 42 | - toc 43 | --- 44 | ` 45 | return fmt.Sprintf(tpl, title) 46 | } 47 | 48 | // automatically generate markdown documentation for all Iter8 commands 49 | return doc.GenMarkdownTreeCustom(rootCmd, docsDir, hdrFunc, standardLinks) 50 | }, 51 | } 52 | addDocsFlags(cmd, &docsDir) 53 | return cmd 54 | } 55 | 56 | // addDocsFlags defines the flags for the docs command 57 | func addDocsFlags(cmd *cobra.Command, docsDirPtr *string) { 58 | cmd.Flags().StringVar(docsDirPtr, "commandDocsDir", "", "directory where Iter8 CLI documentation will be created") 59 | _ = cmd.MarkFlagRequired("commandDocsDir") 60 | } 61 | -------------------------------------------------------------------------------- /cmd/docs_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestDocs(t *testing.T) { 10 | _ = os.Chdir(t.TempDir()) 11 | tests := []cmdTestCase{ 12 | // assert 13 | { 14 | name: "create docs", 15 | cmd: fmt.Sprintf("docs --commandDocsDir %v", t.TempDir()), 16 | }, 17 | } 18 | 19 | runTestActionCmd(t, tests) 20 | } 21 | -------------------------------------------------------------------------------- /cmd/k.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/iter8-tools/iter8/base/log" 7 | "github.com/iter8-tools/iter8/driver" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // kcmd is the root command that enables Kubernetes experiments 12 | var kcmd = &cobra.Command{ 13 | Use: "k", 14 | Short: "Work with Kubernetes experiments", 15 | Long: "Work with Kubernetes experiments", 16 | } 17 | 18 | // addTestFlag adds the test flag 19 | func addTestFlag(cmd *cobra.Command, testP *string) { 20 | cmd.Flags().StringVarP(testP, "test", "t", driver.DefaultTestName, "name of the test") 21 | } 22 | 23 | func init() { 24 | settings.AddFlags(kcmd.PersistentFlags()) 25 | // hiding these Helm flags for now 26 | if err := kcmd.PersistentFlags().MarkHidden("debug"); err != nil { 27 | log.Logger.Fatal(err) 28 | os.Exit(1) 29 | } 30 | if err := kcmd.PersistentFlags().MarkHidden("registry-config"); err != nil { 31 | log.Logger.Fatal(err) 32 | os.Exit(1) 33 | } 34 | if err := kcmd.PersistentFlags().MarkHidden("repository-config"); err != nil { 35 | log.Logger.Fatal(err) 36 | os.Exit(1) 37 | } 38 | if err := kcmd.PersistentFlags().MarkHidden("repository-cache"); err != nil { 39 | log.Logger.Fatal(err) 40 | os.Exit(1) 41 | } 42 | 43 | // add k run 44 | kcmd.AddCommand(newKRunCmd(kd, os.Stdout)) 45 | } 46 | -------------------------------------------------------------------------------- /cmd/krun.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "io" 5 | 6 | ia "github.com/iter8-tools/iter8/action" 7 | "github.com/iter8-tools/iter8/driver" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // krunDesc is the description of the k run command 12 | const krunDesc = ` 13 | Run a performance test on Kubernetes. This command reads a test specified in a secret and writes the result back to the secret. 14 | 15 | $ iter8 k run --namespace {{ namespace }} --test {{ test name }} 16 | 17 | This command is intended for use within the Iter8 Docker image that is used to execute Kubernetes tests. 18 | ` 19 | 20 | // newKRunCmd creates the Kubernetes run command 21 | func newKRunCmd(kd *driver.KubeDriver, _ io.Writer) *cobra.Command { 22 | actor := ia.NewRunOpts(kd) 23 | actor.EnvSettings = settings 24 | cmd := &cobra.Command{ 25 | Use: "run", 26 | Short: "Run a performance test on Kubernetes", 27 | Long: krunDesc, 28 | SilenceUsage: true, 29 | Hidden: true, 30 | RunE: func(_ *cobra.Command, _ []string) error { 31 | return actor.KubeRun() 32 | }, 33 | } 34 | addTestFlag(cmd, &actor.Test) 35 | return cmd 36 | } 37 | -------------------------------------------------------------------------------- /cmd/krun_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "testing" 11 | 12 | "fortio.org/fortio/fhttp" 13 | "github.com/iter8-tools/iter8/base" 14 | id "github.com/iter8-tools/iter8/driver" 15 | "github.com/stretchr/testify/assert" 16 | corev1 "k8s.io/api/core/v1" 17 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 | ) 19 | 20 | const ( 21 | myName = "myName" 22 | myNamespace = "myNamespace" 23 | ) 24 | 25 | func TestKRun(t *testing.T) { 26 | // define METRICS_SERVER_URL 27 | metricsServerURL := "http://iter8.default:8080" 28 | err := os.Setenv(base.MetricsServerURL, metricsServerURL) 29 | assert.NoError(t, err) 30 | 31 | // create and configure HTTP endpoint for testing 32 | mux, addr := fhttp.DynamicHTTPServer(false) 33 | url := fmt.Sprintf("http://127.0.0.1:%d/get", addr.Port) 34 | var verifyHandlerCalled bool 35 | mux.HandleFunc("/get", base.GetTrackingHandler(&verifyHandlerCalled)) 36 | 37 | // mock metrics server 38 | base.StartHTTPMock(t) 39 | metricsServerCalled := false 40 | base.MockMetricsServer(base.MockMetricsServerInput{ 41 | MetricsServerURL: metricsServerURL, 42 | ExperimentResultCallback: func(req *http.Request) { 43 | metricsServerCalled = true 44 | 45 | // check query parameters 46 | assert.Equal(t, myName, req.URL.Query().Get("test")) 47 | assert.Equal(t, myNamespace, req.URL.Query().Get("namespace")) 48 | 49 | // check payload 50 | body, err := io.ReadAll(req.Body) 51 | assert.NoError(t, err) 52 | assert.NotNil(t, body) 53 | 54 | // check payload content 55 | bodyExperimentResult := base.ExperimentResult{} 56 | 57 | err = json.Unmarshal(body, &bodyExperimentResult) 58 | assert.NoError(t, err) 59 | assert.NotNil(t, body) 60 | assert.Equal(t, myName, bodyExperimentResult.Name) 61 | assert.Equal(t, myNamespace, bodyExperimentResult.Namespace) 62 | }, 63 | }) 64 | 65 | _ = os.Chdir(t.TempDir()) 66 | 67 | // create experiment.yaml 68 | base.CreateExperimentYaml(t, base.CompletePath("../testdata", base.ExperimentTemplateFile), url, base.ExperimentFile) 69 | 70 | tests := []cmdTestCase{ 71 | // k report 72 | { 73 | name: "k run", 74 | cmd: "k run -t default --namespace default", 75 | golden: base.CompletePath("../testdata", "output/krun.txt"), 76 | }, 77 | } 78 | 79 | // fake kube cluster 80 | *kd = *id.NewFakeKubeDriver(settings) 81 | 82 | // and read it... 83 | byteArray, _ := os.ReadFile(base.ExperimentFile) 84 | _, _ = kd.Clientset.CoreV1().Secrets("default").Create(context.TODO(), &corev1.Secret{ 85 | ObjectMeta: metav1.ObjectMeta{ 86 | Name: "default", 87 | Namespace: "default", 88 | }, 89 | StringData: map[string]string{base.ExperimentFile: string(byteArray)}, 90 | }, metav1.CreateOptions{}) 91 | 92 | runTestActionCmd(t, tests) 93 | // sanity check -- handler was called 94 | assert.True(t, verifyHandlerCalled) 95 | assert.True(t, metricsServerCalled) 96 | } 97 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/iter8-tools/iter8/controllers/k8sclient" 5 | "github.com/iter8-tools/iter8/driver" 6 | 7 | "github.com/iter8-tools/iter8/base/log" 8 | "github.com/sirupsen/logrus" 9 | "github.com/spf13/cobra" 10 | "helm.sh/helm/v3/pkg/cli" 11 | ) 12 | 13 | var ( 14 | // default log level for Iter8 CLI 15 | logLevel = "info" 16 | // Default Helm and Kubernetes settings 17 | settings = cli.New() 18 | // KubeDriver used by actions package 19 | kd = driver.NewKubeDriver(settings) 20 | // kubeclient is the client used for controllers package 21 | kubeClient k8sclient.Interface 22 | ) 23 | 24 | // rootCmd represents the base command when called without any subcommands 25 | var rootCmd = &cobra.Command{ 26 | Use: "iter8", 27 | Short: "Kubernetes release optimizer", 28 | Long: ` 29 | Iter8 is the Kubernetes release optimizer built for DevOps, MLOps, SRE and data science teams. Iter8 makes it easy to ensure that Kubernetes apps and ML models perform well and maximize business value. 30 | `, 31 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error { 32 | ll, err := logrus.ParseLevel(logLevel) 33 | if err != nil { 34 | log.Logger.Error(err) 35 | return err 36 | } 37 | log.Logger.Level = ll 38 | return nil 39 | }, 40 | } 41 | 42 | // Execute adds all child commands to the root command and sets flags appropriately. 43 | // This is called by main.main(). It only needs to happen once to the rootCmd. 44 | func Execute() { 45 | cobra.CheckErr(rootCmd.Execute()) 46 | } 47 | 48 | // initialize Iter8 CLI root command and add all subcommands 49 | func init() { 50 | // disable completion command for now 51 | rootCmd.CompletionOptions.DisableDefaultCmd = true 52 | rootCmd.PersistentFlags().StringVarP(&logLevel, "loglevel", "l", "info", "trace, debug, info, warning, error, fatal, panic") 53 | rootCmd.SilenceErrors = true // will get printed in Execute() (by cobra.CheckErr()) 54 | 55 | // add docs 56 | rootCmd.AddCommand(newDocsCmd()) 57 | 58 | // add k 59 | rootCmd.AddCommand(kcmd) 60 | 61 | // add version 62 | rootCmd.AddCommand(newVersionCmd()) 63 | 64 | // add controllers 65 | rootCmd.AddCommand(newControllersCmd(nil, kubeClient)) 66 | 67 | } 68 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | 7 | "github.com/iter8-tools/iter8/base" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // versionDesc is the description of the version command 12 | var versionDesc = ` 13 | Print the version of Iter8 CLI. 14 | 15 | $ iter8 version 16 | 17 | The output may look as follows: 18 | 19 | $ cmd.BuildInfo{Version:"v0.13.0", GitCommit:"f24e86f3d3eceb02eabbba54b40af2c940f55ad5", GoVersion:"go1.19.3"} 20 | 21 | In the sample output shown above: 22 | 23 | - Version is the semantic version of the Iter8 CLI. 24 | - GitCommit is the SHA hash for the commit that this version was built from. 25 | - GoVersion is the version of Go that was used to compile Iter8 CLI. 26 | ` 27 | 28 | var ( 29 | // gitCommit is the git sha1 30 | gitCommit = "" 31 | ) 32 | 33 | // BuildInfo describes the compile time information. 34 | type BuildInfo struct { 35 | // Version is the semantic version 36 | Version string `json:"version,omitempty"` 37 | // GitCommit is the git sha1. 38 | GitCommit string `json:"git_commit,omitempty"` 39 | // GoVersion is the version of the Go compiler used to compile Iter8. 40 | GoVersion string `json:"go_version,omitempty"` 41 | } 42 | 43 | // newVersionCmd creates the version command 44 | func newVersionCmd() *cobra.Command { 45 | var short bool 46 | // versionCmd represents the version command 47 | cmd := &cobra.Command{ 48 | Use: "version", 49 | Short: "Print Iter8 CLI version", 50 | Long: versionDesc, 51 | SilenceErrors: true, 52 | RunE: func(_ *cobra.Command, _ []string) error { 53 | v := getBuildInfo() 54 | if short { 55 | if len(v.GitCommit) >= 7 { 56 | fmt.Printf("%s+g%s", base.Version, v.GitCommit[:7]) 57 | fmt.Println() 58 | return nil 59 | } 60 | fmt.Println(base.Version) 61 | return nil 62 | } 63 | fmt.Printf("%#v", v) 64 | fmt.Println() 65 | return nil 66 | }, 67 | } 68 | addShortFlag(cmd, &short) 69 | return cmd 70 | } 71 | 72 | // get returns build info 73 | func getBuildInfo() BuildInfo { 74 | v := BuildInfo{ 75 | Version: base.Version, 76 | GitCommit: gitCommit, 77 | GoVersion: runtime.Version(), 78 | } 79 | return v 80 | } 81 | 82 | // addShortFlag adds the short flag to the version command 83 | func addShortFlag(cmd *cobra.Command, shortPtr *bool) { 84 | cmd.Flags().BoolVar(shortPtr, "short", false, "print abbreviated version info") 85 | cmd.Flags().Lookup("short").NoOptDefVal = "true" 86 | } 87 | -------------------------------------------------------------------------------- /cmd/version_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestVersion(t *testing.T) { 8 | tests := []cmdTestCase{ 9 | // version 10 | { 11 | name: "version", 12 | cmd: "version", 13 | }, 14 | // version 15 | { 16 | name: "version short", 17 | cmd: "version --short", 18 | }, 19 | } 20 | 21 | runTestActionCmd(t, tests) 22 | 23 | } 24 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | # Used by the helm/chart-releaser-action action in the releasecharts.yaml workflow 2 | skip-existing: true -------------------------------------------------------------------------------- /controllers/config.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | util "github.com/iter8-tools/iter8/base" 5 | "k8s.io/apimachinery/pkg/runtime/schema" 6 | ) 7 | 8 | const ( 9 | // configEnv is the name of environment variable with config file path 10 | configEnv = "CONFIG_FILE" 11 | ) 12 | 13 | // GroupVersionResourceConditions is a Kubernetes resource type along with a list of conditions 14 | type GroupVersionResourceConditions struct { 15 | schema.GroupVersionResource 16 | Conditions []string `json:"conditions,omitempty"` 17 | } 18 | 19 | // Config defines the configuration of the controllers 20 | type Config struct { 21 | // ResourceTypes map from shortnames of Kubernetes API resources to their GVRs with conditions 22 | ResourceTypes map[string]GroupVersionResourceConditions `json:"resourceTypes,omitempty"` 23 | // DefaultResync period for controller watch functions 24 | DefaultResync string `json:"defaultResync,omitempty"` 25 | // ClusterScoped is true if Iter8 controller is cluster-scoped 26 | ClusterScoped bool `json:"clusterScoped,omitempty"` 27 | } 28 | 29 | // readConfig reads configuration information from file 30 | func readConfig() (*Config, error) { 31 | conf := &Config{} 32 | err := util.ReadConfig(configEnv, conf, func() {}) 33 | return conf, err 34 | } 35 | 36 | // validate the config 37 | // no-op for now 38 | func (c *Config) validate() error { 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /controllers/config_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/iter8-tools/iter8/base" 8 | "github.com/stretchr/testify/assert" 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | ) 11 | 12 | func TestReadConfig(t *testing.T) { 13 | var tests = []struct { 14 | confEnv bool 15 | confFile string 16 | valid bool 17 | }{ 18 | {true, base.CompletePath("../", "testdata/controllers/config.yaml"), true}, 19 | {false, base.CompletePath("../", "testdata/controllers/config.yaml"), false}, 20 | {true, base.CompletePath("../", "testdata/controllers/garb.age"), false}, 21 | {true, base.CompletePath("../", "this/file/does/not/exist"), false}, 22 | } 23 | 24 | for _, tt := range tests { 25 | _ = os.Unsetenv(configEnv) 26 | if tt.confEnv { 27 | _ = os.Setenv(configEnv, tt.confFile) 28 | } 29 | 30 | c, err := readConfig() 31 | if tt.valid { 32 | assert.NoError(t, err) 33 | assert.Equal(t, "15m", c.DefaultResync) 34 | assert.Equal(t, 5, len(c.ResourceTypes)) 35 | isvc := c.ResourceTypes["isvc"] 36 | assert.Equal(t, isvc, GroupVersionResourceConditions{ 37 | GroupVersionResource: schema.GroupVersionResource{ 38 | Group: "serving.kserve.io", 39 | Version: "v1beta1", 40 | Resource: "inferenceservices", 41 | }, 42 | Conditions: []string{ 43 | "Ready", 44 | }, 45 | }) 46 | } else { 47 | assert.Error(t, err) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /controllers/events.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/iter8-tools/iter8/controllers/k8sclient" 5 | corev1 "k8s.io/api/core/v1" 6 | "k8s.io/apimachinery/pkg/runtime" 7 | typedv1core "k8s.io/client-go/kubernetes/typed/core/v1" 8 | "k8s.io/client-go/tools/record" 9 | ) 10 | 11 | // broadcastEvent broadcasts an event to the controller 12 | func broadcastEvent(object runtime.Object, eventtype, reason, message string, client k8sclient.Interface) { 13 | if object != nil { 14 | scheme := runtime.NewScheme() 15 | _ = corev1.AddToScheme(scheme) 16 | 17 | // TODO: Do we want to reuse the event broadcaster? 18 | eventBroadcaster := record.NewBroadcaster() 19 | eventBroadcaster.StartStructuredLogging(4) 20 | eventBroadcaster.StartRecordingToSink(&typedv1core.EventSinkImpl{Interface: client.CoreV1().Events("")}) 21 | eventRecorder := eventBroadcaster.NewRecorder(scheme, corev1.EventSource{}) 22 | 23 | eventRecorder.Event(object, eventtype, reason, message) 24 | eventBroadcaster.Shutdown() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /controllers/interface.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | // RoutemapsInterface defines behavior for a set of routemaps 4 | type RoutemapsInterface interface { 5 | // GetRoutemapFromNamespaceName returns a route map with the given namespace and name 6 | GetRoutemapFromNamespaceName(string, string) RoutemapInterface 7 | } 8 | 9 | // RoutemapInterface defines behavior of a routemap 10 | type RoutemapInterface interface { 11 | // RLock locks the object for reading 12 | RLock() 13 | // RUnlock unlocks an object locked for reading 14 | RUnlock() 15 | // GetName returns the name of the object 16 | GetName() string 17 | // GetNamespace returns the namespace of the object 18 | GetNamespace() string 19 | // Weights provides the relative weights from traffic routing between versions 20 | Weights() []uint32 21 | // GetVersions returns a list of versions 22 | GetVersions() []VersionInterface 23 | } 24 | 25 | // VersionInterface defines behavior for a version 26 | type VersionInterface interface { 27 | // GetSignature returns a signature of a version 28 | GetSignature() *string 29 | } 30 | 31 | // AllRouteMapsInterface is interface defines way to get all routemaps 32 | type AllRouteMapsInterface interface { 33 | GetAllRoutemaps() RoutemapsInterface 34 | } 35 | 36 | // DefaultRoutemaps is default implementation 37 | type DefaultRoutemaps struct{} 38 | 39 | // GetAllRoutemaps is default implementation that returns package local map 40 | func (cm *DefaultRoutemaps) GetAllRoutemaps() RoutemapsInterface { 41 | return &AllRoutemaps 42 | } 43 | -------------------------------------------------------------------------------- /controllers/interface_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGetAllRoutemaps(t *testing.T) { 10 | rm := DefaultRoutemaps{} 11 | assert.NotNil(t, rm.GetAllRoutemaps()) 12 | } 13 | -------------------------------------------------------------------------------- /controllers/k8sclient/fake/simple.go: -------------------------------------------------------------------------------- 1 | // Package fake provides fake Kuberntes clients for testing 2 | package fake 3 | 4 | import ( 5 | "context" 6 | 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "k8s.io/apimachinery/pkg/types" 12 | fakedynamic "k8s.io/client-go/dynamic/fake" 13 | fakek8s "k8s.io/client-go/kubernetes/fake" 14 | ) 15 | 16 | // Client provides structured and dynamic fake clients 17 | type Client struct { 18 | *fakek8s.Clientset 19 | *fakedynamic.FakeDynamicClient 20 | } 21 | 22 | /* 23 | Patch applies a patch for a resource. 24 | Important: fake clients should not be used for server-side apply or strategic-merge patches. 25 | https://github.com/kubernetes/kubernetes/pull/78630#issuecomment-500424163 26 | 27 | Hence, we are mocking the Patch call in this fake client so that, 28 | instead of server-side apply as in the real client, we perform of merge patch instead. 29 | */ 30 | func (cl *Client) Patch(gvr schema.GroupVersionResource, objNamespace string, objName string, jsonBytes []byte) (*unstructured.Unstructured, error) { 31 | return cl.FakeDynamicClient.Resource(gvr).Namespace(objNamespace).Patch(context.TODO(), objName, types.MergePatchType, jsonBytes, metav1.PatchOptions{}) 32 | } 33 | 34 | // New returns a new fake Kubernetes client populated with runtime objects 35 | func New(sObjs []runtime.Object, unsObjs []runtime.Object) *Client { 36 | s := runtime.NewScheme() 37 | return &Client{ 38 | fakek8s.NewSimpleClientset(sObjs...), 39 | fakedynamic.NewSimpleDynamicClientWithCustomListKinds(s, map[schema.GroupVersionResource]string{ 40 | { 41 | Group: "apps", 42 | Version: "v1", 43 | Resource: "deployments", 44 | }: "DeploymentList", 45 | { 46 | Group: "", 47 | Version: "v1", 48 | Resource: "configmaps", 49 | }: "ConfigMapList", 50 | { 51 | Group: "networking.istio.io", 52 | Version: "v1beta1", 53 | Resource: "virtualservices", 54 | }: "VirtualServiceList", 55 | { 56 | Group: "", 57 | Version: "v1", 58 | Resource: "services", 59 | }: "ServiceList", 60 | { 61 | Group: "serving.kserve.io", 62 | Version: "v1beta1", 63 | Resource: "inferenceservices", 64 | }: "InferenceServiceList", 65 | { 66 | Group: "", 67 | Version: "v1", 68 | Resource: "secrets", 69 | }: "SecretList", 70 | }, unsObjs...), 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /controllers/k8sclient/fake/simple_test.go: -------------------------------------------------------------------------------- 1 | package fake 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | ) 13 | 14 | const ( 15 | myns = "myns" 16 | myname = "myname" 17 | myname2 = "myname2" 18 | hello = "hello" 19 | world = "world" 20 | ) 21 | 22 | func TestNew(t *testing.T) { 23 | var tests = []struct { 24 | a []runtime.Object 25 | b bool 26 | }{ 27 | {nil, true}, 28 | {[]runtime.Object{ 29 | &unstructured.Unstructured{ 30 | Object: map[string]interface{}{ 31 | "apiVersion": "iter8.tools", 32 | "kind": "v1", 33 | "metadata": map[string]interface{}{ 34 | "namespace": myns, 35 | "name": myname, 36 | }, 37 | }, 38 | }, 39 | }, true}, 40 | } 41 | 42 | for _, e := range tests { 43 | client := New(nil, e.a) 44 | assert.NotNil(t, client) 45 | } 46 | } 47 | 48 | func TestPatch(t *testing.T) { 49 | gvr := schema.GroupVersionResource{ 50 | Group: "apps", 51 | Version: "v1", 52 | Resource: "deployments", 53 | } 54 | 55 | client := New(nil, []runtime.Object{ 56 | &unstructured.Unstructured{ 57 | Object: map[string]interface{}{ 58 | "apiVersion": "apps/v1", 59 | "kind": "Deployment", 60 | "metadata": map[string]interface{}{ 61 | "namespace": myns, 62 | "name": myname, 63 | }, 64 | }, 65 | }, 66 | }) 67 | 68 | myDeployment, err := client.FakeDynamicClient.Resource(gvr).Namespace(myns).Get(context.TODO(), myname, v1.GetOptions{}) 69 | assert.NoError(t, err) 70 | assert.NotNil(t, myDeployment) 71 | // myDeployment should not have the hello: world label yet 72 | assert.Equal(t, "", myDeployment.GetLabels()[hello]) 73 | 74 | // Create a copy of myDeployment and add the hello: world label 75 | copiedDeployment := myDeployment.DeepCopy() 76 | copiedDeployment.SetLabels(map[string]string{ 77 | hello: world, 78 | }) 79 | newDeploymentBytes, err := copiedDeployment.MarshalJSON() 80 | assert.NoError(t, err) 81 | assert.NotNil(t, newDeploymentBytes) 82 | 83 | // Patch myDeployment 84 | patchedDeployment, err := client.Patch(gvr, myns, myname, newDeploymentBytes) 85 | assert.NoError(t, err) 86 | assert.NotNil(t, patchedDeployment) 87 | // Patched myDeployment should now have the hello: world label 88 | assert.Equal(t, world, patchedDeployment.GetLabels()[hello]) 89 | } 90 | -------------------------------------------------------------------------------- /controllers/k8sclient/interface.go: -------------------------------------------------------------------------------- 1 | // Package k8sclient provides the Kubernetes client for the controllers package 2 | package k8sclient 3 | 4 | import ( 5 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | "k8s.io/client-go/dynamic" 8 | "k8s.io/client-go/kubernetes" 9 | ) 10 | 11 | // Interface enables interaction with a Kubernetes cluster 12 | // Can be mocked in unit tests with fake implementation 13 | type Interface interface { 14 | kubernetes.Interface 15 | dynamic.Interface 16 | Patch(gvr schema.GroupVersionResource, objNamespace string, objName string, by []byte) (*unstructured.Unstructured, error) 17 | } 18 | -------------------------------------------------------------------------------- /controllers/k8sclient/simple.go: -------------------------------------------------------------------------------- 1 | package k8sclient 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/iter8-tools/iter8/base" 8 | "github.com/iter8-tools/iter8/base/log" 9 | "helm.sh/helm/v3/pkg/cli" 10 | 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 13 | "k8s.io/apimachinery/pkg/runtime/schema" 14 | "k8s.io/apimachinery/pkg/types" 15 | 16 | // Import to initialize client auth plugins. 17 | _ "k8s.io/client-go/plugin/pkg/client/auth" 18 | 19 | "k8s.io/client-go/dynamic" 20 | "k8s.io/client-go/kubernetes" 21 | ) 22 | 23 | // Client provides typed and dynamic Kubernetes clients 24 | type Client struct { 25 | // typed Kubernetes client 26 | *kubernetes.Clientset 27 | // dynamic Kubernetes client 28 | *dynamic.DynamicClient 29 | } 30 | 31 | const iter8ControllerFieldManager = "iter8-controller" 32 | 33 | // Patch performs a server-side apply of GVR 34 | func (cl *Client) Patch(gvr schema.GroupVersionResource, objNamespace string, objName string, jsonBytes []byte) (*unstructured.Unstructured, error) { 35 | return cl.DynamicClient.Resource(gvr).Namespace(objNamespace).Patch(context.TODO(), objName, types.ApplyPatchType, jsonBytes, metav1.PatchOptions{ 36 | FieldManager: iter8ControllerFieldManager, 37 | Force: base.BoolPointer(true), 38 | }) 39 | } 40 | 41 | // New creates a new kubernetes client 42 | func New(settings *cli.EnvSettings) (*Client, error) { 43 | log.Logger.Trace("kubernetes client creation invoked...") 44 | 45 | // get rest config 46 | restConfig, err := settings.RESTClientGetter().ToRESTConfig() 47 | if err != nil { 48 | e := errors.New("unable to get Kubernetes REST config") 49 | log.Logger.WithStackTrace(err.Error()).Error(e) 50 | return nil, e 51 | } 52 | 53 | // get clientset 54 | clientset, err := kubernetes.NewForConfig(restConfig) 55 | if err != nil { 56 | e := errors.New("unable to get Kubernetes clientset") 57 | log.Logger.WithStackTrace(err.Error()).Error(e) 58 | return nil, e 59 | } 60 | 61 | // get dynamic client 62 | dynamicClient, err := dynamic.NewForConfig(restConfig) 63 | if err != nil { 64 | e := errors.New("unable to get Kubernetes dynamic client") 65 | log.Logger.WithStackTrace(err.Error()).Error(e) 66 | return nil, e 67 | } 68 | 69 | log.Logger.Trace("returning kubernetes client... ") 70 | 71 | return &Client{ 72 | Clientset: clientset, 73 | DynamicClient: dynamicClient, 74 | }, nil 75 | 76 | } 77 | -------------------------------------------------------------------------------- /controllers/podname.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "strings" 7 | 8 | "github.com/iter8-tools/iter8/base/log" 9 | ) 10 | 11 | const ( 12 | // podNameEnvVariable is the name of the environment variable with pod name 13 | podNameEnvVariable = "POD_NAME" 14 | // podNamespaceEnvVariable is the name of the environment variable with pod namespace 15 | podNamespaceEnvVariable = "POD_NAMESPACE" 16 | // leaderSuffix is used to determine the leader pod 17 | leaderSuffix = "-0" 18 | ) 19 | 20 | // getPodName returns the name of this pod 21 | func getPodName() (string, bool) { 22 | podName, ok := os.LookupEnv(podNameEnvVariable) 23 | // missing env variable is unacceptable 24 | if !ok { 25 | return "", false 26 | } 27 | // empty podName is unacceptable 28 | if len(podName) == 0 { 29 | return "", false 30 | } 31 | return podName, true 32 | } 33 | 34 | // leaderIsMe is true if this pod has the leaderSuffix ("-0") 35 | func leaderIsMe() (bool, error) { 36 | podName, ok := getPodName() 37 | if !ok { 38 | e := errors.New("unable to retrieve pod name") 39 | log.Logger.Error(e) 40 | return false, e 41 | } 42 | return strings.HasSuffix(podName, leaderSuffix), nil 43 | } 44 | -------------------------------------------------------------------------------- /controllers/podname_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | util "github.com/iter8-tools/iter8/base" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGetPodName(t *testing.T) { 12 | var tests = []struct { 13 | a *string 14 | b string 15 | c bool 16 | }{ 17 | {util.StringPointer("x-0"), "x-0", true}, 18 | {util.StringPointer("x-y-0"), "x-y-0", true}, 19 | {util.StringPointer("x-1"), "x-1", true}, 20 | {util.StringPointer("x-y-1"), "x-y-1", true}, 21 | {util.StringPointer("x"), "x", true}, 22 | {util.StringPointer(""), "", false}, 23 | {nil, "", false}, 24 | } 25 | 26 | for _, e := range tests { 27 | if e.a == nil { 28 | _ = os.Unsetenv(podNameEnvVariable) 29 | } else { 30 | _ = os.Setenv(podNameEnvVariable, *e.a) 31 | } 32 | podName, ok := getPodName() 33 | assert.Equal(t, e.b, podName) 34 | assert.Equal(t, e.c, ok) 35 | } 36 | 37 | } 38 | 39 | func TestLeaderIsMe(t *testing.T) { 40 | var tests = []struct { 41 | a string 42 | b bool 43 | c bool 44 | }{ 45 | {"x-0", true, false}, 46 | {"x-y-0", true, false}, 47 | {"x-1", false, false}, 48 | {"x-y-1", false, false}, 49 | {"x", false, false}, 50 | {"", false, true}, 51 | } 52 | 53 | for _, e := range tests { 54 | _ = os.Setenv(podNameEnvVariable, e.a) 55 | leaderStatus, err := leaderIsMe() 56 | assert.Equal(t, e.b, leaderStatus) 57 | if e.c { 58 | assert.Error(t, err) 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /controllers/routemaps_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/iter8-tools/iter8/base" 8 | "github.com/stretchr/testify/assert" 9 | corev1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | ) 12 | 13 | func TestRouteMaps_Delete(t *testing.T) { 14 | s := routemaps{ 15 | mutex: sync.RWMutex{}, 16 | nsRoutemap: map[string]routemapsByName{ 17 | "default": { 18 | "test": { 19 | mutex: sync.RWMutex{}, 20 | ObjectMeta: metav1.ObjectMeta{}, 21 | Versions: []version{}, 22 | RoutingTemplates: map[string]routingTemplate{}, 23 | normalizedWeights: []uint32{}, 24 | }, 25 | }, 26 | }, 27 | } 28 | s.delete(&corev1.ConfigMap{ 29 | TypeMeta: metav1.TypeMeta{}, 30 | ObjectMeta: metav1.ObjectMeta{ 31 | Name: "test", 32 | Namespace: "default", 33 | }, 34 | Immutable: base.BoolPointer(true), 35 | }) 36 | obj, ok := s.nsRoutemap["default"] 37 | assert.False(t, ok) 38 | assert.Nil(t, obj) 39 | } 40 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package main is the entry point for the Iter8 CLI. 2 | // Iter8 is the Kubernetes release optimizer built for DevOps, MLOps, SRE and data science teams. Iter8 makes it easy to ensure that Kubernetes apps and ML models perform well and maximize business value. 3 | package main 4 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22.1-bookworm AS build-stage 2 | 3 | WORKDIR /app 4 | COPY . ./ 5 | 6 | RUN go mod download 7 | 8 | RUN mkdir -p bin \ 9 | && make clean \ 10 | && make build 11 | 12 | 13 | FROM debian:bookworm-slim 14 | 15 | WORKDIR / 16 | 17 | # Install curl 18 | RUN apt-get update && apt-get install -y curl 19 | 20 | # Install /bin/iter8 21 | COPY --from=build-stage /app/bin/iter8 /bin/iter8 22 | 23 | # Set Iter8 version from build args 24 | ARG TAG 25 | ENV TAG=${TAG:-v1.1.0} 26 | 27 | -------------------------------------------------------------------------------- /driver/common.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "github.com/iter8-tools/iter8/base" 5 | "github.com/iter8-tools/iter8/base/log" 6 | "sigs.k8s.io/yaml" 7 | ) 8 | 9 | const ( 10 | // DefaultTestName is the default name of the performance test 11 | DefaultTestName = "default" 12 | ) 13 | 14 | // ExperimentFromBytes reads experiment from bytes 15 | func ExperimentFromBytes(b []byte) (*base.Experiment, error) { 16 | e := base.Experiment{} 17 | err := yaml.Unmarshal(b, &e) 18 | if err != nil { 19 | log.Logger.WithStackTrace(err.Error()).Error("unable to unmarshal experiment: ", string(b)) 20 | return nil, err 21 | } 22 | return &e, err 23 | } 24 | -------------------------------------------------------------------------------- /driver/common_test.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/iter8-tools/iter8/base" 7 | "github.com/stretchr/testify/assert" 8 | "sigs.k8s.io/yaml" 9 | ) 10 | 11 | func TestExperimentFromBytes(t *testing.T) { 12 | experiment := base.Experiment{} 13 | experimentBytes, err := yaml.Marshal(experiment) 14 | assert.NoError(t, err) 15 | assert.NotNil(t, experimentBytes) 16 | 17 | // Experiment from marshalled experiment 18 | experiment2, err := ExperimentFromBytes(experimentBytes) 19 | assert.NoError(t, err) 20 | assert.NotNil(t, experiment2) 21 | 22 | // Experiment from random bytes 23 | experiment3, err := ExperimentFromBytes([]byte{1, 2, 3}) 24 | assert.Error(t, err) 25 | assert.Nil(t, experiment3) 26 | } 27 | -------------------------------------------------------------------------------- /driver/doc.go: -------------------------------------------------------------------------------- 1 | // Package driver enables interaction with experiment resources. 2 | // It provides drivers for local and Kubernetes experiments. 3 | package driver 4 | -------------------------------------------------------------------------------- /driver/kubedriver_test.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "testing" 11 | 12 | "fortio.org/fortio/fhttp" 13 | "github.com/iter8-tools/iter8/base" 14 | "github.com/stretchr/testify/assert" 15 | "helm.sh/helm/v3/pkg/cli" 16 | corev1 "k8s.io/api/core/v1" 17 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 | ) 19 | 20 | const ( 21 | myName = "myName" 22 | myNamespace = "myNamespace" 23 | ) 24 | 25 | func TestKubeRun(t *testing.T) { 26 | // define METRICS_SERVER_URL 27 | metricsServerURL := "http://iter8.default:8080" 28 | err := os.Setenv(base.MetricsServerURL, metricsServerURL) 29 | assert.NoError(t, err) 30 | 31 | // create and configure HTTP endpoint for testing 32 | mux, addr := fhttp.DynamicHTTPServer(false) 33 | url := fmt.Sprintf("http://127.0.0.1:%d/get", addr.Port) 34 | var verifyHandlerCalled bool 35 | mux.HandleFunc("/get", base.GetTrackingHandler(&verifyHandlerCalled)) 36 | 37 | // mock metrics server 38 | base.StartHTTPMock(t) 39 | metricsServerCalled := false 40 | base.MockMetricsServer(base.MockMetricsServerInput{ 41 | MetricsServerURL: metricsServerURL, 42 | ExperimentResultCallback: func(req *http.Request) { 43 | metricsServerCalled = true 44 | 45 | // check query parameters 46 | assert.Equal(t, myName, req.URL.Query().Get("test")) 47 | assert.Equal(t, myNamespace, req.URL.Query().Get("namespace")) 48 | 49 | // check payload 50 | body, err := io.ReadAll(req.Body) 51 | assert.NoError(t, err) 52 | assert.NotNil(t, body) 53 | 54 | // check payload content 55 | bodyExperimentResult := base.ExperimentResult{} 56 | err = json.Unmarshal(body, &bodyExperimentResult) 57 | assert.NoError(t, err) 58 | assert.NotNil(t, body) 59 | 60 | // no experiment failure 61 | assert.False(t, bodyExperimentResult.Failure) 62 | }, 63 | }) 64 | 65 | _ = os.Chdir(t.TempDir()) 66 | 67 | // create experiment.yaml 68 | base.CreateExperimentYaml(t, base.CompletePath("../testdata/drivertests", "experiment.tpl"), url, base.ExperimentFile) 69 | 70 | kd := NewFakeKubeDriver(cli.New()) 71 | kd.revision = 1 72 | 73 | byteArray, _ := os.ReadFile(base.ExperimentFile) 74 | _, _ = kd.Clientset.CoreV1().Secrets("default").Create(context.TODO(), &corev1.Secret{ 75 | ObjectMeta: metav1.ObjectMeta{ 76 | Name: "default", 77 | Namespace: "default", 78 | }, 79 | StringData: map[string]string{base.ExperimentFile: string(byteArray)}, 80 | }, metav1.CreateOptions{}) 81 | 82 | err = base.RunExperiment(kd) 83 | assert.NoError(t, err) 84 | // sanity check -- handler was called 85 | assert.True(t, verifyHandlerCalled) 86 | assert.True(t, metricsServerCalled) 87 | } 88 | -------------------------------------------------------------------------------- /driver/test_helpers.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/iter8-tools/iter8/base/log" 7 | "helm.sh/helm/v3/pkg/action" 8 | "helm.sh/helm/v3/pkg/chartutil" 9 | "helm.sh/helm/v3/pkg/cli" 10 | helmfake "helm.sh/helm/v3/pkg/kube/fake" 11 | "helm.sh/helm/v3/pkg/registry" 12 | "helm.sh/helm/v3/pkg/storage" 13 | helmdriver "helm.sh/helm/v3/pkg/storage/driver" 14 | corev1 "k8s.io/api/core/v1" 15 | "k8s.io/apimachinery/pkg/runtime" 16 | "k8s.io/client-go/kubernetes/fake" 17 | ktesting "k8s.io/client-go/testing" 18 | ) 19 | 20 | // initKubeFake initialize the Kube clientset with a fake 21 | func initKubeFake(kd *KubeDriver, objects ...runtime.Object) { 22 | // secretDataReactor sets the secret.Data field based on the values from secret.StringData 23 | // Credit: this function is adapted from https://github.com/creydr/go-k8s-utils 24 | var secretDataReactor = func(action ktesting.Action) (bool, runtime.Object, error) { 25 | secret, _ := action.(ktesting.CreateAction).GetObject().(*corev1.Secret) 26 | 27 | if secret.Data == nil { 28 | secret.Data = make(map[string][]byte) 29 | } 30 | 31 | for k, v := range secret.StringData { 32 | secret.Data[k] = []byte(v) 33 | } 34 | 35 | return false, nil, nil 36 | } 37 | 38 | fc := fake.NewSimpleClientset(objects...) 39 | fc.PrependReactor("create", "secrets", secretDataReactor) 40 | fc.PrependReactor("update", "secrets", secretDataReactor) 41 | kd.Clientset = fc 42 | } 43 | 44 | // initHelmFake initializes the Helm config with a fake 45 | // Credit: this function is adapted from helm 46 | // https://github.com/helm/helm/blob/e9abdc5efe11cdc23576c20c97011d452201cd92/pkg/action/action_test.go#L37 47 | func initHelmFake(kd *KubeDriver) { 48 | registryClient, err := registry.NewClient() 49 | if err != nil { 50 | log.Logger.Error(err) 51 | return 52 | } 53 | 54 | kd.Configuration = &action.Configuration{ 55 | Releases: storage.Init(helmdriver.NewMemory()), 56 | KubeClient: &helmfake.FailingKubeClient{PrintingKubeClient: helmfake.PrintingKubeClient{Out: io.Discard}}, 57 | Capabilities: chartutil.DefaultCapabilities, 58 | RegistryClient: registryClient, 59 | Log: log.Logger.Debugf, 60 | } 61 | } 62 | 63 | // NewFakeKubeDriver creates and returns a new KubeDriver with fake clients 64 | func NewFakeKubeDriver(s *cli.EnvSettings, objects ...runtime.Object) *KubeDriver { 65 | kd := &KubeDriver{ 66 | EnvSettings: s, 67 | Test: DefaultTestName, 68 | } 69 | initKubeFake(kd, objects...) 70 | initHelmFake(kd) 71 | return kd 72 | } 73 | -------------------------------------------------------------------------------- /kustomize/controller/clusterScoped/kustomization.yaml: -------------------------------------------------------------------------------- 1 | bases: 2 | - ../namespaceScoped 3 | 4 | namespace: default 5 | 6 | patches: 7 | - patch: |- 8 | - op: replace 9 | path: /kind 10 | value: ClusterRole 11 | target: 12 | kind: Role 13 | 14 | # Order matters 15 | # /roleRef/kind patch should happen before /kind patch 16 | - patch: |- 17 | - op: replace 18 | path: /roleRef/kind 19 | value: ClusterRole 20 | target: 21 | kind: RoleBinding 22 | - patch: |- 23 | - op: replace 24 | path: /kind 25 | value: ClusterRoleBinding 26 | target: 27 | kind: RoleBinding 28 | 29 | - patch: |- 30 | - op: replace 31 | path: /data/config.yaml 32 | value: | 33 | clusterScoped: true 34 | defaultResync: 15m 35 | image: iter8/iter8:1.1 36 | logLevel: info 37 | resourceTypes: 38 | cm: 39 | Group: "" 40 | Resource: configmaps 41 | Version: v1 42 | deploy: 43 | Group: apps 44 | Resource: deployments 45 | Version: v1 46 | conditions: 47 | - Available 48 | isvc: 49 | Group: serving.kserve.io 50 | Resource: inferenceservices 51 | Version: v1beta1 52 | conditions: 53 | - Ready 54 | svc: 55 | Group: "" 56 | Resource: services 57 | Version: v1 58 | service: 59 | Group: "" 60 | Resource: services 61 | Version: v1 62 | vs: 63 | Group: networking.istio.io 64 | Resource: virtualservices 65 | Version: v1beta1 66 | resources: 67 | limits: 68 | cpu: 500m 69 | memory: 128Mi 70 | requests: 71 | cpu: 250m 72 | memory: 64Mi 73 | storage: 50Mi 74 | storageClassName: standard 75 | metrics.yaml: | 76 | port: 8080 77 | abn.yaml: | 78 | port: 50051 79 | target: 80 | kind: ConfigMap 81 | name: iter8 -------------------------------------------------------------------------------- /kustomize/controller/namespaceScoped/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: iter8 5 | data: 6 | config.yaml: | 7 | defaultResync: 15m 8 | image: iter8/iter8:1.1 9 | logLevel: info 10 | resourceTypes: 11 | cm: 12 | Group: "" 13 | Resource: configmaps 14 | Version: v1 15 | deploy: 16 | Group: apps 17 | Resource: deployments 18 | Version: v1 19 | conditions: 20 | - Available 21 | isvc: 22 | Group: serving.kserve.io 23 | Resource: inferenceservices 24 | Version: v1beta1 25 | conditions: 26 | - Ready 27 | svc: 28 | Group: "" 29 | Resource: services 30 | Version: v1 31 | service: 32 | Group: "" 33 | Resource: services 34 | Version: v1 35 | vs: 36 | Group: networking.istio.io 37 | Resource: virtualservices 38 | Version: v1beta1 39 | resources: 40 | limits: 41 | cpu: 500m 42 | memory: 128Mi 43 | requests: 44 | cpu: 250m 45 | memory: 64Mi 46 | storage: 50Mi 47 | storageClassName: standard 48 | metrics.yaml: | 49 | port: 8080 50 | abn.yaml: | 51 | port: 50051 -------------------------------------------------------------------------------- /kustomize/controller/namespaceScoped/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - configmap.yaml 3 | - service.yaml 4 | - pvc.yaml 5 | - role.yaml 6 | - rolebinding.yaml 7 | - serviceaccount.yaml 8 | - statefulset.yaml 9 | 10 | commonLabels: 11 | app.kubernetes.io/name: controller 12 | app.kubernetes.io/version: v1.1 -------------------------------------------------------------------------------- /kustomize/controller/namespaceScoped/pvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: iter8 5 | spec: 6 | accessModes: 7 | - ReadWriteOnce 8 | resources: 9 | requests: 10 | storage: 50Mi 11 | storageClassName: standard 12 | -------------------------------------------------------------------------------- /kustomize/controller/namespaceScoped/role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: Role 3 | metadata: 4 | name: iter8 5 | rules: 6 | - apiGroups: [""] 7 | resources: ["configmaps"] 8 | verbs: ["get", "list", "watch", "patch", "update"] 9 | - apiGroups: ["apps"] 10 | resources: ["deployments"] 11 | verbs: ["get", "list", "watch", "patch", "update"] 12 | - apiGroups: ["serving.kserve.io"] 13 | resources: ["inferenceservices"] 14 | verbs: ["get", "list", "watch", "patch", "update"] 15 | - apiGroups: [""] 16 | resources: ["services"] 17 | verbs: ["get", "list", "watch", "patch", "update"] 18 | - apiGroups: ["networking.istio.io"] 19 | resources: ["virtualservices"] 20 | verbs: ["get", "list", "watch", "patch", "update"] 21 | - apiGroups: [""] 22 | resources: ["events"] 23 | verbs: ["get", "create"] 24 | - apiGroups: [""] 25 | resources: ["pods"] 26 | verbs: ["get"] -------------------------------------------------------------------------------- /kustomize/controller/namespaceScoped/rolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: iter8 5 | subjects: 6 | - kind: ServiceAccount 7 | name: iter8 8 | roleRef: 9 | kind: Role 10 | name: iter8 11 | apiGroup: rbac.authorization.k8s.io -------------------------------------------------------------------------------- /kustomize/controller/namespaceScoped/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: iter8 5 | spec: 6 | clusterIP: None 7 | selector: 8 | app.kubernetes.io/name: controller 9 | ports: 10 | - name: grpc 11 | port: 50051 12 | - name: http 13 | port: 8080 14 | targetPort: 8080 -------------------------------------------------------------------------------- /kustomize/controller/namespaceScoped/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: iter8 -------------------------------------------------------------------------------- /kustomize/controller/namespaceScoped/statefulset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: iter8 5 | spec: 6 | serviceName: iter8 7 | selector: 8 | matchLabels: 9 | app.kubernetes.io/name: controller 10 | template: 11 | metadata: 12 | labels: 13 | app.kubernetes.io/name: controller 14 | spec: 15 | terminationGracePeriodSeconds: 10 16 | serviceAccountName: iter8 17 | containers: 18 | - name: iter8-controller 19 | image: iter8/iter8:1.1 20 | imagePullPolicy: Always 21 | command: ["/bin/iter8"] 22 | args: ["controllers", "-l", "info"] 23 | env: 24 | - name: CONFIG_FILE 25 | value: /config/config.yaml 26 | - name: METRICS_CONFIG_FILE 27 | value: /config/metrics.yaml 28 | - name: ABN_CONFIG_FILE 29 | value: /config/abn.yaml 30 | - name: METRICS_DIR 31 | value: /metrics 32 | - name: POD_NAME 33 | valueFrom: 34 | fieldRef: 35 | fieldPath: metadata.name 36 | - name: POD_NAMESPACE 37 | valueFrom: 38 | fieldRef: 39 | fieldPath: metadata.namespace 40 | volumeMounts: 41 | - name: config 42 | mountPath: "/config" 43 | readOnly: true 44 | - name: metrics 45 | mountPath: "/metrics" 46 | resources: 47 | limits: 48 | cpu: 500m 49 | memory: 128Mi 50 | requests: 51 | cpu: 250m 52 | memory: 64Mi 53 | securityContext: 54 | readOnlyRootFilesystem: true 55 | allowPrivilegeEscalation: false 56 | capabilities: 57 | drop: 58 | - ALL 59 | runAsNonRoot: true 60 | runAsUser: 1001040000 61 | volumes: 62 | - name: config 63 | configMap: 64 | name: iter8 65 | - name: metrics 66 | persistentVolumeClaim: 67 | claimName: iter8 68 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/iter8-tools/iter8/cmd" 5 | _ "k8s.io/client-go/plugin/pkg/client/auth" 6 | ) 7 | 8 | func main() { 9 | cmd.Execute() 10 | } 11 | -------------------------------------------------------------------------------- /metrics/doc.go: -------------------------------------------------------------------------------- 1 | // Package metrics implements an HTTP service that exposes A/B/n SDK metrics 2 | package metrics 3 | -------------------------------------------------------------------------------- /metrics/test_helpers.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import "github.com/iter8-tools/iter8/controllers" 4 | 5 | type testroutemapsByName map[string]*testroutemap 6 | type testroutemaps struct { 7 | nsRoutemap map[string]testroutemapsByName 8 | } 9 | 10 | func (s *testroutemaps) GetRoutemapFromNamespaceName(namespace string, name string) controllers.RoutemapInterface { 11 | rmByName, ok := s.nsRoutemap[namespace] 12 | if ok { 13 | return rmByName[name] 14 | } 15 | return nil 16 | } 17 | 18 | type testversion struct { 19 | signature *string 20 | } 21 | 22 | func (v *testversion) GetSignature() *string { 23 | return v.signature 24 | } 25 | 26 | type testroutemap struct { 27 | name string 28 | namespace string 29 | versions []testversion 30 | normalizedWeights []uint32 31 | } 32 | 33 | func (s *testroutemap) RLock() {} 34 | 35 | func (s *testroutemap) RUnlock() {} 36 | 37 | func (s *testroutemap) GetNamespace() string { 38 | return s.namespace 39 | } 40 | 41 | func (s *testroutemap) GetName() string { 42 | return s.name 43 | } 44 | 45 | func (s *testroutemap) Weights() []uint32 { 46 | return s.normalizedWeights 47 | } 48 | 49 | func (s *testroutemap) GetVersions() []controllers.VersionInterface { 50 | result := make([]controllers.VersionInterface, len(s.versions)) 51 | for i := range s.versions { 52 | v := s.versions[i] 53 | result[i] = controllers.VersionInterface(&v) 54 | } 55 | return result 56 | } 57 | -------------------------------------------------------------------------------- /storage/client/client.go: -------------------------------------------------------------------------------- 1 | // Package client implements an implementation independent storage client 2 | package client 3 | 4 | import ( 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/dgraph-io/badger/v4" 9 | util "github.com/iter8-tools/iter8/base" 10 | "github.com/iter8-tools/iter8/storage" 11 | "github.com/iter8-tools/iter8/storage/badgerdb" 12 | "github.com/iter8-tools/iter8/storage/redis" 13 | ) 14 | 15 | const ( 16 | metricsConfigFileEnv = "METRICS_CONFIG_FILE" 17 | defaultImplementation = "badgerdb" 18 | ) 19 | 20 | var ( 21 | // MetricsClient is storage client 22 | MetricsClient storage.Interface 23 | ) 24 | 25 | // metricsStorageConfig is configuration of metrics service 26 | type metricsStorageConfig struct { 27 | // Implementation method for metrics service 28 | Implementation *string `json:"implementation,omitempty"` 29 | } 30 | 31 | // GetClient creates a metric service client based on configuration 32 | func GetClient() (storage.Interface, error) { 33 | conf := &metricsStorageConfig{} 34 | err := util.ReadConfig(metricsConfigFileEnv, conf, func() { 35 | if conf.Implementation == nil { 36 | conf.Implementation = util.StringPointer(defaultImplementation) 37 | } 38 | }) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | switch strings.ToLower(*conf.Implementation) { 44 | case "badgerdb": 45 | // badgerConfig defines the configuration of a badgerDB based metrics service 46 | type mConfig struct { 47 | badgerdb.ClientConfig `json:"badgerdb,omitempty"` 48 | } 49 | 50 | conf := &mConfig{} 51 | err := util.ReadConfig(metricsConfigFileEnv, conf, func() { 52 | if conf.ClientConfig.Storage == nil { 53 | conf.ClientConfig.Storage = util.StringPointer("50Mi") 54 | } 55 | if conf.ClientConfig.StorageClassName == nil { 56 | conf.ClientConfig.StorageClassName = util.StringPointer("standard") 57 | } 58 | if conf.ClientConfig.Dir == nil { 59 | conf.ClientConfig.Dir = util.StringPointer("/metrics") 60 | } 61 | }) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | cl, err := badgerdb.GetClient(badger.DefaultOptions(*conf.ClientConfig.Dir), badgerdb.AdditionalOptions{}) 67 | if err != nil { 68 | return nil, err 69 | } 70 | return cl, nil 71 | 72 | case "redis": 73 | // redisConfig defines the configuration of a redis based metrics service 74 | type mConfig struct { 75 | redis.ClientConfig `json:"redis,omitempty"` 76 | } 77 | 78 | conf := &mConfig{} 79 | err := util.ReadConfig(metricsConfigFileEnv, conf, func() { 80 | if conf.ClientConfig.Address == nil { 81 | conf.ClientConfig.Address = util.StringPointer("redis:6379") 82 | } 83 | }) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | cl, err := redis.GetClient(conf.ClientConfig) 89 | if err != nil { 90 | return nil, err 91 | } 92 | return cl, nil 93 | 94 | default: 95 | return nil, fmt.Errorf("no metrics store implementation for %s", *conf.Implementation) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /storage/client/client_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/alicebob/miniredis" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestGetClientRedis(t *testing.T) { 12 | 13 | server, _ := miniredis.Run() 14 | assert.NotNil(t, server) 15 | 16 | metricsConfig := `port: 8080 17 | implementation: redis 18 | redis: 19 | address: ` + server.Addr() 20 | 21 | mf, err := os.CreateTemp("", "metrics*.yaml") 22 | assert.NoError(t, err) 23 | 24 | err = os.Setenv(metricsConfigFileEnv, mf.Name()) 25 | assert.NoError(t, err) 26 | 27 | _, err = mf.WriteString(metricsConfig) 28 | assert.NoError(t, err) 29 | 30 | client, err := GetClient() 31 | assert.NoError(t, err) 32 | assert.NotNil(t, client) 33 | } 34 | 35 | func TestGetClientBadger(t *testing.T) { 36 | 37 | tempDirPath := os.TempDir() 38 | 39 | metricsConfig := `port: 8080 40 | implementation: badgerdb 41 | badgerdb: 42 | dir: ` + tempDirPath 43 | 44 | mf, err := os.CreateTemp("", "metrics*.yaml") 45 | assert.NoError(t, err) 46 | 47 | err = os.Setenv(metricsConfigFileEnv, mf.Name()) 48 | assert.NoError(t, err) 49 | 50 | _, err = mf.WriteString(metricsConfig) 51 | assert.NoError(t, err) 52 | 53 | client, err := GetClient() 54 | assert.NoError(t, err) 55 | assert.NotNil(t, client) 56 | } 57 | -------------------------------------------------------------------------------- /storage/interface.go: -------------------------------------------------------------------------------- 1 | // Package storage provides the storage client for the controllers package 2 | package storage 3 | 4 | import "github.com/iter8-tools/iter8/base" 5 | 6 | // SummarizedMetric is a metric summary 7 | type SummarizedMetric struct { 8 | Count uint64 9 | Mean float64 10 | StdDev float64 11 | Min float64 12 | Max float64 13 | } 14 | 15 | // MetricSummary contains metric summary for all metrics as well as cumulative metrics per user 16 | type MetricSummary struct { 17 | // all transactions 18 | SummaryOverTransactions SummarizedMetric 19 | 20 | // cumulative metrics per user 21 | SummaryOverUsers SummarizedMetric 22 | } 23 | 24 | // VersionMetricSummary is a metric summary for a given app version 25 | type VersionMetricSummary struct { 26 | NumUsers uint64 27 | 28 | // key = metric name; value is the metric summary 29 | MetricSummaries map[string]MetricSummary 30 | } 31 | 32 | // VersionMetrics contains all the metrics over transactions and over users 33 | // key = metric name 34 | type VersionMetrics map[string]struct { 35 | MetricsOverTransactions []float64 36 | MetricsOverUsers []float64 37 | } 38 | 39 | // Interface enables interaction with a storage entity 40 | // Can be mocked in unit tests with fake implementation 41 | type Interface interface { 42 | // GetMerics returns all metrics for an app/version 43 | // Returned result is a nested map of the metrics data 44 | // Example: 45 | // { 46 | // "my-metric": { 47 | // "MetricsOverTransactions": [1, 1, 3, 4, 5] 48 | // "MetricsOverUsers": [2, 7, 5] 49 | // } 50 | // } 51 | // 52 | // NOTE: for users that have not produced any metrics (for example, via lookup()), GetMetrics() will add 0s for the extra users in metricsOverUsers 53 | // Example, given 5 total users: 54 | // 55 | // { 56 | // "my-metric": { 57 | // "MetricsOverTransactions": [1, 1, 3, 4, 5] 58 | // "MetricsOverUsers": [2, 7, 5, 0, 0] 59 | // } 60 | // } 61 | GetMetrics(applicationName string, version int, signature string) (*VersionMetrics, error) 62 | 63 | // SetMetric records a metric value 64 | // Called by the A/B/n SDK gRPC API implementation (SDK for application clients) 65 | // Example key: kt-metric::my-app::0::my-signature::my-metric::my-user::my-transaction-id -> my-metric-value (get the metric value with all the provided information) 66 | SetMetric(applicationName string, version int, signature, metric, user, transaction string, metricValue float64) error 67 | 68 | // SetUser records the name of user 69 | // Example key: kt-users::my-app::0::my-signature::my-user -> true 70 | SetUser(applicationName string, version int, signature, user string) error 71 | 72 | // GetExperimentResult returns the experiment result for a particular namespace and experiment 73 | GetExperimentResult(namespace, experiment string) (*base.ExperimentResult, error) 74 | 75 | // SetExperimentResult records an expeirment result 76 | // called by the A/B/n SDK gRPC API implementation (SDK for application clients) 77 | // Example key: kt-metric::my-app::0::my-signature::my-metric::my-user::my-transaction-id -> my-metric-value (get the metric value with all the provided information) 78 | SetExperimentResult(namespace, experiment string, data *base.ExperimentResult) error 79 | } 80 | -------------------------------------------------------------------------------- /storage/util.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/iter8-tools/iter8/base" 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | // GetVolumeUsage gets the available and total capacity of a volume, in that order 14 | func GetVolumeUsage(path string) (uint64, uint64, error) { 15 | var stat unix.Statfs_t 16 | err := unix.Statfs(path, &stat) 17 | if err != nil { 18 | return 0, 0, err 19 | } 20 | 21 | // Available blocks * size per block = available space in bytes 22 | availableBytes := stat.Bavail * uint64(stat.Bsize) 23 | // Total blocks * size per block = available space in bytes 24 | totalBytes := stat.Blocks * uint64(stat.Bsize) 25 | 26 | return availableBytes, totalBytes, nil 27 | } 28 | 29 | func validateKeyToken(s string) error { 30 | if strings.Contains(s, ":") { 31 | return errors.New("key token contains \":\"") 32 | } 33 | 34 | return nil 35 | } 36 | 37 | // GetMetricKeyPrefix returns the prefix of a metric key 38 | func GetMetricKeyPrefix(applicationName string, version int, signature string) string { 39 | return fmt.Sprintf("kt-metric::%s::%d::%s::", applicationName, version, signature) 40 | } 41 | 42 | // GetMetricKey returns a metric key from the inputs 43 | func GetMetricKey(applicationName string, version int, signature, metric, user, transaction string) (string, error) { 44 | if err := validateKeyToken(applicationName); err != nil { 45 | return "", errors.New("application name cannot have \":\"") 46 | } 47 | if err := validateKeyToken(signature); err != nil { 48 | return "", errors.New("signature cannot have \":\"") 49 | } 50 | if err := validateKeyToken(metric); err != nil { 51 | return "", errors.New("metric name cannot have \":\"") 52 | } 53 | if err := validateKeyToken(user); err != nil { 54 | return "", errors.New("user name cannot have \":\"") 55 | } 56 | if err := validateKeyToken(transaction); err != nil { 57 | return "", errors.New("transaction ID cannot have \":\"") 58 | } 59 | 60 | return fmt.Sprintf("%s%s::%s::%s", GetMetricKeyPrefix(applicationName, version, signature), metric, user, transaction), nil 61 | } 62 | 63 | // GetUserKeyPrefix returns the prefix of a user key 64 | func GetUserKeyPrefix(applicationName string, version int, signature string) string { 65 | prefix := fmt.Sprintf("kt-users::%s::%d::%s::", applicationName, version, signature) 66 | return prefix 67 | } 68 | 69 | // GetUserKey returns a user key from the inputs 70 | func GetUserKey(applicationName string, version int, signature, user string) string { 71 | key := fmt.Sprintf("%s%s", GetUserKeyPrefix(applicationName, version, signature), user) 72 | return key 73 | } 74 | 75 | // GetExperimentResultKey returns a performance experiment key from the inputs 76 | func GetExperimentResultKey(namespace, experiment string) string { 77 | // getExperimentResultKey() is just getUserPrefix() with the user appended at the end 78 | return fmt.Sprintf("kt-result::%s::%s", namespace, experiment) 79 | } 80 | 81 | // GetExperimentResult returns an experiment result retrieved from a key value store 82 | func GetExperimentResult(fetch func() ([]byte, error)) (*base.ExperimentResult, error) { 83 | value, err := fetch() 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | experimentResult := base.ExperimentResult{} 89 | err = json.Unmarshal(value, &experimentResult) 90 | if err != nil { 91 | return nil, fmt.Errorf("cannot unmarshal ExperimentResult: \"%s\": %e", string(value), err) 92 | } 93 | 94 | return &experimentResult, err 95 | } 96 | -------------------------------------------------------------------------------- /templates/notify/_payload-github.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "event_type": "iter8", 3 | "client_payload": {{ .Summary | toPrettyJson }} 4 | } -------------------------------------------------------------------------------- /templates/notify/_payload-slack.tpl: -------------------------------------------------------------------------------- 1 | { 2 | "text": "Your Iter8 report is ready: {{ regexReplaceAll "\"" (regexReplaceAll "\n" (.Summary | toPrettyJson) "\\n") "\\\""}}" 3 | } -------------------------------------------------------------------------------- /testdata/.gitignore: -------------------------------------------------------------------------------- 1 | !./experiment.yaml 2 | !iter8.tpl -------------------------------------------------------------------------------- /testdata/abninputs/application.yaml: -------------------------------------------------------------------------------- 1 | name: default/backend 2 | tracks: 3 | candidate: v2 4 | default: v1 5 | versions: 6 | v1: 7 | metrics: 8 | sample_metric: 9 | - 223 10 | - 10309 11 | - 0 12 | - 100 13 | - 652049 14 | v2: 15 | metrics: 16 | sample_metric: 17 | - 252 18 | - 12228 19 | - 0 20 | - 100 21 | - 782208 22 | -------------------------------------------------------------------------------- /testdata/abninputs/config.yaml: -------------------------------------------------------------------------------- 1 | abn: 2 | port: 50051 3 | -------------------------------------------------------------------------------- /testdata/assertinputs/.gitignore: -------------------------------------------------------------------------------- 1 | !experiment.yaml 2 | !result.yaml -------------------------------------------------------------------------------- /testdata/assertinputs/experiment.yaml: -------------------------------------------------------------------------------- 1 | metadata: 2 | name: myName 3 | namespace: myNamespace 4 | spec: 5 | # task 1: generate HTTP requests for application URL 6 | # collect Iter8's built-in HTTP latency and error-related metrics 7 | - task: http 8 | with: 9 | duration: 2s 10 | errorRanges: 11 | - lower: 500 12 | url: https://httpbin.org/get 13 | result: 14 | failure: false 15 | insights: 16 | numVersions: 1 17 | iter8Version: v0.13 18 | numCompletedTasks: 1 19 | startTime: "2022-03-16T10:22:58.540897-04:00" -------------------------------------------------------------------------------- /testdata/controllers/config.yaml: -------------------------------------------------------------------------------- 1 | defaultResync: 15m 2 | # by default, Iter8 controller is namespace scoped 3 | # clusterScoped: true 4 | resourceTypes: 5 | svc: 6 | Group: "" 7 | Version: v1 8 | Resource: services 9 | cm: 10 | Group: "" 11 | Version: v1 12 | Resource: configmaps 13 | deploy: 14 | Group: apps 15 | Version: v1 16 | Resource: deployments 17 | isvc: 18 | Group: serving.kserve.io 19 | Version: v1beta1 20 | Resource: inferenceservices 21 | conditions: 22 | - Ready 23 | vs: 24 | Group: networking.istio.io 25 | Version: v1beta1 26 | Resource: virtualservices 27 | -------------------------------------------------------------------------------- /testdata/controllers/garb.age: -------------------------------------------------------------------------------- 1 | this is not a 2 | . real yaml . 3 | if you try : this or 4 | some thing equally bad 5 | ( yaml parsing breaks) 6 | { real bad } 7 | [ in fact ] -------------------------------------------------------------------------------- /testdata/drivertests/.gitignore: -------------------------------------------------------------------------------- 1 | !experiment.yaml 2 | -------------------------------------------------------------------------------- /testdata/drivertests/experiment.tpl: -------------------------------------------------------------------------------- 1 | metadata: 2 | name: myName 3 | namespace: myNamespace 4 | spec: 5 | # task 1: generate HTTP requests for application URL 6 | # collect Iter8's built-in HTTP latency and error-related metrics 7 | - task: http 8 | with: 9 | duration: 2s 10 | errorRanges: 11 | - lower: 500 12 | url: {{ .URL }} 13 | -------------------------------------------------------------------------------- /testdata/experiment.tpl: -------------------------------------------------------------------------------- 1 | metadata: 2 | name: myName 3 | namespace: myNamespace 4 | spec: 5 | # task 1: generate HTTP requests for application URL 6 | # collect Iter8's built-in HTTP latency and error-related metrics 7 | - task: http 8 | with: 9 | duration: 2s 10 | errorRanges: 11 | - lower: 500 12 | url: {{ .URL }} -------------------------------------------------------------------------------- /testdata/experiment.yaml: -------------------------------------------------------------------------------- 1 | spec: 2 | # task 1: generate HTTP requests for application URL 3 | # collect Iter8's built-in HTTP latency and error-related metrics 4 | - task: http 5 | with: 6 | duration: 2s 7 | errorRanges: 8 | - lower: 500 9 | url: https://httpbin.org/get 10 | -------------------------------------------------------------------------------- /testdata/experiment_grpc.yaml: -------------------------------------------------------------------------------- 1 | spec: 2 | # task 1: generate gRPC requests for application 3 | # collect Iter8's built-in gRPC latency and error-related metrics 4 | - task: grpc 5 | with: 6 | total: 200 7 | concurrency: 5 8 | data: 9 | name: bob 10 | timeout: 10s 11 | connect-timeeout: 5s 12 | protoURL: "https://raw.githubusercontent.com/bojand/ghz/v0.105.0/testdata/greeter.proto" 13 | call: "helloworld.Greeter.SayHello" 14 | host: "127.0.0.1" -------------------------------------------------------------------------------- /testdata/output/.gitignore: -------------------------------------------------------------------------------- 1 | !report.html -------------------------------------------------------------------------------- /testdata/output/gen-cli-values.txt: -------------------------------------------------------------------------------- 1 | time=1977-09-02 22:04:05 level=info msg=created experiment.yaml file 2 | -------------------------------------------------------------------------------- /testdata/output/gen-values-file.txt: -------------------------------------------------------------------------------- 1 | time=1977-09-02 22:04:05 level=info msg=created experiment.yaml file 2 | -------------------------------------------------------------------------------- /testdata/output/hub-with-destdir.txt: -------------------------------------------------------------------------------- 1 | time=1977-09-02 22:04:05 level=info msg=pulling load-test-http 2 | -------------------------------------------------------------------------------- /testdata/output/hub.txt: -------------------------------------------------------------------------------- 1 | time=1977-09-02 22:04:05 level=info msg=downloading github.com/iter8-tools/iter8.git//charts into charts 2 | -------------------------------------------------------------------------------- /testdata/output/kassert.txt: -------------------------------------------------------------------------------- 1 | time=1977-09-02 22:04:05 level=info msg=experiment completed 2 | time=1977-09-02 22:04:05 level=info msg=experiment has no failure 3 | time=1977-09-02 22:04:05 level=info msg=all conditions were satisfied 4 | -------------------------------------------------------------------------------- /testdata/output/kdelete.txt: -------------------------------------------------------------------------------- 1 | time=1977-09-02 22:04:05 level=info msg=experiment group default deleted 2 | -------------------------------------------------------------------------------- /testdata/output/klaunch.txt: -------------------------------------------------------------------------------- 1 | time=1977-09-02 22:04:05 level=info msg=experiment launched. Happy Iter8ing! 2 | -------------------------------------------------------------------------------- /testdata/output/klog.txt: -------------------------------------------------------------------------------- 1 | time=1977-09-02 22:04:05 level=info msg=experiment logs from Kubernetes cluster indented-trace=below ... 2 | fake logs 3 | -------------------------------------------------------------------------------- /testdata/output/krun.txt: -------------------------------------------------------------------------------- 1 | time=1977-09-02 22:04:05 level=info msg=task 1: http: started 2 | time=1977-09-02 22:04:05 level=info msg=task 1: http: completed 3 | -------------------------------------------------------------------------------- /testdata/output/launch-with-destdir.txt: -------------------------------------------------------------------------------- 1 | time=1977-09-02 22:04:05 level=info msg=pulling load-test-http 2 | time=1977-09-02 22:04:05 level=info msg=created experiment.yaml file 3 | time=1977-09-02 22:04:05 level=info msg=starting local experiment 4 | time=1977-09-02 22:04:05 level=info msg=task 1: gen-load-and-collect-metrics-http: started 5 | time=1977-09-02 22:04:05 level=info msg=task 1: gen-load-and-collect-metrics-http: completed 6 | -------------------------------------------------------------------------------- /testdata/output/launch.txt: -------------------------------------------------------------------------------- 1 | time=1977-09-02 22:04:05 level=info msg=created experiment.yaml file 2 | time=1977-09-02 22:04:05 level=info msg=starting local experiment 3 | time=1977-09-02 22:04:05 level=info msg=task 1: http: started 4 | time=1977-09-02 22:04:05 level=info msg=task 1: http: completed 5 | --------------------------------------------------------------------------------