├── docs ├── smi-flagger │ ├── deploy │ ├── manifests │ │ ├── 01-deploy-v1.yaml │ │ ├── 02-canary.yaml │ │ ├── 00-bookinfo-setup.yaml │ │ └── 00-install-flagger.yaml │ └── README.md ├── smi-trafficsplit │ ├── deploy │ ├── manifests │ │ ├── 02-traffic-split-1000-0.yaml │ │ ├── 04-traffic-split-900-100.yaml │ │ ├── 05-traffic-split-750-250.yaml │ │ ├── 06-traffic-split-200-800.yaml │ │ ├── 07-traffic-split-0-1000.yaml │ │ ├── 03-deploy-v2.yaml │ │ ├── 01-deploy-v1.yaml │ │ └── 00-bookinfo-setup.yaml │ ├── overview.md │ └── README.md ├── README.md └── smi-traffictarget │ ├── manifests │ ├── traffictarget-reviews-v2.yaml │ ├── traffictarget.yaml │ └── configs.yml │ └── README.md ├── version └── version.go ├── deploy ├── kustomization.yaml ├── crds │ └── crds.yaml └── operator-and-rbac.yaml ├── test ├── README.md ├── e2e │ ├── main_test.go │ ├── README.md │ ├── trafficsplit_test.go │ └── traffictarget_test.go └── manual │ ├── 02-traffic-split-1000-0.yaml │ ├── 05-traffic-split-0-1000.yaml │ ├── 04-traffic-split-1000-500.yaml │ ├── 04-traffic-split-1000-500-output.yaml │ ├── 02-traffic-split-1000-0-output.yaml │ ├── 05-traffic-split-0-1000-output.yaml │ ├── 03-deploy-v2.yaml │ ├── 01-deploy-v1.yaml │ ├── README.md │ └── 00-bookinfo-setup.yaml ├── pkg ├── apis │ ├── rbac │ │ └── v1alpha1 │ │ │ ├── doc.go │ │ │ ├── zz_generated.defaults.go │ │ │ ├── register.go │ │ │ ├── servicerole_types.go │ │ │ ├── servicerolebinding_types.go │ │ │ ├── rbac.pb.go │ │ │ └── zz_generated.deepcopy.go │ ├── networking │ │ └── v1alpha3 │ │ │ ├── doc.go │ │ │ ├── zz_generated.defaults.go │ │ │ ├── register.go │ │ │ ├── virtualservice_types.go │ │ │ ├── zz_generated.openapi.go │ │ │ └── zz_generated.deepcopy.go │ ├── addtoscheme_split_v1alpha1.go │ ├── addtoscheme_rbac_v1alpha1.go │ ├── addtoscheme_networking_v1alpha3.go │ ├── apis.go │ └── addtoscheme_traffictarget_v1alpha1.go └── controller │ ├── add_trafficsplit.go │ ├── add_traffictarget.go │ ├── controller.go │ ├── traffictarget │ ├── traffictarget_controller_test.go │ └── traffictarget_controller.go │ └── trafficsplit │ ├── trafficsplit_controller_test.go │ └── trafficsplit_controller.go ├── CODEOWNERS ├── Tiltfile ├── .github └── workflows │ └── fossa.yml ├── .circleci └── config.yml ├── GOVERNANCE.md ├── .gitignore ├── Gopkg.toml ├── README.md ├── Makefile ├── cmd └── manager │ └── main.go ├── CONTRIBUTING.md └── LICENSE /docs/smi-flagger/deploy: -------------------------------------------------------------------------------- 1 | ../../deploy -------------------------------------------------------------------------------- /docs/smi-trafficsplit/deploy: -------------------------------------------------------------------------------- 1 | ../../deploy -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var ( 4 | Version = "0.0.1" 5 | ) 6 | -------------------------------------------------------------------------------- /deploy/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - operator-and-rbac.yaml 3 | - ./crds/crds.yaml -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Test 2 | 3 | ## Table of Contents 4 | - [Manual testing](manual/) 5 | - [e2e Testing](e2e/) 6 | -------------------------------------------------------------------------------- /pkg/apis/rbac/v1alpha1/doc.go: -------------------------------------------------------------------------------- 1 | // Package v1alpha1 contains API Schema definitions for the rbac v1alpha1 API group 2 | // +k8s:deepcopy-gen=package,register 3 | // +groupName=rbac.istio.io 4 | package v1alpha1 5 | -------------------------------------------------------------------------------- /test/e2e/main_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "testing" 5 | 6 | f "github.com/operator-framework/operator-sdk/pkg/test" 7 | ) 8 | 9 | func TestMain(m *testing.M) { 10 | f.MainEntry(m) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/apis/networking/v1alpha3/doc.go: -------------------------------------------------------------------------------- 1 | // Package v1alpha3 contains API Schema definitions for the networking v1alpha3 API group 2 | // +k8s:deepcopy-gen=package,register 3 | // +groupName=networking.istio.io 4 | package v1alpha3 5 | -------------------------------------------------------------------------------- /test/manual/02-traffic-split-1000-0.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: split.smi-spec.io/v1alpha2 2 | kind: TrafficSplit 3 | metadata: 4 | name: reviews-rollout 5 | spec: 6 | service: reviews 7 | backends: 8 | - service: reviews-v1 9 | weight: 1 10 | - service: reviews-v2 11 | weight: 0 12 | -------------------------------------------------------------------------------- /test/manual/05-traffic-split-0-1000.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: split.smi-spec.io/v1alpha2 2 | kind: TrafficSplit 3 | metadata: 4 | name: reviews-rollout 5 | spec: 6 | service: reviews 7 | backends: 8 | - service: reviews-v1 9 | weight: 0 10 | - service: reviews-v2 11 | weight: 1 12 | -------------------------------------------------------------------------------- /test/manual/04-traffic-split-1000-500.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: split.smi-spec.io/v1alpha2 2 | kind: TrafficSplit 3 | metadata: 4 | name: reviews-rollout 5 | spec: 6 | service: reviews 7 | backends: 8 | - service: reviews-v1 9 | weight: 1 10 | - service: reviews-v2 11 | weight: 500 12 | -------------------------------------------------------------------------------- /docs/smi-trafficsplit/manifests/02-traffic-split-1000-0.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: split.smi-spec.io/v1alpha2 2 | kind: TrafficSplit 3 | metadata: 4 | name: reviews-rollout 5 | spec: 6 | service: reviews 7 | backends: 8 | - service: reviews-v1 9 | weight: 5000 10 | - service: reviews-v2 11 | weight: 1 12 | -------------------------------------------------------------------------------- /docs/smi-trafficsplit/manifests/04-traffic-split-900-100.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: split.smi-spec.io/v1alpha2 2 | kind: TrafficSplit 3 | metadata: 4 | name: reviews-rollout 5 | spec: 6 | service: reviews 7 | backends: 8 | - service: reviews-v1 9 | weight: 900 10 | - service: reviews-v2 11 | weight: 100 12 | -------------------------------------------------------------------------------- /docs/smi-trafficsplit/manifests/05-traffic-split-750-250.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: split.smi-spec.io/v1alpha2 2 | kind: TrafficSplit 3 | metadata: 4 | name: reviews-rollout 5 | spec: 6 | service: reviews 7 | backends: 8 | - service: reviews-v1 9 | weight: 750 10 | - service: reviews-v2 11 | weight: 250 12 | -------------------------------------------------------------------------------- /docs/smi-trafficsplit/manifests/06-traffic-split-200-800.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: split.smi-spec.io/v1alpha2 2 | kind: TrafficSplit 3 | metadata: 4 | name: reviews-rollout 5 | spec: 6 | service: reviews 7 | backends: 8 | - service: reviews-v1 9 | weight: 200 10 | - service: reviews-v2 11 | weight: 800 12 | -------------------------------------------------------------------------------- /docs/smi-trafficsplit/manifests/07-traffic-split-0-1000.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: split.smi-spec.io/v1alpha2 2 | kind: TrafficSplit 3 | metadata: 4 | name: reviews-rollout 5 | spec: 6 | service: reviews 7 | backends: 8 | - service: reviews-v1 9 | weight: 0 10 | - service: reviews-v2 11 | weight: 1000 12 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Maintainers: @slack @lachie83 @michelleN @rickducott 2 | CODEOWNERS @slack @lachie83 @michelleN @rickducott 3 | CONTRIBUTING.md @slack @lachie83 @michelleN @rickducott 4 | LICENSE @slack @lachie83 @michelleN @rickducott 5 | GOVERNANCE.md @slack @lachie83 @michelleN @rickducott 6 | # EMERITUS Maintainers: @alban @surajssd @stefanprodan 7 | -------------------------------------------------------------------------------- /pkg/controller/add_trafficsplit.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/deislabs/smi-adapter-istio/pkg/controller/trafficsplit" 5 | ) 6 | 7 | func init() { 8 | // AddToManagerFuncs is a list of functions to create controllers and add them to a manager. 9 | AddToManagerFuncs = append(AddToManagerFuncs, trafficsplit.Add) 10 | } 11 | -------------------------------------------------------------------------------- /pkg/apis/addtoscheme_split_v1alpha1.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "github.com/deislabs/smi-sdk-go/pkg/apis/split/v1alpha2" 5 | ) 6 | 7 | func init() { 8 | // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back 9 | AddToSchemes = append(AddToSchemes, v1alpha2.SchemeBuilder.AddToScheme) 10 | } 11 | -------------------------------------------------------------------------------- /pkg/controller/add_traffictarget.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/deislabs/smi-adapter-istio/pkg/controller/traffictarget" 5 | ) 6 | 7 | func init() { 8 | // AddToManagerFuncs is a list of functions to create controllers and add them to a manager. 9 | AddToManagerFuncs = append(AddToManagerFuncs, traffictarget.Add) 10 | } 11 | -------------------------------------------------------------------------------- /pkg/apis/addtoscheme_rbac_v1alpha1.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "github.com/deislabs/smi-adapter-istio/pkg/apis/rbac/v1alpha1" 5 | ) 6 | 7 | func init() { 8 | // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back 9 | AddToSchemes = append(AddToSchemes, v1alpha1.SchemeBuilder.AddToScheme) 10 | } 11 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | The following documentation is available: 4 | 5 | 1. Overviews 6 | 1. [Traffic Split Overview](smi-trafficsplit/overview.md) 7 | 2. Demos 8 | 1. [Traffic Split Demo](smi-trafficsplit/README.md) 9 | 2. [Traffic Target Demo](smi-traffictarget/README.md) 10 | 3. [Flagger + SMI on Istio Demo](smi-flagger/README.md) 11 | -------------------------------------------------------------------------------- /pkg/apis/addtoscheme_networking_v1alpha3.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "github.com/deislabs/smi-adapter-istio/pkg/apis/networking/v1alpha3" 5 | ) 6 | 7 | func init() { 8 | // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back 9 | AddToSchemes = append(AddToSchemes, v1alpha3.SchemeBuilder.AddToScheme) 10 | } 11 | -------------------------------------------------------------------------------- /pkg/apis/apis.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/runtime" 5 | ) 6 | 7 | // AddToSchemes may be used to add all resources defined in the project to a Scheme 8 | var AddToSchemes runtime.SchemeBuilder 9 | 10 | // AddToScheme adds all Resources to the Scheme 11 | func AddToScheme(s *runtime.Scheme) error { 12 | return AddToSchemes.AddToScheme(s) 13 | } 14 | -------------------------------------------------------------------------------- /test/manual/04-traffic-split-1000-500-output.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.istio.io/v1alpha3 2 | kind: VirtualService 3 | metadata: 4 | name: reviews-rollout 5 | spec: 6 | hosts: 7 | - reviews 8 | http: 9 | - route: 10 | - destination: 11 | host: reviews-v1 12 | weight: 67 13 | - destination: 14 | host: reviews-v2 15 | weight: 33 16 | 17 | -------------------------------------------------------------------------------- /test/manual/02-traffic-split-1000-0-output.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: networking.istio.io/v1alpha3 3 | kind: VirtualService 4 | metadata: 5 | name: reviews-rollout 6 | spec: 7 | hosts: 8 | - reviews 9 | http: 10 | - route: 11 | - destination: 12 | host: reviews-v1 13 | weight: 100 14 | - destination: 15 | host: reviews-v2 16 | weight: 0 17 | 18 | -------------------------------------------------------------------------------- /test/manual/05-traffic-split-0-1000-output.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: networking.istio.io/v1alpha3 3 | kind: VirtualService 4 | metadata: 5 | name: reviews-rollout 6 | spec: 7 | hosts: 8 | - reviews 9 | http: 10 | - route: 11 | - destination: 12 | host: reviews-v1 13 | weight: 0 14 | - destination: 15 | host: reviews-v2 16 | weight: 100 17 | 18 | -------------------------------------------------------------------------------- /Tiltfile: -------------------------------------------------------------------------------- 1 | # Deploy: tell Tilt what YAML to deploy 2 | k8s_yaml('deploy/operator-and-rbac.yaml') 3 | 4 | custom_build( 5 | 'deislabs/smi-adapter-istio', 6 | 'operator-sdk build $EXPECTED_REF', 7 | ['./pkg', './cmd'], 8 | tag='latest', 9 | live_update=[ 10 | sync('pkg', '/go/src/github.com/deislabs/smi-adapter-istio/pkg'), 11 | run('go install github.com/deislabs/smi-adapter-istio/pkg'), 12 | restart_container(), 13 | ] 14 | ) 15 | -------------------------------------------------------------------------------- /docs/smi-traffictarget/manifests/traffictarget-reviews-v2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: TrafficTarget 3 | apiVersion: access.smi-spec.io/v1alpha1 4 | metadata: 5 | name: productpage-reviews-v2 6 | namespace: default 7 | destination: 8 | kind: ServiceAccount 9 | name: reviews-v2 10 | namespace: default 11 | specs: 12 | - kind: HTTPRouteGroup 13 | name: bookinfo-allowed-paths 14 | matches: 15 | - api 16 | sources: 17 | - kind: ServiceAccount 18 | name: productpage-v1 19 | namespace: default 20 | -------------------------------------------------------------------------------- /pkg/controller/controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "sigs.k8s.io/controller-runtime/pkg/manager" 5 | ) 6 | 7 | // AddToManagerFuncs is a list of functions to add all Controllers to the Manager 8 | var AddToManagerFuncs []func(manager.Manager) error 9 | 10 | // AddToManager adds all Controllers to the Manager 11 | func AddToManager(m manager.Manager) error { 12 | for _, f := range AddToManagerFuncs { 13 | if err := f(m); err != nil { 14 | return err 15 | } 16 | } 17 | return nil 18 | } 19 | -------------------------------------------------------------------------------- /pkg/apis/rbac/v1alpha1/zz_generated.defaults.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | // Code generated by defaulter-gen. DO NOT EDIT. 4 | 5 | package v1alpha1 6 | 7 | import ( 8 | runtime "k8s.io/apimachinery/pkg/runtime" 9 | ) 10 | 11 | // RegisterDefaults adds defaulters functions to the given scheme. 12 | // Public to allow building arbitrary schemes. 13 | // All generated defaulters are covering - they call all nested defaulters. 14 | func RegisterDefaults(scheme *runtime.Scheme) error { 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /pkg/apis/networking/v1alpha3/zz_generated.defaults.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | // Code generated by defaulter-gen. DO NOT EDIT. 4 | 5 | package v1alpha3 6 | 7 | import ( 8 | runtime "k8s.io/apimachinery/pkg/runtime" 9 | ) 10 | 11 | // RegisterDefaults adds defaulters functions to the given scheme. 12 | // Public to allow building arbitrary schemes. 13 | // All generated defaulters are covering - they call all nested defaulters. 14 | func RegisterDefaults(scheme *runtime.Scheme) error { 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /pkg/apis/addtoscheme_traffictarget_v1alpha1.go: -------------------------------------------------------------------------------- 1 | package apis 2 | 3 | import ( 4 | accessv1alpha1 "github.com/deislabs/smi-sdk-go/pkg/apis/access/v1alpha1" 5 | specsv1alpha1 "github.com/deislabs/smi-sdk-go/pkg/apis/specs/v1alpha1" 6 | ) 7 | 8 | func init() { 9 | // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back 10 | AddToSchemes = append(AddToSchemes, accessv1alpha1.SchemeBuilder.AddToScheme) 11 | AddToSchemes = append(AddToSchemes, specsv1alpha1.SchemeBuilder.AddToScheme) 12 | } 13 | -------------------------------------------------------------------------------- /docs/smi-flagger/manifests/01-deploy-v1.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: reviews 6 | labels: 7 | app: reviews 8 | version: v1 9 | spec: 10 | selector: 11 | matchLabels: 12 | app: reviews 13 | version: v1 14 | replicas: 2 15 | strategy: 16 | type: RollingUpdate 17 | rollingUpdate: 18 | maxUnavailable: 0 19 | template: 20 | metadata: 21 | labels: 22 | app: reviews 23 | version: v1 24 | spec: 25 | containers: 26 | - name: reviews 27 | image: istio/examples-bookinfo-reviews-v1:1.10.1 28 | imagePullPolicy: IfNotPresent 29 | ports: 30 | - containerPort: 9080 31 | -------------------------------------------------------------------------------- /pkg/apis/rbac/v1alpha1/register.go: -------------------------------------------------------------------------------- 1 | // NOTE: Boilerplate only. Ignore this file. 2 | 3 | // Package v1alpha1 contains API Schema definitions for the rbac v1alpha1 API group 4 | // +k8s:deepcopy-gen=package,register 5 | // +groupName=rbac.istio.io 6 | package v1alpha1 7 | 8 | import ( 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | "sigs.k8s.io/controller-runtime/pkg/runtime/scheme" 11 | ) 12 | 13 | var ( 14 | // SchemeGroupVersion is group version used to register these objects 15 | SchemeGroupVersion = schema.GroupVersion{Group: "rbac.istio.io", Version: "v1alpha1"} 16 | 17 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 18 | SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} 19 | ) 20 | -------------------------------------------------------------------------------- /pkg/apis/networking/v1alpha3/register.go: -------------------------------------------------------------------------------- 1 | // NOTE: Boilerplate only. Ignore this file. 2 | 3 | // Package v1alpha3 contains API Schema definitions for the networking v1alpha3 API group 4 | // +k8s:deepcopy-gen=package,register 5 | // +groupName=networking.istio.io 6 | package v1alpha3 7 | 8 | import ( 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | "sigs.k8s.io/controller-runtime/pkg/runtime/scheme" 11 | ) 12 | 13 | var ( 14 | // SchemeGroupVersion is group version used to register these objects 15 | SchemeGroupVersion = schema.GroupVersion{Group: "networking.istio.io", Version: "v1alpha3"} 16 | 17 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 18 | SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} 19 | ) 20 | -------------------------------------------------------------------------------- /test/manual/03-deploy-v2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Deployment 4 | metadata: 5 | name: reviews-v2 6 | labels: 7 | app: reviews 8 | version: v2 9 | spec: 10 | replicas: 1 11 | template: 12 | metadata: 13 | labels: 14 | app: reviews 15 | version: v2 16 | spec: 17 | containers: 18 | - name: reviews 19 | image: istio/examples-bookinfo-reviews-v2:1.10.1 20 | imagePullPolicy: IfNotPresent 21 | ports: 22 | - containerPort: 9080 23 | --- 24 | apiVersion: v1 25 | kind: Service 26 | metadata: 27 | name: reviews-v2 28 | labels: 29 | app: reviews 30 | service: reviews 31 | spec: 32 | ports: 33 | - port: 9080 34 | name: http 35 | selector: 36 | app: reviews 37 | version: v2 38 | -------------------------------------------------------------------------------- /docs/smi-flagger/manifests/02-canary.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: flagger.app/v1alpha3 2 | kind: Canary 3 | metadata: 4 | name: reviews 5 | spec: 6 | targetRef: 7 | apiVersion: apps/v1 8 | kind: Deployment 9 | name: reviews 10 | progressDeadlineSeconds: 60 11 | service: 12 | port: 9080 13 | canaryAnalysis: 14 | interval: 15s 15 | threshold: 15 16 | maxWeight: 90 17 | stepWeight: 10 18 | metrics: 19 | - name: request-success-rate 20 | threshold: 99 21 | interval: 1m 22 | - name: request-duration 23 | threshold: 500 24 | interval: 30s 25 | webhooks: 26 | - name: load-test 27 | url: http://flagger-loadtester/ 28 | timeout: 5s 29 | metadata: 30 | type: cmd 31 | cmd: "hey -z 1m -q 10 -c 2 http://reviews-canary:9080/" 32 | -------------------------------------------------------------------------------- /test/e2e/README.md: -------------------------------------------------------------------------------- 1 | # Running e2e tests for smi-adapter-istio 2 | 3 | ## Prerequistes 4 | - Running Kubernetes cluster 5 | - [Install Istio](https://github.com/servicemeshinterface/smi-adapter-istio/tree/master/docs/smi-trafficsplit#install-istio) on Kubernetes cluster 6 | - [Install operator-sdk cli](https://github.com/operator-framework/operator-sdk/blob/master/doc/user-guide.md#install-the-operator-sdk-cli) 7 | - Install SMI CRDs on cluster 8 | ```bash 9 | $ kubectl apply -f https://raw.githubusercontent.com/servicemeshinterface/smi-adapter-istio/master/deploy/crds/crds.yaml 10 | ``` 11 | 12 | ## Run e2e tests 13 | 14 | ```bash 15 | $ cd $GOPATH/src/github.com/servicemeshinterface/smi-adapter-istio/ 16 | 17 | $ operator-sdk test local ./test/e2e --namespaced-manifest deploy/operator-and-rbac.yaml --namespace istio-system 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/smi-trafficsplit/manifests/03-deploy-v2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: reviews-v2 6 | labels: 7 | app: reviews 8 | version: v2 9 | spec: 10 | selector: 11 | matchLabels: 12 | app: reviews 13 | version: v2 14 | replicas: 1 15 | template: 16 | metadata: 17 | labels: 18 | app: reviews 19 | version: v2 20 | spec: 21 | containers: 22 | - name: reviews 23 | image: istio/examples-bookinfo-reviews-v2:1.10.1 24 | imagePullPolicy: IfNotPresent 25 | ports: 26 | - containerPort: 9080 27 | --- 28 | apiVersion: v1 29 | kind: Service 30 | metadata: 31 | name: reviews-v2 32 | labels: 33 | app: reviews 34 | service: reviews 35 | spec: 36 | ports: 37 | - port: 9080 38 | name: http 39 | selector: 40 | app: reviews 41 | version: v2 42 | -------------------------------------------------------------------------------- /.github/workflows/fossa.yml: -------------------------------------------------------------------------------- 1 | name: FOSSA 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-go@v2 14 | with: 15 | go-version: "^1.14.x" 16 | - run: go version 17 | # Runs a set of commands to initialize and analyze with FOSSA 18 | - name: run FOSSA analysis 19 | env: 20 | # FOSSA Push-Only API Token 21 | FOSSA_API_KEY: '5cda1cd0d5036f6f368682c23a5fdfec' 22 | run: | 23 | export GOPATH=$HOME/go 24 | export PATH=$PATH:$(go env GOPATH)/bin 25 | curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash 26 | fossa init --include-all 27 | fossa analyze 28 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | test: 4 | docker: 5 | - image: circleci/golang:1.12 6 | working_directory: /go/src/github.com/servicemeshinterface/smi-adapter-istio 7 | steps: 8 | - checkout 9 | - run: make bootstrap 10 | - run: make test 11 | 12 | build: 13 | docker: 14 | - image: circleci/golang:1.12 15 | working_directory: /go/src/github.com/servicemeshinterface/smi-adapter-istio 16 | steps: 17 | - checkout 18 | - setup_remote_docker 19 | - run: 20 | name: build and push image 21 | command: | 22 | docker login -u $DOCKER_USER -p $DOCKER_PASS && \ 23 | make bootstrap ci-build ci-push 24 | workflows: 25 | version: 2 26 | build-workflow: 27 | jobs: 28 | - test 29 | - build: 30 | requires: 31 | - test 32 | filters: 33 | branches: 34 | only: master 35 | -------------------------------------------------------------------------------- /test/manual/01-deploy-v1.yaml: -------------------------------------------------------------------------------- 1 | ################################################################################################## 2 | # Reviews service 3 | ################################################################################################## 4 | --- 5 | apiVersion: extensions/v1beta1 6 | kind: Deployment 7 | metadata: 8 | name: reviews-v1 9 | labels: 10 | app: reviews 11 | version: v1 12 | spec: 13 | replicas: 1 14 | template: 15 | metadata: 16 | labels: 17 | app: reviews 18 | version: v1 19 | spec: 20 | containers: 21 | - name: reviews 22 | image: istio/examples-bookinfo-reviews-v1:1.10.1 23 | imagePullPolicy: IfNotPresent 24 | ports: 25 | - containerPort: 9080 26 | --- 27 | apiVersion: v1 28 | kind: Service 29 | metadata: 30 | name: reviews-v1 31 | labels: 32 | app: reviews 33 | service: reviews 34 | spec: 35 | ports: 36 | - port: 9080 37 | name: http 38 | selector: 39 | app: reviews 40 | version: v1 41 | --- 42 | -------------------------------------------------------------------------------- /docs/smi-trafficsplit/manifests/01-deploy-v1.yaml: -------------------------------------------------------------------------------- 1 | ################################################################################################## 2 | # Reviews service 3 | ################################################################################################## 4 | --- 5 | apiVersion: apps/v1 6 | kind: Deployment 7 | metadata: 8 | name: reviews-v1 9 | labels: 10 | app: reviews 11 | version: v1 12 | spec: 13 | selector: 14 | matchLabels: 15 | app: reviews 16 | version: v1 17 | replicas: 1 18 | template: 19 | metadata: 20 | labels: 21 | app: reviews 22 | version: v1 23 | spec: 24 | containers: 25 | - name: reviews 26 | image: istio/examples-bookinfo-reviews-v1:1.10.1 27 | imagePullPolicy: IfNotPresent 28 | ports: 29 | - containerPort: 9080 30 | --- 31 | apiVersion: v1 32 | kind: Service 33 | metadata: 34 | name: reviews-v1 35 | labels: 36 | app: reviews 37 | service: reviews 38 | spec: 39 | ports: 40 | - port: 9080 41 | name: http 42 | selector: 43 | app: reviews 44 | version: v1 45 | --- 46 | -------------------------------------------------------------------------------- /GOVERNANCE.md: -------------------------------------------------------------------------------- 1 | # Governance 2 | 3 | ## Project Maintainers 4 | 5 | [Project maintainers](CODEOWNERS) are responsible for activities around maintaining and updating the SMI Adapter for Istio. Final decisions on the project reside with the project maintainers. 6 | 7 | Changes to the code base require approval from one project maintainer. 8 | 9 | Maintainers MUST remain active. If they are unresponsive for >3 months, they will be automatically removed unless a [super-majority](https://en.wikipedia.org/wiki/Supermajority#Two-thirds_vote) of the other project maintainers agrees to extend the period to be greater than 3 months. 10 | 11 | New maintainers can be added to the project by a [super-majority](https://en.wikipedia.org/wiki/Supermajority#Two-thirds_vote) vote of the existing maintainers. A potential maintainer may be nominated by an existing maintainer. A vote is conducted in private between the current maintainers over the course of a one week voting period. At the end of the week, votes are counted and a pull request is made on the repo adding the new maintainer to the [CODEOWNERS](CODEOWNERS) file. 12 | 13 | A maintainer may step down by submitting an [issue](https://github.com/servicemeshinterface/smi-adapter-istio/issues/new) stating their intent. 14 | 15 | Changes to this governance document require a pull request with approval from a [super-majority](https://en.wikipedia.org/wiki/Supermajority#Two-thirds_vote) of the current maintainers. 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Temporary Build Files 2 | build/_output 3 | build/_test 4 | # Created by https://www.gitignore.io/api/go,vim,emacs,visualstudiocode 5 | ### Emacs ### 6 | # -*- mode: gitignore; -*- 7 | *~ 8 | \#*\# 9 | /.emacs.desktop 10 | /.emacs.desktop.lock 11 | *.elc 12 | auto-save-list 13 | tramp 14 | .\#* 15 | # Org-mode 16 | .org-id-locations 17 | *_archive 18 | # flymake-mode 19 | *_flymake.* 20 | # eshell files 21 | /eshell/history 22 | /eshell/lastdir 23 | # elpa packages 24 | /elpa/ 25 | # reftex files 26 | *.rel 27 | # AUCTeX auto folder 28 | /auto/ 29 | # cask packages 30 | .cask/ 31 | dist/ 32 | # Flycheck 33 | flycheck_*.el 34 | # server auth directory 35 | /server/ 36 | # projectiles files 37 | .projectile 38 | projectile-bookmarks.eld 39 | # directory configuration 40 | .dir-locals.el 41 | # saveplace 42 | places 43 | # url cache 44 | url/cache/ 45 | # cedet 46 | ede-projects.el 47 | # smex 48 | smex-items 49 | # company-statistics 50 | company-statistics-cache.el 51 | # anaconda-mode 52 | anaconda-mode/ 53 | ### Go ### 54 | # Binaries for programs and plugins 55 | *.exe 56 | *.exe~ 57 | *.dll 58 | *.so 59 | *.dylib 60 | # Test binary, build with 'go test -c' 61 | *.test 62 | # Output of the go coverage tool, specifically when used with LiteIDE 63 | *.out 64 | ### Vim ### 65 | # swap 66 | .sw[a-p] 67 | .*.sw[a-p] 68 | # session 69 | Session.vim 70 | # temporary 71 | .netrwhist 72 | # auto-generated tag files 73 | tags 74 | ### VisualStudioCode ### 75 | .vscode/* 76 | .history 77 | # End of https://www.gitignore.io/api/go,vim,emacs,visualstudiocode 78 | vendor/ 79 | -------------------------------------------------------------------------------- /pkg/apis/rbac/v1alpha1/servicerole_types.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // ServiceRoleSpec defines the desired state of ServiceRole 8 | // +k8s:openapi-gen=true 9 | type ServiceRoleSpec struct { 10 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 11 | // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file 12 | // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html 13 | 14 | // Required. The set of access rules (permissions) that the role has. 15 | Rules []*AccessRule `json:"rules,omitempty"` 16 | } 17 | 18 | // ServiceRoleStatus defines the observed state of ServiceRole 19 | // +k8s:openapi-gen=true 20 | type ServiceRoleStatus struct { 21 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 22 | // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file 23 | // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html 24 | } 25 | 26 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 27 | 28 | // ServiceRole is the Schema for the serviceroles API 29 | // +k8s:openapi-gen=true 30 | type ServiceRole struct { 31 | metav1.TypeMeta `json:",inline"` 32 | metav1.ObjectMeta `json:"metadata,omitempty"` 33 | 34 | Spec ServiceRoleSpec `json:"spec,omitempty"` 35 | Status ServiceRoleStatus `json:"status,omitempty"` 36 | } 37 | 38 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 39 | 40 | // ServiceRoleList contains a list of ServiceRole 41 | type ServiceRoleList struct { 42 | metav1.TypeMeta `json:",inline"` 43 | metav1.ListMeta `json:"metadata,omitempty"` 44 | Items []ServiceRole `json:"items"` 45 | } 46 | 47 | func init() { 48 | SchemeBuilder.Register(&ServiceRole{}, &ServiceRoleList{}) 49 | } 50 | -------------------------------------------------------------------------------- /deploy/crds/crds.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | name: traffictargets.access.smi-spec.io 5 | spec: 6 | additionalPrinterColumns: 7 | - JSONPath: .spec.service 8 | description: The service 9 | name: Service 10 | type: string 11 | group: access.smi-spec.io 12 | names: 13 | kind: TrafficTarget 14 | listKind: TrafficTargetList 15 | plural: traffictargets 16 | singular: traffictarget 17 | scope: Namespaced 18 | subresources: 19 | status: {} 20 | version: v1alpha1 21 | versions: 22 | - name: v1alpha1 23 | served: true 24 | storage: true 25 | --- 26 | apiVersion: apiextensions.k8s.io/v1beta1 27 | kind: CustomResourceDefinition 28 | metadata: 29 | name: httproutegroups.specs.smi-spec.io 30 | spec: 31 | additionalPrinterColumns: 32 | - JSONPath: .spec.service 33 | description: The service 34 | name: Service 35 | type: string 36 | group: specs.smi-spec.io 37 | names: 38 | kind: HTTPRouteGroup 39 | listKind: HTTPRouteGroupList 40 | plural: httproutegroups 41 | singular: httproutegroup 42 | scope: Namespaced 43 | subresources: 44 | status: {} 45 | version: v1alpha1 46 | versions: 47 | - name: v1alpha1 48 | served: true 49 | storage: true 50 | --- 51 | apiVersion: apiextensions.k8s.io/v1beta1 52 | kind: CustomResourceDefinition 53 | metadata: 54 | name: trafficsplits.split.smi-spec.io 55 | spec: 56 | additionalPrinterColumns: 57 | - JSONPath: .spec.service 58 | description: The service 59 | name: Service 60 | type: string 61 | group: split.smi-spec.io 62 | names: 63 | kind: TrafficSplit 64 | listKind: TrafficSplitList 65 | plural: trafficsplits 66 | singular: trafficsplit 67 | scope: Namespaced 68 | subresources: 69 | status: {} 70 | version: v1alpha2 71 | versions: 72 | - name: v1alpha2 73 | served: true 74 | storage: true 75 | -------------------------------------------------------------------------------- /docs/smi-traffictarget/manifests/traffictarget.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: specs.smi-spec.io/v1alpha1 3 | kind: HTTPRouteGroup 4 | metadata: 5 | name: bookinfo-allowed-paths 6 | namespace: default 7 | matches: 8 | - name: api 9 | pathRegex: "" 10 | methods: ["*"] 11 | --- 12 | kind: TrafficTarget 13 | apiVersion: access.smi-spec.io/v1alpha1 14 | metadata: 15 | name: productpage-reviews-v1 16 | namespace: default 17 | destination: 18 | kind: ServiceAccount 19 | name: reviews-v1 20 | namespace: default 21 | specs: 22 | - kind: HTTPRouteGroup 23 | name: bookinfo-allowed-paths 24 | matches: 25 | - api 26 | sources: 27 | - kind: ServiceAccount 28 | name: productpage-v1 29 | namespace: default 30 | --- 31 | kind: TrafficTarget 32 | apiVersion: access.smi-spec.io/v1alpha1 33 | metadata: 34 | name: productpage-reviews-v3 35 | namespace: default 36 | destination: 37 | kind: ServiceAccount 38 | name: reviews-v3 39 | namespace: default 40 | specs: 41 | - kind: HTTPRouteGroup 42 | name: bookinfo-allowed-paths 43 | matches: 44 | - api 45 | sources: 46 | - kind: ServiceAccount 47 | name: productpage-v1 48 | namespace: default 49 | --- 50 | kind: TrafficTarget 51 | apiVersion: access.smi-spec.io/v1alpha1 52 | metadata: 53 | name: productpage-details-v3 54 | namespace: default 55 | destination: 56 | kind: ServiceAccount 57 | name: details-v1 58 | namespace: default 59 | specs: 60 | - kind: HTTPRouteGroup 61 | name: bookinfo-allowed-paths 62 | matches: 63 | - api 64 | sources: 65 | - kind: ServiceAccount 66 | name: productpage-v1 67 | namespace: default 68 | --- 69 | kind: TrafficTarget 70 | apiVersion: access.smi-spec.io/v1alpha1 71 | metadata: 72 | name: reviews-ratings 73 | namespace: default 74 | destination: 75 | kind: ServiceAccount 76 | name: ratings-v1 77 | namespace: default 78 | specs: 79 | - kind: HTTPRouteGroup 80 | name: bookinfo-allowed-paths 81 | matches: 82 | - api 83 | sources: 84 | - kind: ServiceAccount 85 | name: reviews-v2 86 | namespace: default 87 | - kind: ServiceAccount 88 | name: reviews-v3 89 | namespace: default 90 | --- 91 | -------------------------------------------------------------------------------- /pkg/apis/rbac/v1alpha1/servicerolebinding_types.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // ServiceRoleBindingSpec defines the desired state of ServiceRoleBinding 8 | // +k8s:openapi-gen=true 9 | type ServiceRoleBindingSpec struct { 10 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 11 | // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file 12 | // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html 13 | 14 | // Required. List of subjects that are assigned the ServiceRole object. 15 | Subjects []*Subject `json:"subjects,omitempty"` 16 | // Required. Reference to the ServiceRole object. 17 | RoleRef *RoleRef `json:"roleRef,omitempty"` 18 | } 19 | 20 | // ServiceRoleBindingStatus defines the observed state of ServiceRoleBinding 21 | // +k8s:openapi-gen=true 22 | type ServiceRoleBindingStatus struct { 23 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 24 | // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file 25 | // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html 26 | } 27 | 28 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 29 | 30 | // ServiceRoleBinding is the Schema for the servicerolebindings API 31 | // +k8s:openapi-gen=true 32 | type ServiceRoleBinding struct { 33 | metav1.TypeMeta `json:",inline"` 34 | metav1.ObjectMeta `json:"metadata,omitempty"` 35 | 36 | Spec ServiceRoleBindingSpec `json:"spec,omitempty"` 37 | Status ServiceRoleBindingStatus `json:"status,omitempty"` 38 | } 39 | 40 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 41 | 42 | // ServiceRoleBindingList contains a list of ServiceRoleBinding 43 | type ServiceRoleBindingList struct { 44 | metav1.TypeMeta `json:",inline"` 45 | metav1.ListMeta `json:"metadata,omitempty"` 46 | Items []ServiceRoleBinding `json:"items"` 47 | } 48 | 49 | func init() { 50 | SchemeBuilder.Register(&ServiceRoleBinding{}, &ServiceRoleBindingList{}) 51 | } 52 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Force dep to vendor the code generators, which aren't imported just used at dev time. 2 | required = [ 3 | "k8s.io/code-generator/cmd/deepcopy-gen", 4 | "k8s.io/code-generator/cmd/conversion-gen", 5 | "k8s.io/code-generator/cmd/client-gen", 6 | "k8s.io/code-generator/cmd/lister-gen", 7 | "k8s.io/code-generator/cmd/informer-gen", 8 | "k8s.io/kube-openapi/cmd/openapi-gen", 9 | "k8s.io/gengo/args", 10 | #"sigs.k8s.io/controller-tools/pkg/crd/generator", 11 | ] 12 | 13 | [[override]] 14 | name = "k8s.io/code-generator" 15 | # revision for tag "kubernetes-1.13.1" 16 | revision = "c2090bec4d9b1fb25de3812f868accc2bc9ecbae" 17 | 18 | [[override]] 19 | name = "k8s.io/kube-openapi" 20 | revision = "0cf8f7e6ed1d2e3d47d02e3b6e559369af24d803" 21 | 22 | [[override]] 23 | name = "github.com/go-openapi/spec" 24 | branch = "master" 25 | 26 | [[override]] 27 | name = "sigs.k8s.io/controller-tools" 28 | #version = "=v0.1.9" 29 | revision = "9d55346c2bde73fb3326ac22eac2e5210a730207" 30 | 31 | [[override]] 32 | name = "k8s.io/api" 33 | # revision for tag "kubernetes-1.13.1" 34 | revision = "05914d821849570fba9eacfb29466f2d8d3cd229" 35 | 36 | [[override]] 37 | name = "k8s.io/apiextensions-apiserver" 38 | # revision for tag "kubernetes-1.13.1" 39 | revision = "0fe22c71c47604641d9aa352c785b7912c200562" 40 | 41 | [[override]] 42 | name = "k8s.io/apimachinery" 43 | # revision for tag "kubernetes-1.13.1" 44 | revision = "2b1284ed4c93a43499e781493253e2ac5959c4fd" 45 | 46 | [[override]] 47 | name = "k8s.io/client-go" 48 | # revision for tag "kubernetes-1.13.1" 49 | revision = "8d9ed539ba3134352c586810e749e58df4e94e4f" 50 | 51 | [[override]] 52 | name = "github.com/coreos/prometheus-operator" 53 | version = "=v0.29.0" 54 | 55 | [[override]] 56 | name = "sigs.k8s.io/controller-runtime" 57 | version = "=v0.1.10" 58 | 59 | [[override]] 60 | name = "github.com/operator-framework/operator-sdk" 61 | # The version rule is used for a specific release and the master branch for in between releases. 62 | #branch = "master" #osdk_branch_annotation 63 | version = "=v0.10.0" #osdk_version_annotation 64 | 65 | [[constraint]] 66 | name = "github.com/deislabs/smi-sdk-go" 67 | version = "v0.2.0" 68 | 69 | [prune] 70 | go-tests = true 71 | non-go = true 72 | 73 | [[prune.project]] 74 | name = "k8s.io/code-generator" 75 | non-go = false 76 | 77 | [[prune.project]] 78 | name = "k8s.io/gengo" 79 | non-go = false 80 | 81 | [[override]] 82 | name = "github.com/evanphx/json-patch" 83 | version = "4.5.0" 84 | -------------------------------------------------------------------------------- /docs/smi-trafficsplit/overview.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The SMI Istio Adapter implements the SMI Traffic Split API as dictated by the [SMI spec](https://github.com/servicemeshinterface/smi-spec/blob/master/traffic-split.md). 4 | 5 | See example TrafficSplit resource below: 6 | ```yaml 7 | apiVersion: split.smi-spec.io/v1alpha1 8 | kind: TrafficSplit 9 | metadata: 10 | name: my-trafficsplit 11 | spec: 12 | # The root service that clients use to connect to the destination application. 13 | service: website 14 | # Services inside the namespace with their own selectors, endpoints and configuration. 15 | backends: 16 | - service: website-v1 17 | weight: 80 18 | - service: website-v2 19 | weight: 20 20 | ``` 21 | 22 | The above TrafficSplit resource describes how traffic should be split between the services `website-v1` and `website-v2`. Under the hood, this adapter watches Traffic Split resources and creates/modifies Istio [Virtual Services](https://istio.io/docs/reference/config/networking/v1alpha3/virtual-service/) which then direct traffic to Kubernetes Services accordingly. Once this example TrafficSplit resource is created in Kubernetes, a corresponding Istio `VirtualService` will be created called `my-trafficsplit-vs`. Note the naming convention between the `TrafficSplit` resource and the `VirtualService` resource. The name of the `VirtualService` will be the same as the `TrafficSplit` resource with `-vs` appended to it. 23 | 24 | ## Weights 25 | A user can specify a field called `weight` for every service specified in the list of backends on the Traffic Split resource. 26 | 27 | The weights above correlate to the weight on a corresponding `VirtualService` object in Istio. There are some rules around weights in Istio Virtual Services though: 28 | 1. Weights must be whole numbers 0-100. 29 | 2. The sum of the weights must equal 100 30 | 31 | To achieve the most intentional behavior, it is a best practice to ensure that the weights specified in a TrafficSplit resource meet these constraints. A `weight` in a TrafficSplit object must be a whole number as is specified in the SMI spec, however there is no constraint/limit on what that number can be. If you specify numbers that do not sum to 100, the SMI Istio adapter will take the total weight and assign whole numbers to each service that correspond to the percentage of how much traffic each weight should receive. If the weights don't cleanly add up to 100, then the last service will be rounded such that the total weight does equal 100 and the corresponding Istio `VirtualService` will be created with those re-calculated weights. 32 | 33 | ## Demo 34 | Take the Traffic Split functionality for a spin with [this demo](smi-trafficsplit/README.md). 35 | -------------------------------------------------------------------------------- /deploy/operator-and-rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: smi-adapter-istio 5 | namespace: istio-system 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | name: smi-adapter-istio 11 | template: 12 | metadata: 13 | labels: 14 | name: smi-adapter-istio 15 | annotations: 16 | sidecar.istio.io/inject: "false" 17 | spec: 18 | serviceAccountName: smi-adapter-istio 19 | containers: 20 | - name: smi-adapter-istio 21 | image: deislabs/smi-adapter-istio:latest 22 | command: 23 | - smi-adapter-istio 24 | imagePullPolicy: Always 25 | env: 26 | - name: WATCH_NAMESPACE 27 | value: "" 28 | - name: POD_NAME 29 | valueFrom: 30 | fieldRef: 31 | fieldPath: metadata.name 32 | - name: OPERATOR_NAME 33 | value: "smi-adapter-istio" 34 | --- 35 | apiVersion: v1 36 | kind: ServiceAccount 37 | metadata: 38 | name: smi-adapter-istio 39 | namespace: istio-system 40 | --- 41 | apiVersion: rbac.authorization.k8s.io/v1 42 | kind: ClusterRole 43 | metadata: 44 | name: smi-adapter-istio 45 | namespace: istio-system 46 | rules: 47 | - apiGroups: 48 | - "" 49 | resources: 50 | - pods 51 | - services 52 | - endpoints 53 | - persistentvolumeclaims 54 | - events 55 | - configmaps 56 | - secrets 57 | verbs: 58 | - '*' 59 | - apiGroups: 60 | - apps 61 | resources: 62 | - deployments 63 | - daemonsets 64 | - replicasets 65 | - statefulsets 66 | verbs: 67 | - '*' 68 | - apiGroups: 69 | - monitoring.coreos.com 70 | resources: 71 | - servicemonitors 72 | verbs: 73 | - get 74 | - create 75 | - apiGroups: 76 | - apps 77 | resourceNames: 78 | - smi-adapter-istio 79 | resources: 80 | - deployments/finalizers 81 | verbs: 82 | - update 83 | - apiGroups: 84 | - split.smi-spec.io 85 | - access.smi-spec.io 86 | - specs.smi-spec.io 87 | resources: 88 | - '*' 89 | verbs: 90 | - '*' 91 | - apiGroups: 92 | - networking.istio.io 93 | - rbac.istio.io 94 | resources: 95 | - '*' 96 | verbs: 97 | - '*' 98 | --- 99 | kind: ClusterRoleBinding 100 | apiVersion: rbac.authorization.k8s.io/v1 101 | metadata: 102 | name: smi-adapter-istio 103 | subjects: 104 | - kind: ServiceAccount 105 | name: smi-adapter-istio 106 | namespace: istio-system 107 | roleRef: 108 | kind: ClusterRole 109 | name: smi-adapter-istio 110 | apiGroup: rbac.authorization.k8s.io 111 | -------------------------------------------------------------------------------- /pkg/apis/networking/v1alpha3/virtualservice_types.go: -------------------------------------------------------------------------------- 1 | package v1alpha3 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | ) 6 | 7 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 8 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 9 | 10 | // +k8s:openapi-gen=true 11 | type Destination struct { 12 | Host string `json:"host,omitempty"` 13 | Subset string `json:"subset,omitempty"` 14 | } 15 | 16 | // +k8s:openapi-gen=true 17 | type HTTPRouteDestination struct { 18 | Destination *Destination `protobuf:"bytes,1,opt,name=destination,proto3" json:"destination,omitempty"` 19 | Weight int32 `protobuf:"varint,2,opt,name=weight,proto3" json:"weight,omitempty"` 20 | } 21 | 22 | // Describes match conditions and actions for routing HTTP/1.1, HTTP2, and 23 | // gRPC traffic. See VirtualService for usage examples. 24 | // +k8s:openapi-gen=true 25 | type HTTPRoute struct { 26 | Route []*HTTPRouteDestination `json:"route,omitempty"` 27 | } 28 | 29 | // VirtualServiceSpec defines the desired state of VirtualService 30 | // +k8s:openapi-gen=true 31 | type VirtualServiceSpec struct { 32 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 33 | // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file 34 | // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html 35 | Hosts []string `json:"hosts,omitempty"` 36 | Gateways []string `json:"gateways,omitempty"` 37 | Http []*HTTPRoute `json:"http,omitempty"` 38 | } 39 | 40 | // VirtualServiceStatus defines the observed state of VirtualService 41 | // +k8s:openapi-gen=true 42 | type VirtualServiceStatus struct { 43 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 44 | // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file 45 | // Add custom validation using kubebuilder tags: https://book.kubebuilder.io/beyond_basics/generating_crd.html 46 | } 47 | 48 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 49 | 50 | // VirtualService is the Schema for the virtualservices API 51 | // +k8s:openapi-gen=true 52 | type VirtualService struct { 53 | metav1.TypeMeta `json:",inline"` 54 | metav1.ObjectMeta `json:"metadata,omitempty"` 55 | 56 | Spec VirtualServiceSpec `json:"spec,omitempty"` 57 | Status VirtualServiceStatus `json:"status,omitempty"` 58 | } 59 | 60 | // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 61 | 62 | // VirtualServiceList contains a list of VirtualService 63 | type VirtualServiceList struct { 64 | metav1.TypeMeta `json:",inline"` 65 | metav1.ListMeta `json:"metadata,omitempty"` 66 | Items []VirtualService `json:"items"` 67 | } 68 | 69 | func init() { 70 | SchemeBuilder.Register(&VirtualService{}, &VirtualServiceList{}) 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # smi-adapter-istio 2 | 3 | This is a Kubernetes operator which implements the Service Mesh Interface(SMI) [Traffic Split](https://github.com/servicemeshinterface/smi-spec/blob/master/traffic-split.md), [Traffic Access Control](https://github.com/servicemeshinterface/smi-spec/blob/master/traffic-access-control.md) and [Traffic Specs](https://github.com/servicemeshinterface/smi-spec/blob/master/traffic-specs.md) APIs to work with Istio. The [Traffic Metrics](https://github.com/servicemeshinterface/smi-spec/blob/master/traffic-metrics.md) part of the SMI spec is implemented in the [smi-metrics](https://github.com/servicemeshinterface/smi-metrics) repo. 4 | 5 | Tools or humans may set up and use this operator after installing Istio to do things like: 6 | - orchestrate canary releases for new versions of software or more generally manage traffic shifting over time for applications 7 | - define which services are allowed to send traffic to another service (and even a route on a service) or more generally define access control policies for applications 8 | 9 | SMI defines a set of CRDs that allow for a common set of interfaces to build on top of when building tooling or working with service mesh implementations like Istio. This project builds logic around those commonly defined CRDs to work specifically with Istio. 10 | 11 | ## Getting Started 12 | 13 | ### Prerequesites 14 | - Running Kubernetes cluster 15 | - `kubectl` is installed locally 16 | - [Istio installed](https://istio.io/docs/setup/kubernetes/install/kubernetes/) on Kubernetes cluster 17 | 18 | ### Install operator 19 | Install crds, operator, and rbac configuration in Kubernetes cluster: 20 | ```bash 21 | $ kubectl apply -f https://raw.githubusercontent.com/servicemeshinterface/smi-adapter-istio/master/deploy/crds/crds.yaml 22 | $ kubectl apply -f https://raw.githubusercontent.com/servicemeshinterface/smi-adapter-istio/master/deploy/operator-and-rbac.yaml 23 | 24 | Check that the operator has been deployed: 25 | ```bash 26 | $ kubectl get pods -n istio-system -l name=smi-adapter-istio 27 | NAME READY STATUS RESTARTS AGE 28 | smi-adapter-istio-5ffcm8fqm 1/1 Running 0 20s 29 | ``` 30 | 31 | ### Deploy SMI custom resources for managing traffic to services 32 | - See how to use this project for traffic splitting [here](docs/smi-trafficsplit) 33 | - See how to use this project to restrict service to service communication [here](docs/smi-traffictarget) 34 | - See how to use Flagger and this project to do canary deploys [here](docs/smi-flagger) 35 | 36 | ## Contributing 37 | 38 | Please refer to [CONTRIBUTING.md](./CONTRIBUTING.md) for more information on contributing to this repository and setting up your development environment. 39 | 40 | Find information for building and pushing your own operator image [here](CONTRIBUTING.md#build-and-push-operator-image). 41 | 42 | More detailed instructions for developing against the operator can be found [here](CONTRIBUTING.md#Developing-Using-Tilt). 43 | -------------------------------------------------------------------------------- /test/manual/README.md: -------------------------------------------------------------------------------- 1 | # Testing SMI Istio Adapter 2 | 3 | Make sure you have setup Istio on a cluster. 4 | 5 | #### Initial Setup 6 | 7 | Install the bookinfo applications except the reviews application: 8 | 9 | ```bash 10 | kubectl label namespace default istio-injection=enabled 11 | kubectl apply -f 00-bookinfo-setup.yaml 12 | ``` 13 | 14 | **Note:** Change namespace to suit your needs. 15 | 16 | #### Install reviews v1 application 17 | 18 | ```bash 19 | kubectl apply -f 01-deploy-v1.yaml 20 | ``` 21 | 22 | #### Verify the application works for version v1 23 | 24 | ```bash 25 | curl `minikube ip`:31380/productpage 26 | ``` 27 | 28 | OR visit above URL in the browser and see that you won't see any `ratings` since the `reviews v1` does not call `ratings` service at all. 29 | 30 | 31 | #### Create TrafficSplit for v1 32 | 33 | ```bash 34 | kubectl apply -f 02-traffic-split-1000-0.yaml 35 | ``` 36 | 37 | If you don't have SMI Istio Adapter installed run following: 38 | 39 | ``` 40 | kubectl apply -f 02-traffic-split-1000-0-output.yaml 41 | ``` 42 | 43 | #### Install reviews v2 application 44 | 45 | ``` 46 | kubectl apply -f 03-deploy-v2.yaml 47 | ``` 48 | 49 | You can try following step to verify that the traffic is still only sent to `v1` of the application even though there is `v2` version of the application deployed. 50 | 51 | ```bash 52 | curl `minikube ip`:31380/productpage 53 | ``` 54 | 55 | Until you perform the next step no traffic will be sent to the `v2`. 56 | 57 | #### Create TrafficSplit to send 33% of traffic to v2 58 | 59 | ```bash 60 | kubectl apply -f 04-traffic-split-1000-500.yaml 61 | ``` 62 | 63 | If you don't have SMI Istio Adapter installed run following: 64 | 65 | ``` 66 | kubectl apply -f 04-traffic-split-1000-500-output.yaml 67 | ``` 68 | 69 | Refresh browser multiple times to see that the traffic is sent to `v1` `67%` of the time and to `v2` `33%` of the time. 70 | 71 | #### Verify the v2 to send all traffic 72 | 73 | If v2 of the reviews application looks good then start sending entire traffic to v2. 74 | 75 | ```bash 76 | kubectl apply -f 05-traffic-split-0-1000.yaml 77 | ``` 78 | 79 | If you don't have SMI Istio Adapter installed run following: 80 | 81 | ``` 82 | kubectl apply -f 05-traffic-split-0-1000-output.yaml 83 | ``` 84 | 85 | #### Now cleanup unwanted resources 86 | 87 | General Kubernetes cleanup 88 | 89 | ```bash 90 | kubectl delete deployment reviews-v1 91 | kubectl delete service reviews-v1 92 | kubectl delete service reviews-v2 93 | ``` 94 | 95 | SMI Istio Adapter cleanup 96 | 97 | ```bash 98 | kubectl delete trafficsplit reviews-rollout 99 | ``` 100 | 101 | If you don't have SMI Istio Adapter installed run following: 102 | 103 | ```bash 104 | kubectl delete destinationrule reviews-rollout 105 | kubectl delete virtualservice reviews-rollout 106 | ``` 107 | 108 | To clean everything from this test run following command: 109 | 110 | ```bash 111 | kubectl delete -f . 112 | ``` 113 | -------------------------------------------------------------------------------- /docs/smi-traffictarget/README.md: -------------------------------------------------------------------------------- 1 | # Traffic Target 2 | 3 | This demo will show you how to use the [traffic spec](https://github.com/servicemeshinterface/smi-spec/blob/master/traffic-specs.md) and [traffic access control](https://github.com/servicemeshinterface/smi-spec/blob/master/traffic-access-control.md) specification 4 | 5 | ## Prerequisites 6 | Follow the [getting started section](https://github.com/servicemeshinterface/smi-adapter-istio#getting-started) to ensure you have the following: 7 | - A running Kubernetes cluster 8 | - Istio installed on Kubernetes cluster 9 | - Deployed smi-istio-operator and related configs 10 | ```bash 11 | kubectl apply -f https://raw.githubusercontent.com/servicemeshinterface/smi-adapter-istio/master/deploy/crds/crds.yaml 12 | kubectl apply -f https://raw.githubusercontent.com/servicemeshinterface/smi-adapter-istio/master/deploy/operator-and-rbac.yaml 13 | ``` 14 | 15 | ## Setup sample bookinfo application 16 | * Now deploy the sample bookinfo application 17 | 18 | ```bash 19 | kubectl label namespace default istio-injection=enabled 20 | kubectl apply -f manifests/configs.yml 21 | ``` 22 | 23 | Try to visit the application homepage and verify that the application is running fine. 24 | 25 | If you are running istio and this whole setup on minikube then the easiest way to access the application is to open the URL generated by following command in your browser. 26 | 27 | ```bash 28 | echo "http://$(minikube ip):31380/productpage" 29 | ``` 30 | 31 | But if you are running this setup in a public cloud that allows load balancer services then run following command to get the URL. 32 | 33 | ```bash 34 | echo "http://$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')/productpage" 35 | ``` 36 | 37 | Above command extracts the public IP of the service `istio-ingressgateway` of type Load Balancer. And constructs the URL to reach the productpage. 38 | 39 | * Deploy configs which are needed to provide access to frontend application *productpage*. 40 | 41 | ```bash 42 | kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.1/samples/bookinfo/platform/kube/rbac/rbac-config-ON.yaml 43 | kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.1/samples/bookinfo/platform/kube/rbac/productpage-policy.yaml 44 | ``` 45 | 46 | If you try to revisit the application, you will see that you don't get any output from `details` and `reviews` page. And will see the output as `Error fetching product details!` and `Error fetching product reviews!`. 47 | 48 | 49 | ## Deploy Traffic Access Control and Traffic Spec 50 | 51 | * Finally deploy the configs 52 | 53 | File [traffictarget.yaml](manifests/traffictarget.yaml) has configs needed to allow traffic among all the micro-services. 54 | 55 | ```bash 56 | kubectl apply -f manifests/traffictarget.yaml 57 | ``` 58 | 59 | Once the configs are deployed after a while you can see that the application functions without any problems. The only difference from the original application is that right now you will see reviews from v1 and v3 only i.e. you will see only orange colored stars or no stars. 60 | 61 | * Deploy the config to allow productpage to talk to reviews-v2 62 | 63 | ``` 64 | kubectl apply -f manifests/traffictarget-reviews-v2.yaml 65 | ``` 66 | 67 | Now you can see that black colored stars also start appearing on the page. If you look at the config file [traffictarget-reviews-v2.yaml](manifests/traffictarget-reviews-v2.yaml) there productpage is allowed to talk to reviews-v2 service. 68 | 69 | ## Cleanup 70 | Delete sample bookinfo application and all SMI TrafficTarget related configuration: 71 | 72 | ```console 73 | $ kubectl delete -f manifests/ 74 | ``` 75 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OPERATOR_IMAGE ?= docker.io/servicemeshinterface/smi-adapter-istio 2 | OPERATOR_SDK_RELEASE_VERSION ?= v0.9.0 3 | OPERATOR_IMAGE_TAG ?= latest 4 | 5 | #go options 6 | GO ?= go 7 | GOFLAGS := 8 | TESTS := . 9 | TESTFLAGS := 10 | 11 | KIND_VERSION=v0.5.1 12 | HOST_OS := $(shell uname -s) 13 | ifeq ($(HOST_OS), Darwin) 14 | OS_ARCH = darwin-amd64 15 | else 16 | ifeq ($(HOST_OS), Linux) 17 | OS_ARCH = linux-amd64 18 | else 19 | $(error Unsupported Host OS) 20 | endif 21 | endif 22 | 23 | # Required for globs to work correctly 24 | SHELL=/usr/bin/env bash 25 | 26 | GIT_COMMIT ?= $(shell git rev-parse --short HEAD) 27 | 28 | HAS_DEP := $(shell command -v dep;) 29 | HAS_OPERATOR_SDK := $(shell command -v operator-sdk) 30 | HAS_KIND := $(shell command -v kind) 31 | 32 | KIND_KUBECONFIG = $(shell kind get kubeconfig-path --name=local-kind) 33 | 34 | .PHONY: ci-build 35 | ci-build: 36 | ifndef HAS_OPERATOR_SDK 37 | # install linux release binary 38 | curl -JL https://github.com/operator-framework/operator-sdk/releases/download/${OPERATOR_SDK_RELEASE_VERSION}/operator-sdk-${OPERATOR_SDK_RELEASE_VERSION}-x86_64-linux-gnu > operator-sdk 39 | sudo chmod a+x operator-sdk 40 | sudo mv operator-sdk /usr/local/bin 41 | endif 42 | 43 | operator-sdk build $(OPERATOR_IMAGE):$(OPERATOR_IMAGE_TAG) 44 | docker tag $(OPERATOR_IMAGE):$(OPERATOR_IMAGE_TAG) $(OPERATOR_IMAGE):$(GIT_COMMIT) 45 | 46 | ci-push: 47 | docker push $(OPERATOR_IMAGE):$(OPERATOR_IMAGE_TAG) 48 | docker push $(OPERATOR_IMAGE):$(GIT_COMMIT) 49 | 50 | .PHONY: build check-env 51 | build: check-env 52 | operator-sdk build $(OPERATOR_IMAGE):$(OPERATOR_IMAGE_TAG) 53 | docker tag $(OPERATOR_IMAGE):$(OPERATOR_IMAGE_TAG) $(OPERATOR_IMAGE):$(GIT_COMMIT) 54 | 55 | push: 56 | docker push $(OPERATOR_IMAGE):$(OPERATOR_IMAGE_TAG) 57 | docker push $(OPERATOR_IMAGE):$(GIT_COMMIT) 58 | 59 | check-env: 60 | ifndef OPERATOR_IMAGE 61 | $(error Environment variable OPERATOR_IMAGE is undefined) 62 | endif 63 | 64 | GOFORMAT_FILES := $(shell find . -name '*.go' | grep -v '\./vendor/') 65 | 66 | .PHONY: format-go-code 67 | ## Formats any go file that differs from gofmt's style 68 | format-go-code: 69 | @gofmt -s -l -w ${GOFORMAT_FILES} 70 | 71 | .PHONY: test 72 | test: test-unit 73 | 74 | .PHONY: test-unit 75 | test-unit: 76 | @echo 77 | @echo "==> Running unit tests <==" 78 | $(GO) test $(GOFLAGS) -run $(TESTS) ./cmd/... ./pkg/... $(TESTFLAGS) -v 79 | 80 | test-e2e: 81 | KUBECONFIG=$(KIND_KUBECONFIG) \ 82 | operator-sdk test local ./test/e2e --namespaced-manifest deploy/operator-and-rbac.yaml --namespace istio-system 83 | 84 | .PHONY: bootstrap 85 | bootstrap: 86 | ifndef HAS_DEP 87 | go get -u github.com/golang/dep/cmd/dep 88 | endif 89 | dep ensure 90 | 91 | create-kindcluster: 92 | ifndef HAS_KIND 93 | @echo "installing kind" 94 | @curl -fsSLo kind "https://github.com/kubernetes-sigs/kind/releases/download/$(KIND_VERSION)/kind-$(OS_ARCH)" 95 | @chmod +x kind 96 | @mv kind /usr/local/bin/kind 97 | endif 98 | kind create cluster --name local-kind --image kindest/node:v1.15.3 99 | 100 | install-istio: 101 | curl -fsSL https://git.io/getLatestIstio | ISTIO_VERSION=1.1.6 sh - 102 | ls istio-1.1.6/install/kubernetes/helm/istio-init/files/crd*yaml | \ 103 | xargs -I{} kubectl apply -f {} --kubeconfig=$(KIND_KUBECONFIG) 104 | kubectl apply -f istio-1.1.6/install/kubernetes/istio-demo-auth.yaml \ 105 | --kubeconfig=$(KIND_KUBECONFIG) 106 | 107 | install-smi-crds: 108 | kubectl apply -f https://raw.githubusercontent.com/servicemeshinterface/smi-adapter-istio/master/deploy/crds/crds.yaml \ 109 | --kubeconfig=$(KIND_KUBECONFIG) 110 | 111 | clean-kind: 112 | kind delete cluster --name local-kind 113 | -------------------------------------------------------------------------------- /pkg/apis/networking/v1alpha3/zz_generated.openapi.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | // Code generated by openapi-gen. DO NOT EDIT. 4 | 5 | // This file was autogenerated by openapi-gen. Do not edit it manually! 6 | 7 | package v1alpha3 8 | 9 | import ( 10 | spec "github.com/go-openapi/spec" 11 | common "k8s.io/kube-openapi/pkg/common" 12 | ) 13 | 14 | func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { 15 | return map[string]common.OpenAPIDefinition{ 16 | "github.com/deislabs/smi-adapter-istio/pkg/apis/networking/v1alpha3.VirtualService": schema_pkg_apis_networking_v1alpha3_VirtualService(ref), 17 | "github.com/deislabs/smi-adapter-istio/pkg/apis/networking/v1alpha3.VirtualServiceSpec": schema_pkg_apis_networking_v1alpha3_VirtualServiceSpec(ref), 18 | "github.com/deislabs/smi-adapter-istio/pkg/apis/networking/v1alpha3.VirtualServiceStatus": schema_pkg_apis_networking_v1alpha3_VirtualServiceStatus(ref), 19 | } 20 | } 21 | 22 | func schema_pkg_apis_networking_v1alpha3_VirtualService(ref common.ReferenceCallback) common.OpenAPIDefinition { 23 | return common.OpenAPIDefinition{ 24 | Schema: spec.Schema{ 25 | SchemaProps: spec.SchemaProps{ 26 | Description: "VirtualService is the Schema for the virtualservices API", 27 | Properties: map[string]spec.Schema{ 28 | "kind": { 29 | SchemaProps: spec.SchemaProps{ 30 | Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds", 31 | Type: []string{"string"}, 32 | Format: "", 33 | }, 34 | }, 35 | "apiVersion": { 36 | SchemaProps: spec.SchemaProps{ 37 | Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources", 38 | Type: []string{"string"}, 39 | Format: "", 40 | }, 41 | }, 42 | "metadata": { 43 | SchemaProps: spec.SchemaProps{ 44 | Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"), 45 | }, 46 | }, 47 | "spec": { 48 | SchemaProps: spec.SchemaProps{ 49 | Ref: ref("github.com/deislabs/smi-adapter-istio/pkg/apis/networking/v1alpha3.VirtualServiceSpec"), 50 | }, 51 | }, 52 | "status": { 53 | SchemaProps: spec.SchemaProps{ 54 | Ref: ref("github.com/deislabs/smi-adapter-istio/pkg/apis/networking/v1alpha3.VirtualServiceStatus"), 55 | }, 56 | }, 57 | }, 58 | }, 59 | }, 60 | Dependencies: []string{ 61 | "github.com/deislabs/smi-adapter-istio/pkg/apis/networking/v1alpha3.VirtualServiceSpec", "github.com/deislabs/smi-adapter-istio/pkg/apis/networking/v1alpha3.VirtualServiceStatus", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"}, 62 | } 63 | } 64 | 65 | func schema_pkg_apis_networking_v1alpha3_VirtualServiceSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { 66 | return common.OpenAPIDefinition{ 67 | Schema: spec.Schema{ 68 | SchemaProps: spec.SchemaProps{ 69 | Description: "VirtualServiceSpec defines the desired state of VirtualService", 70 | Properties: map[string]spec.Schema{}, 71 | }, 72 | }, 73 | Dependencies: []string{}, 74 | } 75 | } 76 | 77 | func schema_pkg_apis_networking_v1alpha3_VirtualServiceStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { 78 | return common.OpenAPIDefinition{ 79 | Schema: spec.Schema{ 80 | SchemaProps: spec.SchemaProps{ 81 | Description: "VirtualServiceStatus defines the observed state of VirtualService", 82 | Properties: map[string]spec.Schema{}, 83 | }, 84 | }, 85 | Dependencies: []string{}, 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /docs/smi-flagger/README.md: -------------------------------------------------------------------------------- 1 | # Using Flagger 2 | 3 | ## Setup 4 | 5 | ### Prerequisite 6 | 7 | * `kubectl` installed. 8 | * Working Kubernetes cluster. 9 | 10 | ### Install Istio 11 | 12 | Download manifests as mentioned in upstream [docs](https://istio.io/docs/setup/kubernetes/download/#download-and-prepare-for-the-installation). 13 | 14 | 15 | ```bash 16 | curl -L https://git.io/getLatestIstio | ISTIO_VERSION=1.1.6 sh - 17 | ``` 18 | 19 | Install Istio on Kubernetes 20 | 21 | ```bash 22 | cd istio-1.1.6 23 | kubectl apply -f install/kubernetes/istio-demo-auth.yaml 24 | ``` 25 | 26 | **NOTE**: Above apply might sometimes give errors like `unable to recognize "install/kubernetes/istio-demo-auth.yaml": no matches for kind "DestinationRule" in version "networking.istio.io/v1alpha3"`, this is most likely because the CRDs are not registered yet and apiserver will reconcile it. Try running the above `kubectl apply ...` again. 27 | 28 | ### Install SMI CRDs and Operator to work with Istio 29 | 30 | ```bash 31 | cd $GOPATH/src/github.com/servicemeshinterface/smi-adapter-istio/docs/smi-flagger 32 | kubectl apply -f https://raw.githubusercontent.com/servicemeshinterface/smi-adapter-istio/master/deploy/crds/crds.yaml 33 | kubectl apply -f https://raw.githubusercontent.com/servicemeshinterface/smi-adapter-istio/master/deploy/operator-and-rbac.yaml 34 | ``` 35 | 36 | ### Install Flagger 37 | 38 | ```bash 39 | kubectl apply -f manifests/00-install-flagger.yaml 40 | ``` 41 | 42 | ## Demo 43 | 44 | ### Deploy `v1` of Bookinfo application 45 | 46 | ```bash 47 | kubectl create ns smi-demo 48 | kubectl label namespace smi-demo istio-injection=enabled 49 | kubectl -n smi-demo apply -f manifests/00-bookinfo-setup.yaml 50 | kubectl -n smi-demo apply -f manifests/01-deploy-v1.yaml 51 | ``` 52 | 53 | ### Create the canary object 54 | 55 | ```bash 56 | kubectl -n smi-demo apply -f manifests/02-canary.yaml 57 | ``` 58 | 59 | ### Verify the application works 60 | 61 | Run following command to get the URL to acces the application. 62 | 63 | ```bash 64 | echo "http://$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')/productpage" 65 | ``` 66 | 67 | It generally looks like following: `http://52.168.69.51/productpage`. 68 | 69 | Visit above URL in the browser and you won't see any `ratings` since the `reviews v1` does not call `ratings` service at all. 70 | 71 | ### Deploy LoadTester 72 | 73 | Deploy the load testing service to generate traffic during the canary analysis: 74 | 75 | ```bash 76 | export REPO=https://raw.githubusercontent.com/weaveworks/flagger/master 77 | kubectl -n smi-demo apply -f ${REPO}/artifacts/loadtester/deployment.yaml 78 | kubectl -n smi-demo apply -f ${REPO}/artifacts/loadtester/service.yaml 79 | ``` 80 | 81 | ### Deploy `v2` of reviews application 82 | 83 | ```bash 84 | kubectl -n smi-demo set image deploy reviews reviews=istio/examples-bookinfo-reviews-v2:1.10.1 85 | ``` 86 | 87 | Once any changes are detected to the deployment it will be gradually scaled up. You can refresh the page to see that the traffic to new version is gradually increased. 88 | 89 | ### Verify successful deployment of v2 90 | 91 | Run following command to see that the deployment is progressing. Also meanwhile you can refresh browser to see that the traffic sent to version `v2` of the reviews micro-service is increasing. 92 | 93 | ```bash 94 | kubectl -n smi-demo describe canaries reviews 95 | ``` 96 | 97 | Once the output looks like following that means the newer version is successfully deployed. And all of the traffic will be sent to the newer version. 98 | 99 | ```console 100 | $ kubectl -n smi-demo get canaries 101 | NAME STATUS WEIGHT LASTTRANSITIONTIME 102 | reviews Succeeded 0 2019-05-16T14:45:15Z 103 | ``` 104 | 105 | Also you can observe that the TrafficSplit object is created. 106 | 107 | ```bash 108 | kubectl get trafficsplit 109 | ``` 110 | -------------------------------------------------------------------------------- /cmd/manager/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "runtime" 9 | 10 | "github.com/operator-framework/operator-sdk/pkg/k8sutil" 11 | "github.com/operator-framework/operator-sdk/pkg/leader" 12 | "github.com/operator-framework/operator-sdk/pkg/log/zap" 13 | "github.com/operator-framework/operator-sdk/pkg/metrics" 14 | "github.com/operator-framework/operator-sdk/pkg/restmapper" 15 | sdkVersion "github.com/operator-framework/operator-sdk/version" 16 | "github.com/spf13/pflag" 17 | v1 "k8s.io/api/core/v1" 18 | _ "k8s.io/client-go/plugin/pkg/client/auth" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 19 | "sigs.k8s.io/controller-runtime/pkg/client/config" 20 | "sigs.k8s.io/controller-runtime/pkg/manager" 21 | logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" 22 | "sigs.k8s.io/controller-runtime/pkg/runtime/signals" 23 | 24 | "github.com/deislabs/smi-adapter-istio/pkg/apis" 25 | "github.com/deislabs/smi-adapter-istio/pkg/controller" 26 | ) 27 | 28 | // Change below variables to serve metrics on different host or port. 29 | var ( 30 | metricsHost = "0.0.0.0" 31 | metricsPort int32 = 8383 32 | ) 33 | var log = logf.Log.WithName("cmd") 34 | 35 | func printVersion() { 36 | log.Info(fmt.Sprintf("Go Version: %s", runtime.Version())) 37 | log.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)) 38 | log.Info(fmt.Sprintf("Version of operator-sdk: %v", sdkVersion.Version)) 39 | } 40 | 41 | func main() { 42 | // Add the zap logger flag set to the CLI. The flag set must 43 | // be added before calling pflag.Parse(). 44 | pflag.CommandLine.AddFlagSet(zap.FlagSet()) 45 | 46 | // Add flags registered by imported packages (e.g. glog and 47 | // controller-runtime) 48 | pflag.CommandLine.AddGoFlagSet(flag.CommandLine) 49 | 50 | pflag.Parse() 51 | 52 | // Use a zap logr.Logger implementation. If none of the zap 53 | // flags are configured (or if the zap flag set is not being 54 | // used), this defaults to a production zap logger. 55 | // 56 | // The logger instantiated here can be changed to any logger 57 | // implementing the logr.Logger interface. This logger will 58 | // be propagated through the whole operator, generating 59 | // uniform and structured logs. 60 | logf.SetLogger(zap.Logger()) 61 | 62 | printVersion() 63 | 64 | namespace, err := k8sutil.GetWatchNamespace() 65 | if err != nil { 66 | log.Error(err, "Failed to get watch namespace") 67 | os.Exit(1) 68 | } 69 | 70 | // Get a config to talk to the apiserver 71 | cfg, err := config.GetConfig() 72 | if err != nil { 73 | log.Error(err, "") 74 | os.Exit(1) 75 | } 76 | 77 | ctx := context.TODO() 78 | 79 | // Become the leader before proceeding 80 | err = leader.Become(ctx, "smi-adapter-istio-lock") 81 | if err != nil { 82 | log.Error(err, "") 83 | os.Exit(1) 84 | } 85 | 86 | // Create a new Cmd to provide shared dependencies and start components 87 | mgr, err := manager.New(cfg, manager.Options{ 88 | Namespace: namespace, 89 | MapperProvider: restmapper.NewDynamicRESTMapper, 90 | MetricsBindAddress: fmt.Sprintf("%s:%d", metricsHost, metricsPort), 91 | }) 92 | if err != nil { 93 | log.Error(err, "") 94 | os.Exit(1) 95 | } 96 | 97 | log.Info("Registering Components.") 98 | 99 | // Setup Scheme for all resources 100 | if err := apis.AddToScheme(mgr.GetScheme()); err != nil { 101 | log.Error(err, "") 102 | os.Exit(1) 103 | } 104 | 105 | // Setup all Controllers 106 | if err := controller.AddToManager(mgr); err != nil { 107 | log.Error(err, "") 108 | os.Exit(1) 109 | } 110 | 111 | // Create Service object to expose the metrics port. 112 | _, err = metrics.CreateMetricsService(ctx, cfg, []v1.ServicePort{v1.ServicePort{Port: metricsPort}}) 113 | if err != nil { 114 | log.Info(err.Error()) 115 | } 116 | 117 | log.Info("Starting the Cmd.") 118 | 119 | // Start the Cmd 120 | if err := mgr.Start(signals.SetupSignalHandler()); err != nil { 121 | log.Error(err, "Manager exited non-zero") 122 | os.Exit(1) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /docs/smi-flagger/manifests/00-bookinfo-setup.yaml: -------------------------------------------------------------------------------- 1 | ################################################################################################## 2 | # Details service 3 | ################################################################################################## 4 | apiVersion: v1 5 | kind: Service 6 | metadata: 7 | name: details 8 | labels: 9 | app: details 10 | service: details 11 | spec: 12 | ports: 13 | - port: 9080 14 | name: http 15 | selector: 16 | app: details 17 | --- 18 | apiVersion: apps/v1 19 | kind: Deployment 20 | metadata: 21 | name: details-v1 22 | labels: 23 | app: details 24 | version: v1 25 | spec: 26 | selector: 27 | matchLabels: 28 | app: details 29 | version: v1 30 | replicas: 1 31 | template: 32 | metadata: 33 | labels: 34 | app: details 35 | version: v1 36 | spec: 37 | containers: 38 | - name: details 39 | image: istio/examples-bookinfo-details-v1:1.10.1 40 | imagePullPolicy: IfNotPresent 41 | ports: 42 | - containerPort: 9080 43 | --- 44 | ################################################################################################## 45 | # Ratings service 46 | ################################################################################################## 47 | apiVersion: v1 48 | kind: Service 49 | metadata: 50 | name: ratings 51 | labels: 52 | app: ratings 53 | service: ratings 54 | spec: 55 | ports: 56 | - port: 9080 57 | name: http 58 | selector: 59 | app: ratings 60 | --- 61 | apiVersion: apps/v1 62 | kind: Deployment 63 | metadata: 64 | name: ratings-v1 65 | labels: 66 | app: ratings 67 | version: v1 68 | spec: 69 | selector: 70 | matchLabels: 71 | app: ratings 72 | version: v1 73 | replicas: 1 74 | template: 75 | metadata: 76 | labels: 77 | app: ratings 78 | version: v1 79 | spec: 80 | containers: 81 | - name: ratings 82 | image: istio/examples-bookinfo-ratings-v1:1.10.1 83 | imagePullPolicy: IfNotPresent 84 | ports: 85 | - containerPort: 9080 86 | --- 87 | ################################################################################################## 88 | # Productpage services 89 | ################################################################################################## 90 | apiVersion: v1 91 | kind: Service 92 | metadata: 93 | name: productpage 94 | labels: 95 | app: productpage 96 | service: productpage 97 | spec: 98 | ports: 99 | - port: 9080 100 | name: http 101 | selector: 102 | app: productpage 103 | --- 104 | apiVersion: apps/v1 105 | kind: Deployment 106 | metadata: 107 | name: productpage-v1 108 | labels: 109 | app: productpage 110 | version: v1 111 | spec: 112 | selector: 113 | matchLabels: 114 | app: productpage 115 | version: v1 116 | replicas: 1 117 | template: 118 | metadata: 119 | labels: 120 | app: productpage 121 | version: v1 122 | spec: 123 | containers: 124 | - name: productpage 125 | image: istio/examples-bookinfo-productpage-v1:1.10.1 126 | imagePullPolicy: IfNotPresent 127 | ports: 128 | - containerPort: 9080 129 | --- 130 | ################################################################################################## 131 | # Gateway for services 132 | ################################################################################################## 133 | apiVersion: networking.istio.io/v1alpha3 134 | kind: Gateway 135 | metadata: 136 | name: bookinfo-gateway 137 | spec: 138 | selector: 139 | istio: ingressgateway # use istio default controller 140 | servers: 141 | - port: 142 | number: 80 143 | name: http 144 | protocol: HTTP 145 | hosts: 146 | - "*" 147 | --- 148 | apiVersion: networking.istio.io/v1alpha3 149 | kind: VirtualService 150 | metadata: 151 | name: bookinfo 152 | spec: 153 | hosts: 154 | - "*" 155 | gateways: 156 | - bookinfo-gateway 157 | http: 158 | - match: 159 | - uri: 160 | exact: /productpage 161 | - uri: 162 | exact: /login 163 | - uri: 164 | exact: /logout 165 | - uri: 166 | prefix: /api/v1/products 167 | route: 168 | - destination: 169 | host: productpage 170 | port: 171 | number: 9080 172 | -------------------------------------------------------------------------------- /docs/smi-trafficsplit/README.md: -------------------------------------------------------------------------------- 1 | # Using SMI spec TrafficSplit 2 | 3 | ## Setup 4 | 5 | ### Prerequisite 6 | 7 | * `kubectl` installed. 8 | * Working Kubernetes cluster. 9 | 10 | ### Install Istio 11 | 12 | Download manifests as mentioned in upstream [docs](https://istio.io/docs/setup/kubernetes/download/#download-and-prepare-for-the-installation). 13 | 14 | 15 | ```bash 16 | curl -L https://git.io/getLatestIstio | ISTIO_VERSION=1.1.6 sh - 17 | ``` 18 | 19 | Install Istio on Kubernetes 20 | 21 | ```bash 22 | cd istio-1.1.6 23 | kubectl apply -f install/kubernetes/istio-demo-auth.yaml 24 | ``` 25 | 26 | **NOTE**: Above apply might sometimes give errors like `unable to recognize "install/kubernetes/istio-demo-auth.yaml": no matches for kind "DestinationRule" in version "networking.istio.io/v1alpha3"`, this is most likely because the CRDs are not registered yet and apiserver will reconcile it. Try running the above `kubectl apply ...` again. 27 | 28 | ### Install SMI CRDs and Operator to work with Istio 29 | 30 | ```bash 31 | cd $GOPATH/src/github.com/servicemeshinterface/smi-adapter-istio/docs/smi-trafficsplit 32 | kubectl apply -f https://raw.githubusercontent.com/servicemeshinterface/smi-adapter-istio/master/deploy/crds/crds.yaml 33 | kubectl apply -f https://raw.githubusercontent.com/servicemeshinterface/smi-adapter-istio/master/deploy/operator-and-rbac.yaml 34 | ``` 35 | 36 | ## Demo 37 | 38 | ### Deploy `v1` of Bookinfo application 39 | 40 | ```bash 41 | kubectl create ns smi-demo 42 | kubectl label namespace smi-demo istio-injection=enabled 43 | kubectl -n smi-demo apply -f manifests/00-bookinfo-setup.yaml 44 | kubectl -n smi-demo apply -f manifests/01-deploy-v1.yaml 45 | ``` 46 | 47 | ### Verify the application works 48 | 49 | Run following command to get the URL to acces the application. 50 | 51 | ```bash 52 | echo "http://$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')/productpage" 53 | ``` 54 | 55 | It generally looks like following: `http://52.168.69.51/productpage`. 56 | 57 | Visit above URL in the browser and you won't see any `ratings` since the `reviews v1` does not call `ratings` service at all. 58 | 59 | ### Create TrafficSplit for `v1` 60 | 61 | ```bash 62 | kubectl -n smi-demo apply -f manifests/02-traffic-split-1000-0.yaml 63 | ``` 64 | 65 | ### Deploy `v2` of `reviews` application 66 | 67 | ``` 68 | kubectl -n smi-demo apply -f manifests/03-deploy-v2.yaml 69 | ``` 70 | 71 | Verify that the traffic is still only sent to version `v1` of the application even though there is `v2` version deployed. Until you perform the next step no traffic will be sent to the `v2`. 72 | 73 | ### Update TrafficSplit to send `10%` of traffic 74 | 75 | ```bash 76 | kubectl -n smi-demo apply -f manifests/04-traffic-split-900-100.yaml 77 | ``` 78 | 79 | Refresh browser multiple times to see that the traffic is sent to version `v1` of reviews micro-service `90%` of the time and to `v2`, `10%` of the time. Verify that application works fine and increase the traffic for `v2`. 80 | 81 | ### Update TrafficSplit to send `25%` of traffic 82 | 83 | ```bash 84 | kubectl -n smi-demo apply -f manifests/05-traffic-split-750-250.yaml 85 | ``` 86 | 87 | Refresh browser multiple times to see that the traffic is sent to version `v1` of reviews micro-service `75%` of the time and to `v2`, `25%` of the time. Verify that application works fine and increase the traffic for `v2`. 88 | 89 | ### Update TrafficSplit to send `80%` of traffic 90 | 91 | ```bash 92 | kubectl -n smi-demo apply -f manifests/06-traffic-split-200-800.yaml 93 | ``` 94 | 95 | Refresh browser multiple times to see that the lesser traffic is sent to version `v1` of reviews micro-service which is `20%` of the time and to `v2`, `80%` of the time. Verify that application still works fine and now is the right time to send entire traffic to `v2`. 96 | 97 | ### Update TrafficSplit to send `100%` of traffic 98 | 99 | ```bash 100 | kubectl -n smi-demo apply -f manifests/07-traffic-split-0-1000.yaml 101 | ``` 102 | 103 | Refresh browser and you will see that the traffic is sent only to version `v2` of the reviews application. 104 | 105 | ### Delete old version of application 106 | 107 | ```bash 108 | kubectl -n smi-demo delete -f manifests/01-deploy-v1.yaml 109 | ``` 110 | 111 | ### Get rid of the TrafficSplit object 112 | 113 | ```bash 114 | kubectl -n smi-demo delete trafficsplit reviews-rollout 115 | ``` 116 | -------------------------------------------------------------------------------- /test/manual/00-bookinfo-setup.yaml: -------------------------------------------------------------------------------- 1 | ################################################################################################## 2 | # Details service 3 | ################################################################################################## 4 | apiVersion: v1 5 | kind: Service 6 | metadata: 7 | name: details 8 | labels: 9 | app: details 10 | service: details 11 | spec: 12 | ports: 13 | - port: 9080 14 | name: http 15 | selector: 16 | app: details 17 | --- 18 | apiVersion: extensions/v1beta1 19 | kind: Deployment 20 | metadata: 21 | name: details-v1 22 | labels: 23 | app: details 24 | version: v1 25 | spec: 26 | replicas: 1 27 | template: 28 | metadata: 29 | labels: 30 | app: details 31 | version: v1 32 | spec: 33 | containers: 34 | - name: details 35 | image: istio/examples-bookinfo-details-v1:1.10.1 36 | imagePullPolicy: IfNotPresent 37 | ports: 38 | - containerPort: 9080 39 | --- 40 | ################################################################################################## 41 | # Ratings service 42 | ################################################################################################## 43 | apiVersion: v1 44 | kind: Service 45 | metadata: 46 | name: ratings 47 | labels: 48 | app: ratings 49 | service: ratings 50 | spec: 51 | ports: 52 | - port: 9080 53 | name: http 54 | selector: 55 | app: ratings 56 | --- 57 | apiVersion: extensions/v1beta1 58 | kind: Deployment 59 | metadata: 60 | name: ratings-v1 61 | labels: 62 | app: ratings 63 | version: v1 64 | spec: 65 | replicas: 1 66 | template: 67 | metadata: 68 | labels: 69 | app: ratings 70 | version: v1 71 | spec: 72 | containers: 73 | - name: ratings 74 | image: istio/examples-bookinfo-ratings-v1:1.10.1 75 | imagePullPolicy: IfNotPresent 76 | ports: 77 | - containerPort: 9080 78 | --- 79 | ################################################################################################## 80 | # Productpage services 81 | ################################################################################################## 82 | apiVersion: v1 83 | kind: Service 84 | metadata: 85 | name: productpage 86 | labels: 87 | app: productpage 88 | service: productpage 89 | spec: 90 | ports: 91 | - port: 9080 92 | name: http 93 | selector: 94 | app: productpage 95 | --- 96 | apiVersion: extensions/v1beta1 97 | kind: Deployment 98 | metadata: 99 | name: productpage-v1 100 | labels: 101 | app: productpage 102 | version: v1 103 | spec: 104 | replicas: 1 105 | template: 106 | metadata: 107 | labels: 108 | app: productpage 109 | version: v1 110 | spec: 111 | containers: 112 | - name: productpage 113 | image: istio/examples-bookinfo-productpage-v1:1.10.1 114 | imagePullPolicy: IfNotPresent 115 | ports: 116 | - containerPort: 9080 117 | --- 118 | ################################################################################################## 119 | # Reviews service 120 | ################################################################################################## 121 | apiVersion: v1 122 | kind: Service 123 | metadata: 124 | name: reviews 125 | labels: 126 | app: reviews 127 | service: reviews 128 | spec: 129 | ports: 130 | - port: 9080 131 | name: http 132 | selector: 133 | app: reviews 134 | --- 135 | ################################################################################################## 136 | # Gateway for services 137 | ################################################################################################## 138 | apiVersion: networking.istio.io/v1alpha3 139 | kind: Gateway 140 | metadata: 141 | name: bookinfo-gateway 142 | spec: 143 | selector: 144 | istio: ingressgateway # use istio default controller 145 | servers: 146 | - port: 147 | number: 80 148 | name: http 149 | protocol: HTTP 150 | hosts: 151 | - "*" 152 | --- 153 | apiVersion: networking.istio.io/v1alpha3 154 | kind: VirtualService 155 | metadata: 156 | name: bookinfo 157 | spec: 158 | hosts: 159 | - "*" 160 | gateways: 161 | - bookinfo-gateway 162 | http: 163 | - match: 164 | - uri: 165 | exact: /productpage 166 | - uri: 167 | exact: /login 168 | - uri: 169 | exact: /logout 170 | - uri: 171 | prefix: /api/v1/products 172 | route: 173 | - destination: 174 | host: productpage 175 | port: 176 | number: 9080 177 | -------------------------------------------------------------------------------- /test/e2e/trafficsplit_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | goctx "context" 5 | "fmt" 6 | "testing" 7 | "time" 8 | 9 | splitv1alpha2 "github.com/deislabs/smi-sdk-go/pkg/apis/split/v1alpha2" 10 | framework "github.com/operator-framework/operator-sdk/pkg/test" 11 | "github.com/operator-framework/operator-sdk/pkg/test/e2eutil" 12 | apierrors "k8s.io/apimachinery/pkg/api/errors" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/util/wait" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | 17 | "github.com/deislabs/smi-adapter-istio/pkg/apis" 18 | networkingv1alpha3 "github.com/deislabs/smi-adapter-istio/pkg/apis/networking/v1alpha3" 19 | ) 20 | 21 | var ( 22 | retryInterval = time.Second * 5 23 | timeout = time.Second * 60 24 | cleanupRetryInterval = time.Second * 1 25 | cleanupTimeout = time.Second * 5 26 | ) 27 | 28 | // deploy operator, create traffic split, verify virtualservice was created 29 | func TestTrafficSplit(t *testing.T) { 30 | 31 | tsList := &splitv1alpha2.TrafficSplitList{} 32 | err := framework.AddToFrameworkScheme(apis.AddToScheme, tsList) 33 | if err != nil { 34 | t.Fatalf("failed to add custom resource scheme to framework: %v", err) 35 | } 36 | 37 | // run subtests 38 | t.Run("trafficsplit-group", func(t *testing.T) { 39 | t.Run("Cluster", TrafficSplitCluster) 40 | }) 41 | 42 | } 43 | 44 | func TrafficSplitCluster(t *testing.T) { 45 | t.Parallel() 46 | ctx := framework.NewTestCtx(t) 47 | defer ctx.Cleanup() 48 | 49 | err := ctx.InitializeClusterResources(&framework.CleanupOptions{TestContext: ctx, Timeout: cleanupTimeout, RetryInterval: cleanupRetryInterval}) 50 | if err != nil { 51 | t.Fatalf("failed to initialize cluster resources: %v", err) 52 | } 53 | 54 | t.Log("Initialized cluster resources") 55 | 56 | namespace, err := ctx.GetNamespace() 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | t.Log("namespace: " + namespace) 61 | // get global framework variables 62 | f := framework.Global 63 | 64 | // wait for smi-istio-adapter operator to be ready 65 | err = e2eutil.WaitForDeployment(t, f.KubeClient, namespace, "smi-adapter-istio", 1, retryInterval, timeout) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | t.Log("wait completed") 70 | 71 | if err = TrafficSplitCreateTest(t, f, ctx); err != nil { 72 | t.Error(err) 73 | } 74 | 75 | } 76 | 77 | func TrafficSplitCreateTest(t *testing.T, f *framework.Framework, ctx *framework.TestCtx) error { 78 | 79 | namespace, err := ctx.GetNamespace() 80 | if err != nil { 81 | return fmt.Errorf("could not get namespace: %v", err) 82 | } 83 | 84 | // create custom resource 85 | trafficSplit := &splitv1alpha2.TrafficSplit{ 86 | TypeMeta: metav1.TypeMeta{ 87 | Kind: "TrafficSplit", 88 | APIVersion: "split.smi-spec.io/v1alpha2", 89 | }, 90 | ObjectMeta: metav1.ObjectMeta{ 91 | Name: "test-service-rollout", 92 | Namespace: namespace, 93 | }, 94 | Spec: splitv1alpha2.TrafficSplitSpec{ 95 | Service: "test-service", 96 | Backends: []splitv1alpha2.TrafficSplitBackend{ 97 | splitv1alpha2.TrafficSplitBackend{ 98 | Service: "test-service-v1", 99 | Weight: 1, 100 | }, 101 | splitv1alpha2.TrafficSplitBackend{ 102 | Service: "test-service-v2", 103 | Weight: 0, 104 | }, 105 | }, 106 | }, 107 | } 108 | 109 | t.Log("Ensure virtual service is not already running") 110 | virtualService := &networkingv1alpha3.VirtualService{} 111 | vsName := "test-service-rollout-vs" 112 | err = f.Client.Get(goctx.TODO(), client.ObjectKey{Name: vsName, Namespace: namespace}, virtualService) 113 | if !apierrors.IsNotFound(err) { 114 | return fmt.Errorf("virtual service %s already exists in namespace: %s", vsName, namespace) 115 | } 116 | 117 | // use TestCtx's create helper to create the traffic split object and 118 | // add a cleanup function for the new object 119 | err = f.Client.Create(goctx.TODO(), trafficSplit, &framework.CleanupOptions{TestContext: ctx, Timeout: cleanupTimeout, RetryInterval: cleanupRetryInterval}) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | waitErr := wait.Poll(retryInterval, timeout, func() (bool, error) { 125 | err := f.Client.Get(goctx.TODO(), client.ObjectKey{Name: vsName, Namespace: namespace}, virtualService) 126 | if err != nil && apierrors.IsNotFound(err) { 127 | return false, nil 128 | } else if err != nil { 129 | return false, err 130 | } 131 | return true, nil 132 | }) 133 | if waitErr != nil { 134 | return fmt.Errorf("Expected virtual service '%s' to exist in namespace '%s' but does not exist: %s", vsName, namespace, waitErr) 135 | 136 | } 137 | 138 | return nil 139 | } 140 | -------------------------------------------------------------------------------- /docs/smi-trafficsplit/manifests/00-bookinfo-setup.yaml: -------------------------------------------------------------------------------- 1 | ################################################################################################## 2 | # Details service 3 | ################################################################################################## 4 | apiVersion: v1 5 | kind: Service 6 | metadata: 7 | name: details 8 | labels: 9 | app: details 10 | service: details 11 | spec: 12 | ports: 13 | - port: 9080 14 | name: http 15 | selector: 16 | app: details 17 | --- 18 | apiVersion: apps/v1 19 | kind: Deployment 20 | metadata: 21 | name: details-v1 22 | labels: 23 | app: details 24 | version: v1 25 | spec: 26 | selector: 27 | matchLabels: 28 | app: details 29 | version: v1 30 | replicas: 1 31 | template: 32 | metadata: 33 | labels: 34 | app: details 35 | version: v1 36 | spec: 37 | containers: 38 | - name: details 39 | image: istio/examples-bookinfo-details-v1:1.10.1 40 | imagePullPolicy: IfNotPresent 41 | ports: 42 | - containerPort: 9080 43 | --- 44 | ################################################################################################## 45 | # Ratings service 46 | ################################################################################################## 47 | apiVersion: v1 48 | kind: Service 49 | metadata: 50 | name: ratings 51 | labels: 52 | app: ratings 53 | service: ratings 54 | spec: 55 | ports: 56 | - port: 9080 57 | name: http 58 | selector: 59 | app: ratings 60 | --- 61 | apiVersion: apps/v1 62 | kind: Deployment 63 | metadata: 64 | name: ratings-v1 65 | labels: 66 | app: ratings 67 | version: v1 68 | spec: 69 | selector: 70 | matchLabels: 71 | app: ratings 72 | version: v1 73 | replicas: 1 74 | template: 75 | metadata: 76 | labels: 77 | app: ratings 78 | version: v1 79 | spec: 80 | containers: 81 | - name: ratings 82 | image: istio/examples-bookinfo-ratings-v1:1.10.1 83 | imagePullPolicy: IfNotPresent 84 | ports: 85 | - containerPort: 9080 86 | --- 87 | ################################################################################################## 88 | # Productpage services 89 | ################################################################################################## 90 | apiVersion: v1 91 | kind: Service 92 | metadata: 93 | name: productpage 94 | labels: 95 | app: productpage 96 | service: productpage 97 | spec: 98 | ports: 99 | - port: 9080 100 | name: http 101 | selector: 102 | app: productpage 103 | --- 104 | apiVersion: apps/v1 105 | kind: Deployment 106 | metadata: 107 | name: productpage-v1 108 | labels: 109 | app: productpage 110 | version: v1 111 | spec: 112 | selector: 113 | matchLabels: 114 | app: productpage 115 | version: v1 116 | replicas: 1 117 | template: 118 | metadata: 119 | labels: 120 | app: productpage 121 | version: v1 122 | spec: 123 | containers: 124 | - name: productpage 125 | image: istio/examples-bookinfo-productpage-v1:1.10.1 126 | imagePullPolicy: IfNotPresent 127 | ports: 128 | - containerPort: 9080 129 | --- 130 | ################################################################################################## 131 | # Reviews service 132 | ################################################################################################## 133 | apiVersion: v1 134 | kind: Service 135 | metadata: 136 | name: reviews 137 | labels: 138 | app: reviews 139 | service: reviews 140 | spec: 141 | ports: 142 | - port: 9080 143 | name: http 144 | selector: 145 | app: reviews 146 | --- 147 | ################################################################################################## 148 | # Gateway for services 149 | ################################################################################################## 150 | apiVersion: networking.istio.io/v1alpha3 151 | kind: Gateway 152 | metadata: 153 | name: bookinfo-gateway 154 | spec: 155 | selector: 156 | istio: ingressgateway # use istio default controller 157 | servers: 158 | - port: 159 | number: 80 160 | name: http 161 | protocol: HTTP 162 | hosts: 163 | - "*" 164 | --- 165 | apiVersion: networking.istio.io/v1alpha3 166 | kind: VirtualService 167 | metadata: 168 | name: bookinfo 169 | spec: 170 | hosts: 171 | - "*" 172 | gateways: 173 | - bookinfo-gateway 174 | http: 175 | - match: 176 | - uri: 177 | exact: /productpage 178 | - uri: 179 | exact: /login 180 | - uri: 181 | exact: /logout 182 | - uri: 183 | prefix: /api/v1/products 184 | route: 185 | - destination: 186 | host: productpage 187 | port: 188 | number: 9080 189 | -------------------------------------------------------------------------------- /pkg/controller/traffictarget/traffictarget_controller_test.go: -------------------------------------------------------------------------------- 1 | package traffictarget 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | targetv1alpha1 "github.com/deislabs/smi-sdk-go/pkg/apis/access/v1alpha1" 8 | apierrors "k8s.io/apimachinery/pkg/api/errors" 9 | "k8s.io/apimachinery/pkg/api/meta" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | apitypes "k8s.io/apimachinery/pkg/types" 13 | "k8s.io/client-go/kubernetes/scheme" 14 | "k8s.io/client-go/rest" 15 | "k8s.io/client-go/tools/record" 16 | "sigs.k8s.io/controller-runtime/pkg/cache" 17 | "sigs.k8s.io/controller-runtime/pkg/client" 18 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 19 | "sigs.k8s.io/controller-runtime/pkg/manager" 20 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 21 | "sigs.k8s.io/controller-runtime/pkg/webhook" 22 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission/types" 23 | 24 | networkingv1alpha3 "github.com/deislabs/smi-adapter-istio/pkg/apis/networking/v1alpha3" 25 | ) 26 | 27 | func TestNewReconciler(t *testing.T) { 28 | mgr := FakeManager{} 29 | r := newReconciler(mgr) 30 | var _ reconcile.Reconciler = r // test r is reconcile.Reconciler 31 | } 32 | 33 | func TestReconcile_ErrorIsNotFound(t *testing.T) { 34 | trafficTargetObj := &targetv1alpha1.TrafficTarget{ 35 | ObjectMeta: metav1.ObjectMeta{ 36 | Name: "traffic-target-name", 37 | Namespace: "default", 38 | }, 39 | Specs: []targetv1alpha1.TrafficTargetSpec{}, 40 | } 41 | objs := []runtime.Object{} 42 | cl := fake.NewFakeClient(objs...) 43 | s := scheme.Scheme 44 | s.AddKnownTypes(targetv1alpha1.SchemeGroupVersion, trafficTargetObj) 45 | reconcileTrafficTarget := &ReconcileTrafficTarget{client: cl, scheme: s} 46 | req := reconcile.Request{NamespacedName: apitypes.NamespacedName{ 47 | Namespace: "default", 48 | Name: "traffic-target-name"}, 49 | } 50 | _, err := reconcileTrafficTarget.Reconcile(req) 51 | if err != nil { 52 | t.Errorf("Expected no err, got %v", err) 53 | } 54 | } 55 | 56 | func TestReconcile(t *testing.T) { 57 | trafficTargetObj := &targetv1alpha1.TrafficTarget{ 58 | ObjectMeta: metav1.ObjectMeta{ 59 | Name: "traffic-target-name", 60 | Namespace: "default", 61 | }, 62 | Specs: []targetv1alpha1.TrafficTargetSpec{}, 63 | } 64 | virtualServiceObj := &networkingv1alpha3.VirtualService{ 65 | ObjectMeta: metav1.ObjectMeta{ 66 | Name: "traffic-target-name-vs", 67 | Namespace: "default", 68 | Labels: map[string]string{"traffic-target": "traffic-target-name"}, 69 | }, 70 | Spec: networkingv1alpha3.VirtualServiceSpec{}, 71 | } 72 | // Register the object in the fake client. 73 | objs := []runtime.Object{ 74 | trafficTargetObj, 75 | } 76 | s := scheme.Scheme 77 | s.AddKnownTypes(targetv1alpha1.SchemeGroupVersion, trafficTargetObj) 78 | s.AddKnownTypes(networkingv1alpha3.SchemeGroupVersion, virtualServiceObj) 79 | 80 | cl := fake.NewFakeClient(objs...) 81 | err := cl.Get(context.TODO(), apitypes.NamespacedName{ 82 | Namespace: "default", 83 | Name: "traffic-target-name-vs"}, 84 | virtualServiceObj) 85 | if !apierrors.IsNotFound(err) { 86 | t.Fatalf("expected virtual service not to exist, got err: %s", err) 87 | } 88 | 89 | reconcileTrafficTarget := &ReconcileTrafficTarget{client: cl, scheme: s} 90 | req := reconcile.Request{NamespacedName: apitypes.NamespacedName{ 91 | Namespace: "default", 92 | Name: "traffic-target-name-vs"}, 93 | } 94 | 95 | _, err = reconcileTrafficTarget.Reconcile(req) 96 | if err != nil { 97 | t.Errorf("Expected no err, got %s", err) 98 | } 99 | 100 | err = cl.Get(context.TODO(), apitypes.NamespacedName{ 101 | Namespace: "default", 102 | Name: "traffic-target-name"}, 103 | trafficTargetObj) 104 | if err != nil { 105 | t.Errorf("Expected virtual service object to be created successfully, but was not: %s", err) 106 | } 107 | } 108 | 109 | type FakeManager struct{} 110 | 111 | func (fm FakeManager) Add(manager.Runnable) error { return nil } 112 | func (fm FakeManager) SetFields(interface{}) error { return nil } 113 | func (fm FakeManager) Start(<-chan struct{}) error { return nil } 114 | func (fm FakeManager) GetConfig() *rest.Config { return &rest.Config{} } 115 | func (fm FakeManager) GetScheme() *runtime.Scheme { return &runtime.Scheme{} } 116 | func (fm FakeManager) GetAdmissionDecoder() types.Decoder { return nil } 117 | func (fm FakeManager) GetClient() client.Client { return nil } 118 | func (fm FakeManager) GetFieldIndexer() client.FieldIndexer { return nil } 119 | func (fm FakeManager) GetCache() cache.Cache { return nil } 120 | func (fm FakeManager) GetRecorder(name string) record.EventRecorder { return nil } 121 | func (fm FakeManager) GetRESTMapper() meta.RESTMapper { return nil } 122 | func (fm FakeManager) GetAPIReader() client.Reader { return nil } 123 | func (fm FakeManager) GetWebhookServer() *webhook.Server { return &webhook.Server{} } 124 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Support Channels 4 | 5 | Whether you are a user or contributor, official support channels include: 6 | 7 | - [Issues](https://github.com/servicemeshinterface/smi-spec/issues) 8 | - #smi Slack channel in the [CNCF Slack](https://cloud-native.slack.com) 9 | 10 | ## Sign Your Work 11 | 12 | The sign-off is a simple line at the end of the explanation for a commit. All 13 | commits needs to be signed. Your signature certifies that you wrote the patch or 14 | otherwise have the right to contribute the material. The rules are pretty simple, 15 | if you can certify the below (from [developercertificate.org](https://developercertificate.org/)): 16 | 17 | ```text 18 | Developer Certificate of Origin 19 | Version 1.1 20 | 21 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 22 | 1 Letterman Drive 23 | Suite D4700 24 | San Francisco, CA, 94129 25 | 26 | Everyone is permitted to copy and distribute verbatim copies of this 27 | license document, but changing it is not allowed. 28 | 29 | Developer's Certificate of Origin 1.1 30 | 31 | By making a contribution to this project, I certify that: 32 | 33 | (a) The contribution was created in whole or in part by me and I 34 | have the right to submit it under the open source license 35 | indicated in the file; or 36 | 37 | (b) The contribution is based upon previous work that, to the best 38 | of my knowledge, is covered under an appropriate open source 39 | license and I have the right under that license to submit that 40 | work with modifications, whether created in whole or in part 41 | by me, under the same open source license (unless I am 42 | permitted to submit under a different license), as indicated 43 | in the file; or 44 | 45 | (c) The contribution was provided directly to me by some other 46 | person who certified (a), (b) or (c) and I have not modified 47 | it. 48 | 49 | (d) I understand and agree that this project and the contribution 50 | are public and that a record of the contribution (including all 51 | personal information I submit with it, including my sign-off) is 52 | maintained indefinitely and may be redistributed consistent with 53 | this project or the open source license(s) involved. 54 | ``` 55 | 56 | Then you just add a line to every git commit message: 57 | 58 | ```text 59 | Signed-off-by: Joe Smith 60 | ``` 61 | 62 | Use your real name (sorry, no pseudonyms or anonymous contributions.) 63 | 64 | If you set your `user.name` and `user.email` git configs, you can sign your 65 | commit automatically with `git commit -s`. 66 | 67 | Note: If your git config information is set properly then viewing the 68 | `git log` information for your commit will look something like this: 69 | 70 | ```text 71 | Author: Joe Smith 72 | Date: Thu Feb 2 11:41:15 2018 -0800 73 | 74 | Update README 75 | 76 | Signed-off-by: Joe Smith 77 | ``` 78 | 79 | Notice the `Author` and `Signed-off-by` lines match. If they don't 80 | your PR will be rejected by the automated DCO check. 81 | 82 | # Community Code of Conduct 83 | 84 | Service Mesh Interface follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). 85 | 86 | ## Project Governance 87 | 88 | Project maintainership is outlined in the [GOVERNANCE](GOVERNANCE.md) file. 89 | 90 | ## Developer setup 91 | 92 | ### Prerequisites 93 | - Running Kubernetes cluster (If using Minikube, Minikube machine with atleast `4GB` memory). 94 | - `operator-sdk` installed, download from [here](https://github.com/operator-framework/operator-sdk/releases). 95 | - Follow [instructions in istio documentation](https://istio.io/docs/setup/kubernetes/download/#download-and-prepare-for-the-installation) to download istio. Once you are the root of the downloaded directory. Run following command to install Istio. 96 | ```bash 97 | kubectl apply -f install/kubernetes/istio-demo-auth.yaml 98 | ``` 99 | Verify the istio is running fine as mentioned [here](https://istio.io/docs/setup/kubernetes/install/kubernetes/#verifying-the-installation). 100 | 101 | ### Build Operator Image 102 | 103 | #### If using Minikube, use the following instructions: 104 | ```bash 105 | eval $(minikube docker-env) 106 | operator-sdk build devimage 107 | ``` 108 | By exporting all the minikube docker environment variables locally the build happens in the virtual machine directly. 109 | 110 | Deploy image using: 111 | ```bash 112 | kubectl apply -R -f deploy/ 113 | cat deploy/kubernetes-manifests.yaml | sed 's|servicemeshinterface/smi-adapter-istio:latest|devimage|g'| sed 's|imagePullPolicy: Always|imagePullPolicy: Never|g' | kubectl apply -f - 114 | ``` 115 | 116 | To rebuild and redeploy after making changes to the code, run the following commands: 117 | ```bash 118 | eval $(minikube docker-env) 119 | operator-sdk build devimage 120 | kubectl -n istio-system delete pod -l 'name=smi-adapter-istio' 121 | ``` 122 | ## Build and Push Operator Image 123 | If you're not using Minikube, you'll want to build and push the image to a remote container registry. Choose the container image name for the operator and build: 124 | ```bash 125 | export OPERATOR_IMAGE=docker.io//smi-adapter-istio:latest 126 | make 127 | ``` 128 | 129 | Push to your container registry: 130 | ```bash 131 | make push 132 | ``` 133 | 134 | ### Developing Using Tilt 135 | - Install [Tilt](https://docs.tilt.dev/install.html) 136 | - Replace `servicemeshinterface/smi-adapter-istio` in [manifest](deploy/kubernetes-manifests.yaml) and [Tiltfile](Tiltfile) with your own image name i.e. `/smi-adapter-istio` 137 | - Run `$ tilt up` in project directory 138 | 139 | This will build and deploy the operator to Kubernetes and you can iterate and watch changes get updated in the cluster! 140 | -------------------------------------------------------------------------------- /pkg/apis/networking/v1alpha3/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | // Code generated by deepcopy-gen. DO NOT EDIT. 4 | 5 | package v1alpha3 6 | 7 | import ( 8 | runtime "k8s.io/apimachinery/pkg/runtime" 9 | ) 10 | 11 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 12 | func (in *Destination) DeepCopyInto(out *Destination) { 13 | *out = *in 14 | return 15 | } 16 | 17 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Destination. 18 | func (in *Destination) DeepCopy() *Destination { 19 | if in == nil { 20 | return nil 21 | } 22 | out := new(Destination) 23 | in.DeepCopyInto(out) 24 | return out 25 | } 26 | 27 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 28 | func (in *HTTPRoute) DeepCopyInto(out *HTTPRoute) { 29 | *out = *in 30 | if in.Route != nil { 31 | in, out := &in.Route, &out.Route 32 | *out = make([]*HTTPRouteDestination, len(*in)) 33 | for i := range *in { 34 | if (*in)[i] != nil { 35 | in, out := &(*in)[i], &(*out)[i] 36 | *out = new(HTTPRouteDestination) 37 | (*in).DeepCopyInto(*out) 38 | } 39 | } 40 | } 41 | return 42 | } 43 | 44 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRoute. 45 | func (in *HTTPRoute) DeepCopy() *HTTPRoute { 46 | if in == nil { 47 | return nil 48 | } 49 | out := new(HTTPRoute) 50 | in.DeepCopyInto(out) 51 | return out 52 | } 53 | 54 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 55 | func (in *HTTPRouteDestination) DeepCopyInto(out *HTTPRouteDestination) { 56 | *out = *in 57 | if in.Destination != nil { 58 | in, out := &in.Destination, &out.Destination 59 | *out = new(Destination) 60 | **out = **in 61 | } 62 | return 63 | } 64 | 65 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HTTPRouteDestination. 66 | func (in *HTTPRouteDestination) DeepCopy() *HTTPRouteDestination { 67 | if in == nil { 68 | return nil 69 | } 70 | out := new(HTTPRouteDestination) 71 | in.DeepCopyInto(out) 72 | return out 73 | } 74 | 75 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 76 | func (in *VirtualService) DeepCopyInto(out *VirtualService) { 77 | *out = *in 78 | out.TypeMeta = in.TypeMeta 79 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 80 | in.Spec.DeepCopyInto(&out.Spec) 81 | out.Status = in.Status 82 | return 83 | } 84 | 85 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualService. 86 | func (in *VirtualService) DeepCopy() *VirtualService { 87 | if in == nil { 88 | return nil 89 | } 90 | out := new(VirtualService) 91 | in.DeepCopyInto(out) 92 | return out 93 | } 94 | 95 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 96 | func (in *VirtualService) DeepCopyObject() runtime.Object { 97 | if c := in.DeepCopy(); c != nil { 98 | return c 99 | } 100 | return nil 101 | } 102 | 103 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 104 | func (in *VirtualServiceList) DeepCopyInto(out *VirtualServiceList) { 105 | *out = *in 106 | out.TypeMeta = in.TypeMeta 107 | out.ListMeta = in.ListMeta 108 | if in.Items != nil { 109 | in, out := &in.Items, &out.Items 110 | *out = make([]VirtualService, len(*in)) 111 | for i := range *in { 112 | (*in)[i].DeepCopyInto(&(*out)[i]) 113 | } 114 | } 115 | return 116 | } 117 | 118 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualServiceList. 119 | func (in *VirtualServiceList) DeepCopy() *VirtualServiceList { 120 | if in == nil { 121 | return nil 122 | } 123 | out := new(VirtualServiceList) 124 | in.DeepCopyInto(out) 125 | return out 126 | } 127 | 128 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 129 | func (in *VirtualServiceList) DeepCopyObject() runtime.Object { 130 | if c := in.DeepCopy(); c != nil { 131 | return c 132 | } 133 | return nil 134 | } 135 | 136 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 137 | func (in *VirtualServiceSpec) DeepCopyInto(out *VirtualServiceSpec) { 138 | *out = *in 139 | if in.Hosts != nil { 140 | in, out := &in.Hosts, &out.Hosts 141 | *out = make([]string, len(*in)) 142 | copy(*out, *in) 143 | } 144 | if in.Gateways != nil { 145 | in, out := &in.Gateways, &out.Gateways 146 | *out = make([]string, len(*in)) 147 | copy(*out, *in) 148 | } 149 | if in.Http != nil { 150 | in, out := &in.Http, &out.Http 151 | *out = make([]*HTTPRoute, len(*in)) 152 | for i := range *in { 153 | if (*in)[i] != nil { 154 | in, out := &(*in)[i], &(*out)[i] 155 | *out = new(HTTPRoute) 156 | (*in).DeepCopyInto(*out) 157 | } 158 | } 159 | } 160 | return 161 | } 162 | 163 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualServiceSpec. 164 | func (in *VirtualServiceSpec) DeepCopy() *VirtualServiceSpec { 165 | if in == nil { 166 | return nil 167 | } 168 | out := new(VirtualServiceSpec) 169 | in.DeepCopyInto(out) 170 | return out 171 | } 172 | 173 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 174 | func (in *VirtualServiceStatus) DeepCopyInto(out *VirtualServiceStatus) { 175 | *out = *in 176 | return 177 | } 178 | 179 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VirtualServiceStatus. 180 | func (in *VirtualServiceStatus) DeepCopy() *VirtualServiceStatus { 181 | if in == nil { 182 | return nil 183 | } 184 | out := new(VirtualServiceStatus) 185 | in.DeepCopyInto(out) 186 | return out 187 | } 188 | -------------------------------------------------------------------------------- /test/e2e/traffictarget_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | goctx "context" 5 | "fmt" 6 | "testing" 7 | 8 | accessv1alpha1 "github.com/deislabs/smi-sdk-go/pkg/apis/access/v1alpha1" 9 | specsv1alpha1 "github.com/deislabs/smi-sdk-go/pkg/apis/specs/v1alpha1" 10 | framework "github.com/operator-framework/operator-sdk/pkg/test" 11 | "github.com/operator-framework/operator-sdk/pkg/test/e2eutil" 12 | apierrors "k8s.io/apimachinery/pkg/api/errors" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/util/wait" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | 17 | "github.com/deislabs/smi-adapter-istio/pkg/apis" 18 | rbacv1alpha1 "github.com/deislabs/smi-adapter-istio/pkg/apis/rbac/v1alpha1" 19 | ) 20 | 21 | // deploy operator, create traffic target, verify servicerole and servicerolebind were created 22 | func TestTrafficTarget(t *testing.T) { 23 | 24 | ttList := &accessv1alpha1.TrafficTarget{} 25 | err := framework.AddToFrameworkScheme(apis.AddToScheme, ttList) 26 | if err != nil { 27 | t.Fatalf("failed to add custom resource scheme to framework: %v", err) 28 | } 29 | 30 | // run subtests 31 | t.Run("traffictarget-group", func(t *testing.T) { 32 | t.Run("Cluster", TrafficTargetCluster) 33 | }) 34 | 35 | } 36 | 37 | func TrafficTargetCluster(t *testing.T) { 38 | t.Parallel() 39 | ctx := framework.NewTestCtx(t) 40 | defer ctx.Cleanup() 41 | 42 | err := ctx.InitializeClusterResources(&framework.CleanupOptions{TestContext: ctx, Timeout: cleanupTimeout, RetryInterval: cleanupRetryInterval}) 43 | if err != nil { 44 | t.Fatalf("failed to initialize cluster resources: %v", err) 45 | } 46 | 47 | t.Log("Initialized cluster resources") 48 | 49 | namespace, err := ctx.GetNamespace() 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | t.Log("namespace: " + namespace) 54 | // get global framework variables 55 | f := framework.Global 56 | 57 | // wait for smi-istio-adapter operator to be ready 58 | err = e2eutil.WaitForDeployment(t, f.KubeClient, namespace, "smi-adapter-istio", 1, retryInterval, timeout) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | t.Log("wait completed") 63 | 64 | if err = TrafficTargetCreateTest(t, f, ctx); err != nil { 65 | t.Error(err) 66 | } 67 | 68 | } 69 | 70 | func TrafficTargetCreateTest(t *testing.T, f *framework.Framework, ctx *framework.TestCtx) error { 71 | 72 | namespace, err := ctx.GetNamespace() 73 | if err != nil { 74 | return fmt.Errorf("could not get namespace: %v", err) 75 | } 76 | ttName := "test-traffic-target" 77 | httpRouteGroup := &specsv1alpha1.HTTPRouteGroup{ 78 | TypeMeta: metav1.TypeMeta{ 79 | Kind: "HTTPRouteGroup", 80 | APIVersion: "specs.smi-spec.io/v1alpha1", 81 | }, 82 | ObjectMeta: metav1.ObjectMeta{ 83 | Name: "the-routes", 84 | Namespace: namespace, 85 | }, 86 | Matches: []specsv1alpha1.HTTPMatch{{ 87 | Name: "testGet", 88 | PathRegex: "/test", 89 | Methods: []string{"Get"}, 90 | }}, 91 | } 92 | // create custom resource 93 | trafficTarget := &accessv1alpha1.TrafficTarget{ 94 | TypeMeta: metav1.TypeMeta{ 95 | Kind: "TrafficTarget", 96 | APIVersion: "access.smi-spec.io/v1alpha1", 97 | }, 98 | ObjectMeta: metav1.ObjectMeta{ 99 | Name: ttName, 100 | Namespace: namespace, 101 | }, 102 | Destination: accessv1alpha1.IdentityBindingSubject{ 103 | Kind: "ServiceAccount", 104 | Name: "service-a", 105 | Namespace: namespace, 106 | Port: "8080", 107 | }, 108 | Specs: []accessv1alpha1.TrafficTargetSpec{{ 109 | Kind: "HTTPRouteGroup", 110 | Name: "the-routes", 111 | Matches: []string{"testGet"}, 112 | }}, 113 | Sources: []accessv1alpha1.IdentityBindingSubject{ 114 | { 115 | Kind: "ServiceAccount", 116 | Name: "service-b", 117 | Namespace: namespace, 118 | }, 119 | }, 120 | } 121 | 122 | t.Log("Ensure servicerole and servicerolebinding not already running") 123 | serviceRoleBinding := &rbacv1alpha1.ServiceRoleBinding{} 124 | serviceRole := &rbacv1alpha1.ServiceRole{} 125 | err = f.Client.Get(goctx.TODO(), client.ObjectKey{Name: ttName, Namespace: namespace}, serviceRole) 126 | if !apierrors.IsNotFound(err) { 127 | return fmt.Errorf("Service role %s already exists in namespace: %s", ttName, namespace) 128 | } 129 | err = f.Client.Get(goctx.TODO(), client.ObjectKey{Name: ttName, Namespace: namespace}, serviceRoleBinding) 130 | if !apierrors.IsNotFound(err) { 131 | return fmt.Errorf("Service role binding %s already exists in namespace: %s", ttName, namespace) 132 | } 133 | 134 | // use TestCtx's create helper to create the specs and traffic target objects and 135 | // add a cleanup function for the new object 136 | err = f.Client.Create(goctx.TODO(), httpRouteGroup, &framework.CleanupOptions{TestContext: ctx, Timeout: cleanupTimeout, RetryInterval: cleanupRetryInterval}) 137 | if err != nil { 138 | return err 139 | } 140 | err = f.Client.Create(goctx.TODO(), trafficTarget, &framework.CleanupOptions{TestContext: ctx, Timeout: cleanupTimeout, RetryInterval: cleanupRetryInterval}) 141 | if err != nil { 142 | return err 143 | } 144 | 145 | waitErr := wait.Poll(retryInterval, timeout, func() (bool, error) { 146 | err := f.Client.Get(goctx.TODO(), client.ObjectKey{Name: ttName, Namespace: namespace}, serviceRole) 147 | if err != nil && apierrors.IsNotFound(err) { 148 | return false, nil 149 | } else if err != nil { 150 | return false, err 151 | } 152 | return true, nil 153 | }) 154 | if waitErr != nil { 155 | return fmt.Errorf("Expected service role '%s' to exist in namespace '%s' but does not exist: %s", ttName, namespace, waitErr) 156 | 157 | } 158 | waitErr = wait.Poll(retryInterval, timeout, func() (bool, error) { 159 | err = f.Client.Get(goctx.TODO(), client.ObjectKey{Name: ttName, Namespace: namespace}, serviceRoleBinding) 160 | if err != nil && apierrors.IsNotFound(err) { 161 | return false, nil 162 | } else if err != nil { 163 | return false, err 164 | } 165 | return true, nil 166 | }) 167 | if waitErr != nil { 168 | return fmt.Errorf("Expected service role binding '%s' to exist in namespace '%s' but does not exist: %s", ttName, namespace, waitErr) 169 | 170 | } 171 | 172 | return nil 173 | } 174 | -------------------------------------------------------------------------------- /pkg/controller/trafficsplit/trafficsplit_controller_test.go: -------------------------------------------------------------------------------- 1 | package trafficsplit 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | splitv1alpha2 "github.com/deislabs/smi-sdk-go/pkg/apis/split/v1alpha2" 8 | apierrors "k8s.io/apimachinery/pkg/api/errors" 9 | "k8s.io/apimachinery/pkg/api/meta" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/runtime" 12 | apitypes "k8s.io/apimachinery/pkg/types" 13 | "k8s.io/client-go/kubernetes/scheme" 14 | "k8s.io/client-go/rest" 15 | "k8s.io/client-go/tools/record" 16 | "sigs.k8s.io/controller-runtime/pkg/cache" 17 | "sigs.k8s.io/controller-runtime/pkg/client" 18 | "sigs.k8s.io/controller-runtime/pkg/client/fake" 19 | "sigs.k8s.io/controller-runtime/pkg/manager" 20 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 21 | "sigs.k8s.io/controller-runtime/pkg/webhook" 22 | "sigs.k8s.io/controller-runtime/pkg/webhook/admission/types" 23 | 24 | networkingv1alpha3 "github.com/deislabs/smi-adapter-istio/pkg/apis/networking/v1alpha3" 25 | ) 26 | 27 | func TestNewReconciler(t *testing.T) { 28 | mgr := FakeManager{} 29 | r := newReconciler(mgr) 30 | var _ reconcile.Reconciler = r // test r is reconcile.Reconciler 31 | } 32 | 33 | func TestReconcile_ErrorIsNotFound(t *testing.T) { 34 | trafficSplitObj := &splitv1alpha2.TrafficSplit{ 35 | ObjectMeta: metav1.ObjectMeta{ 36 | Name: "traffic-split-name", 37 | Namespace: "default", 38 | }, 39 | Spec: splitv1alpha2.TrafficSplitSpec{}, 40 | } 41 | objs := []runtime.Object{} 42 | cl := fake.NewFakeClient(objs...) 43 | s := scheme.Scheme 44 | s.AddKnownTypes(splitv1alpha2.SchemeGroupVersion, trafficSplitObj) 45 | reconcileTrafficSplit := &ReconcileTrafficSplit{client: cl, scheme: s} 46 | req := reconcile.Request{NamespacedName: apitypes.NamespacedName{ 47 | Namespace: "default", 48 | Name: "traffic-split-name"}, 49 | } 50 | _, err := reconcileTrafficSplit.Reconcile(req) 51 | if err != nil { 52 | t.Errorf("Expected no err, got %v", err) 53 | } 54 | } 55 | 56 | func TestReconcile(t *testing.T) { 57 | trafficSplitObj := &splitv1alpha2.TrafficSplit{ 58 | ObjectMeta: metav1.ObjectMeta{ 59 | Name: "traffic-split-name", 60 | Namespace: "default", 61 | }, 62 | Spec: splitv1alpha2.TrafficSplitSpec{}, 63 | } 64 | virtualServiceObj := &networkingv1alpha3.VirtualService{ 65 | ObjectMeta: metav1.ObjectMeta{ 66 | Name: "traffic-split-name-vs", 67 | Namespace: "default", 68 | Labels: map[string]string{"traffic-split": "traffic-split-name"}, 69 | }, 70 | Spec: networkingv1alpha3.VirtualServiceSpec{}, 71 | } 72 | // Register the object in the fake client. 73 | objs := []runtime.Object{ 74 | trafficSplitObj, 75 | } 76 | s := scheme.Scheme 77 | s.AddKnownTypes(splitv1alpha2.SchemeGroupVersion, trafficSplitObj) 78 | s.AddKnownTypes(networkingv1alpha3.SchemeGroupVersion, virtualServiceObj) 79 | 80 | cl := fake.NewFakeClient(objs...) 81 | err := cl.Get(context.TODO(), apitypes.NamespacedName{ 82 | Namespace: "default", 83 | Name: "traffic-split-name-vs"}, 84 | virtualServiceObj) 85 | if !apierrors.IsNotFound(err) { 86 | t.Fatalf("expected virtual service not to exist, got err: %s", err) 87 | } 88 | 89 | reconcileTrafficSplit := &ReconcileTrafficSplit{client: cl, scheme: s} 90 | req := reconcile.Request{NamespacedName: apitypes.NamespacedName{ 91 | Namespace: "default", 92 | Name: "traffic-split-name"}, 93 | } 94 | 95 | _, err = reconcileTrafficSplit.Reconcile(req) 96 | if err != nil { 97 | t.Errorf("Expected no err, got %s", err) 98 | } 99 | 100 | err = cl.Get(context.TODO(), apitypes.NamespacedName{ 101 | Namespace: "default", 102 | Name: "traffic-split-name-vs"}, 103 | virtualServiceObj) 104 | if err != nil { 105 | t.Errorf("Expected virtual service object to be created successfully, but was not: %s", err) 106 | } 107 | } 108 | 109 | func TestWeightToPercent(t *testing.T) { 110 | backends := []splitv1alpha2.TrafficSplitBackend{ 111 | splitv1alpha2.TrafficSplitBackend{Service: "a", Weight: 1000}, 112 | splitv1alpha2.TrafficSplitBackend{Service: "b", Weight: 2000}, 113 | } 114 | weights := weightToPercent(backends) 115 | if weights["a"] != 33 { 116 | t.Errorf("Expected Service a to have percent weight of 33 but got %v", weights["a"]) 117 | } 118 | if weights["b"] != 67 { 119 | t.Errorf("Expected Service b to have percent weight of 67 but got %v", weights["b"]) 120 | } 121 | 122 | backends = []splitv1alpha2.TrafficSplitBackend{ 123 | splitv1alpha2.TrafficSplitBackend{Service: "a", Weight: 1000}, 124 | splitv1alpha2.TrafficSplitBackend{Service: "b", Weight: 1000}, 125 | splitv1alpha2.TrafficSplitBackend{Service: "c", Weight: 1000}, 126 | } 127 | weights = weightToPercent(backends) 128 | if weights["a"] != 33 { 129 | t.Errorf("Expected Service a to have percent weight of 33 but got %v", weights["a"]) 130 | } 131 | if weights["b"] != 33 { 132 | t.Errorf("Expected Service b to have percent weight of 33 but got %v", weights["b"]) 133 | } 134 | if weights["c"] != 34 { 135 | t.Errorf("Expected Service b to have percent weight of 34 but got %v", weights["c"]) 136 | } 137 | 138 | backends = []splitv1alpha2.TrafficSplitBackend{ 139 | splitv1alpha2.TrafficSplitBackend{Service: "a", Weight: 20}, 140 | splitv1alpha2.TrafficSplitBackend{Service: "b", Weight: 30}, 141 | splitv1alpha2.TrafficSplitBackend{Service: "c", Weight: 50}, 142 | } 143 | weights = weightToPercent(backends) 144 | if weights["a"] != 20 { 145 | t.Errorf("Expected Service a to have percent weight of 20 but got %v", weights["a"]) 146 | } 147 | if weights["b"] != 30 { 148 | t.Errorf("Expected Service b to have percent weight of 30 but got %v", weights["b"]) 149 | } 150 | if weights["c"] != 50 { 151 | t.Errorf("Expected Service b to have percent weight of 50 but got %v", weights["c"]) 152 | } 153 | 154 | backends = []splitv1alpha2.TrafficSplitBackend{ 155 | splitv1alpha2.TrafficSplitBackend{Service: "a", Weight: 5}, 156 | splitv1alpha2.TrafficSplitBackend{Service: "b", Weight: 10}, 157 | splitv1alpha2.TrafficSplitBackend{Service: "c", Weight: 20}, 158 | } 159 | weights = weightToPercent(backends) 160 | if weights["a"] != 14 { 161 | t.Errorf("Expected Service a to have percent weight of 14 but got %v", weights["a"]) 162 | } 163 | if weights["b"] != 29 { 164 | t.Errorf("Expected Service b to have percent weight of 29 but got %v", weights["b"]) 165 | } 166 | if weights["c"] != 57 { 167 | t.Errorf("Expected Service b to have percent weight of 57 but got %v", weights["c"]) 168 | } 169 | } 170 | 171 | type FakeManager struct{} 172 | 173 | func (fm FakeManager) Add(manager.Runnable) error { return nil } 174 | func (fm FakeManager) SetFields(interface{}) error { return nil } 175 | func (fm FakeManager) Start(<-chan struct{}) error { return nil } 176 | func (fm FakeManager) GetConfig() *rest.Config { return &rest.Config{} } 177 | func (fm FakeManager) GetScheme() *runtime.Scheme { return &runtime.Scheme{} } 178 | func (fm FakeManager) GetAdmissionDecoder() types.Decoder { return nil } 179 | func (fm FakeManager) GetClient() client.Client { return nil } 180 | func (fm FakeManager) GetFieldIndexer() client.FieldIndexer { return nil } 181 | func (fm FakeManager) GetCache() cache.Cache { return nil } 182 | func (fm FakeManager) GetRecorder(name string) record.EventRecorder { return nil } 183 | func (fm FakeManager) GetRESTMapper() meta.RESTMapper { return nil } 184 | func (fm FakeManager) GetAPIReader() client.Reader { return nil } 185 | func (fm FakeManager) GetWebhookServer() *webhook.Server { return &webhook.Server{} } 186 | -------------------------------------------------------------------------------- /pkg/controller/trafficsplit/trafficsplit_controller.go: -------------------------------------------------------------------------------- 1 | package trafficsplit 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "math" 7 | 8 | splitv1alpha2 "github.com/deislabs/smi-sdk-go/pkg/apis/split/v1alpha2" 9 | "github.com/go-logr/logr" 10 | "github.com/google/go-cmp/cmp" 11 | "k8s.io/apimachinery/pkg/api/errors" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "k8s.io/apimachinery/pkg/types" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | "sigs.k8s.io/controller-runtime/pkg/controller" 17 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 18 | "sigs.k8s.io/controller-runtime/pkg/handler" 19 | "sigs.k8s.io/controller-runtime/pkg/manager" 20 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 21 | logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" 22 | "sigs.k8s.io/controller-runtime/pkg/source" 23 | 24 | networkingv1alpha3 "github.com/deislabs/smi-adapter-istio/pkg/apis/networking/v1alpha3" 25 | ) 26 | 27 | var log = logf.Log.WithName("controller_trafficsplit") 28 | 29 | // Add creates a new TrafficSplit Controller and adds it to the Manager. The Manager will set fields on the Controller 30 | // and Start it when the Manager is Started. 31 | func Add(mgr manager.Manager) error { 32 | return add(mgr, newReconciler(mgr)) 33 | } 34 | 35 | // newReconciler returns a new reconcile.Reconciler 36 | func newReconciler(mgr manager.Manager) reconcile.Reconciler { 37 | return &ReconcileTrafficSplit{client: mgr.GetClient(), scheme: mgr.GetScheme()} 38 | } 39 | 40 | // add adds a new Controller to mgr with r as the reconcile.Reconciler 41 | func add(mgr manager.Manager, r reconcile.Reconciler) error { 42 | // Create a new controller 43 | c, err := controller.New("trafficsplit-controller", mgr, controller.Options{Reconciler: r}) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | // Watch for changes to primary resource TrafficSplit 49 | err = c.Watch(&source.Kind{Type: &splitv1alpha2.TrafficSplit{}}, &handler.EnqueueRequestForObject{}) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | // Watch for changes to secondary resource VirtualService 55 | return c.Watch(&source.Kind{Type: &networkingv1alpha3.VirtualService{}}, &handler.EnqueueRequestForOwner{ 56 | IsController: true, 57 | OwnerType: &splitv1alpha2.TrafficSplit{}, 58 | }) 59 | } 60 | 61 | var _ reconcile.Reconciler = &ReconcileTrafficSplit{} 62 | 63 | // ReconcileTrafficSplit reconciles a TrafficSplit object 64 | type ReconcileTrafficSplit struct { 65 | // This client, initialized using mgr.Client() above, is a split client 66 | // that reads objects from the cache and writes to the apiserver 67 | client client.Client 68 | scheme *runtime.Scheme 69 | } 70 | 71 | // Reconcile reads that state of the cluster for a TrafficSplit object and makes changes based on the state read 72 | // and what is in the TrafficSplit.Spec 73 | // Note: 74 | // The Controller will requeue the Request to be processed again if the returned error is non-nil or 75 | // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. 76 | func (r *ReconcileTrafficSplit) Reconcile(request reconcile.Request) (reconcile.Result, error) { 77 | reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) 78 | reqLogger.Info("Reconciling TrafficSplit") 79 | 80 | // Fetch the TrafficSplit instance 81 | trafficSplit := &splitv1alpha2.TrafficSplit{} 82 | err := r.client.Get(context.TODO(), request.NamespacedName, trafficSplit) 83 | if err != nil { 84 | if errors.IsNotFound(err) { 85 | // Request object not found, could have been deleted after reconcile request. 86 | // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. 87 | // Return and don't requeue 88 | reqLogger.Info("TrafficSplit object not found.") 89 | return reconcile.Result{}, nil 90 | } 91 | // Error reading the object - requeue the request. 92 | reqLogger.Error(err, "Failed to get TrafficSplit. Request will be requeued.") 93 | return reconcile.Result{}, err 94 | } 95 | 96 | return r.reconcileVirtualService(trafficSplit, reqLogger) 97 | } 98 | 99 | func (r *ReconcileTrafficSplit) reconcileVirtualService(trafficSplit *splitv1alpha2.TrafficSplit, 100 | reqLogger logr.Logger) (reconcile.Result, error) { 101 | // Define a new VirtualService object 102 | vs := newVSForCR(trafficSplit) 103 | 104 | // Set TrafficSplit instance as the owner and controller 105 | if err := controllerutil.SetControllerReference(trafficSplit, vs, r.scheme); err != nil { 106 | return reconcile.Result{}, err 107 | } 108 | 109 | // Check if this VS already exists 110 | found := &networkingv1alpha3.VirtualService{} 111 | err := r.client.Get(context.TODO(), types.NamespacedName{Name: vs.Name, Namespace: vs.Namespace}, found) 112 | 113 | // Create VS 114 | if err != nil && errors.IsNotFound(err) { 115 | reqLogger.Info("Creating a new VirtualService", "VirtualService.Namespace", vs.Namespace, 116 | "VirtualService.Name", vs.Name) 117 | err = r.client.Create(context.TODO(), vs) 118 | if err != nil { 119 | return reconcile.Result{}, err 120 | } 121 | 122 | // VirtualService created successfully - don't requeue 123 | return reconcile.Result{}, nil 124 | } else if err != nil { 125 | reqLogger.Error(err, "Failed to get VirtualService.", "VirtualService.Namespace", vs.Namespace, 126 | "VirtualService.Name", vs.Name) 127 | return reconcile.Result{}, err 128 | } 129 | 130 | // Update VS 131 | if diff := cmp.Diff(vs.Spec, found.Spec); diff != "" { 132 | reqLogger.Info("Updating VirtualService", "VirtualService.Namespace", vs.Namespace, 133 | "VirtualService.Name", vs.Name) 134 | clone := found.DeepCopy() 135 | clone.Spec = vs.Spec 136 | err = r.client.Update(context.TODO(), clone) 137 | if err != nil { 138 | return reconcile.Result{}, err 139 | } 140 | } 141 | 142 | return reconcile.Result{}, nil 143 | } 144 | 145 | //weightToPercent takes TrafficSplitBackends and maps a service to a weight 146 | // in the form of an integer from 1 - 100. The sum of weights must be 100. The last service 147 | // will be adjusted so that the total weight of the map returned at the end is equal to 100. 148 | func weightToPercent(backends []splitv1alpha2.TrafficSplitBackend) map[string]int { 149 | weights := map[string]int{} 150 | totalWeight := 0 151 | for _, b := range backends { 152 | totalWeight = totalWeight + int(b.Weight) 153 | weights[b.Service] = 0 154 | } 155 | 156 | if totalWeight == 0 { 157 | return weights 158 | } 159 | 160 | totalPercent := 0 161 | for i, b := range backends { 162 | w := (float64(b.Weight) / float64(totalWeight)) * 100 163 | per := math.Round(float64(w)) 164 | percent := int(per) 165 | if i == len(backends)-1 { 166 | percent = 100 - totalPercent 167 | } 168 | weights[b.Service] = percent 169 | totalPercent = totalPercent + percent 170 | } 171 | return weights 172 | } 173 | 174 | // newVSForCR returns a VirtualService with the same name/namespace as the cr 175 | func newVSForCR(cr *splitv1alpha2.TrafficSplit) *networkingv1alpha3.VirtualService { 176 | labels := map[string]string{ 177 | "traffic-split": cr.Name, 178 | } 179 | 180 | var backends []*networkingv1alpha3.HTTPRouteDestination 181 | weights := weightToPercent(cr.Spec.Backends) 182 | 183 | for _, b := range cr.Spec.Backends { 184 | r := &networkingv1alpha3.HTTPRouteDestination{ 185 | Destination: &networkingv1alpha3.Destination{Host: b.Service}, 186 | Weight: int32(weights[b.Service]), 187 | } 188 | 189 | backends = append(backends, r) 190 | } 191 | 192 | gatewaysStr := cr.ObjectMeta.Annotations["VirtualService.v1alpha3.networking.istio.io/spec.gateways"] 193 | var gateways []string 194 | _ = json.Unmarshal([]byte(gatewaysStr), &gateways) 195 | 196 | return &networkingv1alpha3.VirtualService{ 197 | ObjectMeta: metav1.ObjectMeta{ 198 | Name: cr.Name + "-vs", 199 | Namespace: cr.Namespace, 200 | Labels: labels, 201 | }, 202 | 203 | Spec: networkingv1alpha3.VirtualServiceSpec{ 204 | Hosts: []string{cr.Spec.Service}, 205 | Gateways: gateways, 206 | 207 | Http: []*networkingv1alpha3.HTTPRoute{ 208 | { 209 | Route: backends, 210 | }, 211 | }, 212 | }, 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /docs/smi-traffictarget/manifests/configs.yml: -------------------------------------------------------------------------------- 1 | ################################################################################################## 2 | # Details service 3 | ################################################################################################## 4 | apiVersion: v1 5 | kind: Service 6 | metadata: 7 | name: details 8 | labels: 9 | app: details 10 | service: details 11 | spec: 12 | ports: 13 | - port: 9080 14 | name: http 15 | selector: 16 | app: details 17 | --- 18 | apiVersion: v1 19 | kind: ServiceAccount 20 | metadata: 21 | name: details-v1 22 | --- 23 | apiVersion: apps/v1 24 | kind: Deployment 25 | metadata: 26 | name: details-v1 27 | labels: 28 | app: details 29 | version: v1 30 | spec: 31 | selector: 32 | matchLabels: 33 | app: details 34 | version: v1 35 | replicas: 1 36 | template: 37 | metadata: 38 | labels: 39 | app: details 40 | version: v1 41 | spec: 42 | serviceAccountName: details-v1 43 | containers: 44 | - name: details 45 | image: istio/examples-bookinfo-details-v1:1.13.0 46 | imagePullPolicy: IfNotPresent 47 | ports: 48 | - containerPort: 9080 49 | --- 50 | ################################################################################################## 51 | # Ratings service 52 | ################################################################################################## 53 | apiVersion: v1 54 | kind: Service 55 | metadata: 56 | name: ratings 57 | labels: 58 | app: ratings 59 | service: ratings 60 | spec: 61 | ports: 62 | - port: 9080 63 | name: http 64 | selector: 65 | app: ratings 66 | --- 67 | --- 68 | apiVersion: v1 69 | kind: ServiceAccount 70 | metadata: 71 | name: ratings-v1 72 | --- 73 | apiVersion: apps/v1 74 | kind: Deployment 75 | metadata: 76 | name: ratings-v1 77 | labels: 78 | app: ratings 79 | version: v1 80 | spec: 81 | selector: 82 | matchLabels: 83 | app: ratings 84 | version: v1 85 | replicas: 1 86 | template: 87 | metadata: 88 | labels: 89 | app: ratings 90 | version: v1 91 | spec: 92 | serviceAccountName: ratings-v1 93 | containers: 94 | - name: ratings 95 | image: istio/examples-bookinfo-ratings-v1:1.13.0 96 | imagePullPolicy: IfNotPresent 97 | ports: 98 | - containerPort: 9080 99 | --- 100 | ################################################################################################## 101 | # Reviews service 102 | ################################################################################################## 103 | apiVersion: v1 104 | kind: Service 105 | metadata: 106 | name: reviews 107 | labels: 108 | app: reviews 109 | service: reviews 110 | spec: 111 | ports: 112 | - port: 9080 113 | name: http 114 | selector: 115 | app: reviews 116 | --- 117 | apiVersion: v1 118 | kind: ServiceAccount 119 | metadata: 120 | name: reviews-v1 121 | --- 122 | apiVersion: apps/v1 123 | kind: Deployment 124 | metadata: 125 | name: reviews-v1 126 | labels: 127 | app: reviews 128 | version: v1 129 | spec: 130 | selector: 131 | matchLabels: 132 | app: reviews 133 | version: v1 134 | replicas: 1 135 | template: 136 | metadata: 137 | labels: 138 | app: reviews 139 | version: v1 140 | spec: 141 | serviceAccountName: reviews-v1 142 | containers: 143 | - name: reviews 144 | image: istio/examples-bookinfo-reviews-v1:1.13.0 145 | imagePullPolicy: IfNotPresent 146 | ports: 147 | - containerPort: 9080 148 | --- 149 | apiVersion: v1 150 | kind: ServiceAccount 151 | metadata: 152 | name: reviews-v2 153 | --- 154 | apiVersion: apps/v1 155 | kind: Deployment 156 | metadata: 157 | name: reviews-v2 158 | labels: 159 | app: reviews 160 | version: v2 161 | spec: 162 | selector: 163 | matchLabels: 164 | app: reviews 165 | version: v2 166 | replicas: 1 167 | template: 168 | metadata: 169 | labels: 170 | app: reviews 171 | version: v2 172 | spec: 173 | serviceAccountName: reviews-v2 174 | containers: 175 | - name: reviews 176 | image: istio/examples-bookinfo-reviews-v2:1.13.0 177 | imagePullPolicy: IfNotPresent 178 | ports: 179 | - containerPort: 9080 180 | --- 181 | apiVersion: v1 182 | kind: ServiceAccount 183 | metadata: 184 | name: reviews-v3 185 | --- 186 | apiVersion: apps/v1 187 | kind: Deployment 188 | metadata: 189 | name: reviews-v3 190 | labels: 191 | app: reviews 192 | version: v3 193 | spec: 194 | selector: 195 | matchLabels: 196 | app: reviews 197 | version: v3 198 | replicas: 1 199 | template: 200 | metadata: 201 | labels: 202 | app: reviews 203 | version: v3 204 | spec: 205 | serviceAccountName: reviews-v3 206 | containers: 207 | - name: reviews 208 | image: istio/examples-bookinfo-reviews-v3:1.13.0 209 | imagePullPolicy: IfNotPresent 210 | ports: 211 | - containerPort: 9080 212 | --- 213 | ################################################################################################## 214 | # Productpage services 215 | ################################################################################################## 216 | apiVersion: v1 217 | kind: Service 218 | metadata: 219 | name: productpage 220 | labels: 221 | app: productpage 222 | service: productpage 223 | spec: 224 | ports: 225 | - port: 9080 226 | name: http 227 | selector: 228 | app: productpage 229 | --- 230 | apiVersion: v1 231 | kind: ServiceAccount 232 | metadata: 233 | name: productpage-v1 234 | --- 235 | apiVersion: apps/v1 236 | kind: Deployment 237 | metadata: 238 | name: productpage-v1 239 | labels: 240 | app: productpage 241 | version: v1 242 | spec: 243 | selector: 244 | matchLabels: 245 | app: productpage 246 | version: v1 247 | replicas: 1 248 | template: 249 | metadata: 250 | labels: 251 | app: productpage 252 | version: v1 253 | spec: 254 | serviceAccountName: productpage-v1 255 | containers: 256 | - name: productpage 257 | image: istio/examples-bookinfo-productpage-v1:1.13.0 258 | imagePullPolicy: IfNotPresent 259 | ports: 260 | - containerPort: 9080 261 | --- 262 | apiVersion: networking.istio.io/v1alpha3 263 | kind: Gateway 264 | metadata: 265 | name: bookinfo-gateway 266 | spec: 267 | selector: 268 | istio: ingressgateway # use istio default controller 269 | servers: 270 | - port: 271 | number: 80 272 | name: http 273 | protocol: HTTP 274 | hosts: 275 | - "*" 276 | --- 277 | apiVersion: networking.istio.io/v1alpha3 278 | kind: VirtualService 279 | metadata: 280 | name: bookinfo 281 | spec: 282 | hosts: 283 | - "*" 284 | gateways: 285 | - bookinfo-gateway 286 | http: 287 | - match: 288 | - uri: 289 | exact: /productpage 290 | - uri: 291 | exact: /login 292 | - uri: 293 | exact: /logout 294 | - uri: 295 | prefix: /api/v1/products 296 | route: 297 | - destination: 298 | host: productpage 299 | port: 300 | number: 9080 301 | --- 302 | apiVersion: networking.istio.io/v1alpha3 303 | kind: DestinationRule 304 | metadata: 305 | name: productpage 306 | spec: 307 | host: productpage 308 | trafficPolicy: 309 | tls: 310 | mode: ISTIO_MUTUAL 311 | subsets: 312 | - name: v1 313 | labels: 314 | version: v1 315 | --- 316 | apiVersion: networking.istio.io/v1alpha3 317 | kind: DestinationRule 318 | metadata: 319 | name: reviews 320 | spec: 321 | host: reviews 322 | trafficPolicy: 323 | tls: 324 | mode: ISTIO_MUTUAL 325 | subsets: 326 | - name: v1 327 | labels: 328 | version: v1 329 | - name: v2 330 | labels: 331 | version: v2 332 | - name: v3 333 | labels: 334 | version: v3 335 | --- 336 | apiVersion: networking.istio.io/v1alpha3 337 | kind: DestinationRule 338 | metadata: 339 | name: ratings 340 | spec: 341 | host: ratings 342 | trafficPolicy: 343 | tls: 344 | mode: ISTIO_MUTUAL 345 | subsets: 346 | - name: v1 347 | labels: 348 | version: v1 349 | - name: v2 350 | labels: 351 | version: v2 352 | - name: v2-mysql 353 | labels: 354 | version: v2-mysql 355 | - name: v2-mysql-vm 356 | labels: 357 | version: v2-mysql-vm 358 | --- 359 | apiVersion: networking.istio.io/v1alpha3 360 | kind: DestinationRule 361 | metadata: 362 | name: details 363 | spec: 364 | host: details 365 | trafficPolicy: 366 | tls: 367 | mode: ISTIO_MUTUAL 368 | subsets: 369 | - name: v1 370 | labels: 371 | version: v1 372 | - name: v2 373 | labels: 374 | version: v2 375 | --- 376 | -------------------------------------------------------------------------------- /docs/smi-flagger/manifests/00-install-flagger.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1beta1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: canaries.flagger.app 6 | annotations: 7 | helm.sh/resource-policy: keep 8 | spec: 9 | group: flagger.app 10 | version: v1alpha3 11 | versions: 12 | - name: v1alpha3 13 | served: true 14 | storage: true 15 | - name: v1alpha2 16 | served: true 17 | storage: false 18 | - name: v1alpha1 19 | served: true 20 | storage: false 21 | names: 22 | plural: canaries 23 | singular: canary 24 | kind: Canary 25 | categories: 26 | - all 27 | scope: Namespaced 28 | subresources: 29 | status: {} 30 | additionalPrinterColumns: 31 | - name: Status 32 | type: string 33 | JSONPath: .status.phase 34 | - name: Weight 35 | type: string 36 | JSONPath: .status.canaryWeight 37 | - name: LastTransitionTime 38 | type: string 39 | JSONPath: .status.lastTransitionTime 40 | validation: 41 | openAPIV3Schema: 42 | properties: 43 | spec: 44 | required: 45 | - targetRef 46 | - service 47 | - canaryAnalysis 48 | properties: 49 | progressDeadlineSeconds: 50 | type: number 51 | targetRef: 52 | type: object 53 | required: ['apiVersion', 'kind', 'name'] 54 | properties: 55 | apiVersion: 56 | type: string 57 | kind: 58 | type: string 59 | name: 60 | type: string 61 | autoscalerRef: 62 | anyOf: 63 | - type: string 64 | - type: object 65 | required: ['apiVersion', 'kind', 'name'] 66 | properties: 67 | apiVersion: 68 | type: string 69 | kind: 70 | type: string 71 | name: 72 | type: string 73 | ingressRef: 74 | anyOf: 75 | - type: string 76 | - type: object 77 | required: ['apiVersion', 'kind', 'name'] 78 | properties: 79 | apiVersion: 80 | type: string 81 | kind: 82 | type: string 83 | name: 84 | type: string 85 | service: 86 | type: object 87 | required: ['port'] 88 | properties: 89 | port: 90 | type: number 91 | portName: 92 | type: string 93 | meshName: 94 | type: string 95 | timeout: 96 | type: string 97 | skipAnalysis: 98 | type: boolean 99 | canaryAnalysis: 100 | properties: 101 | interval: 102 | type: string 103 | pattern: "^[0-9]+(m|s)" 104 | iterations: 105 | type: number 106 | threshold: 107 | type: number 108 | maxWeight: 109 | type: number 110 | stepWeight: 111 | type: number 112 | metrics: 113 | type: array 114 | properties: 115 | items: 116 | type: object 117 | required: ['name', 'threshold'] 118 | properties: 119 | name: 120 | type: string 121 | interval: 122 | type: string 123 | pattern: "^[0-9]+(m|s)" 124 | threshold: 125 | type: number 126 | query: 127 | type: string 128 | webhooks: 129 | type: array 130 | properties: 131 | items: 132 | type: object 133 | required: ['name', 'url', 'timeout'] 134 | properties: 135 | name: 136 | type: string 137 | type: 138 | type: string 139 | enum: 140 | - "" 141 | - pre-rollout 142 | - rollout 143 | - post-rollout 144 | url: 145 | type: string 146 | format: url 147 | timeout: 148 | type: string 149 | pattern: "^[0-9]+(m|s)" 150 | status: 151 | properties: 152 | phase: 153 | type: string 154 | enum: 155 | - "" 156 | - Initialized 157 | - Progressing 158 | - Succeeded 159 | - Failed 160 | canaryWeight: 161 | type: number 162 | failedChecks: 163 | type: number 164 | iterations: 165 | type: number 166 | lastAppliedSpec: 167 | type: string 168 | lastTransitionTime: 169 | type: string 170 | --- 171 | apiVersion: v1 172 | kind: ServiceAccount 173 | metadata: 174 | name: flagger 175 | namespace: istio-system 176 | labels: 177 | app: flagger 178 | --- 179 | apiVersion: rbac.authorization.k8s.io/v1beta1 180 | kind: ClusterRole 181 | metadata: 182 | name: flagger 183 | labels: 184 | app: flagger 185 | rules: 186 | - apiGroups: 187 | - "" 188 | resources: 189 | - events 190 | - configmaps 191 | - secrets 192 | - services 193 | verbs: ["*"] 194 | - apiGroups: 195 | - apps 196 | resources: 197 | - deployments 198 | verbs: ["*"] 199 | - apiGroups: 200 | - autoscaling 201 | resources: 202 | - horizontalpodautoscalers 203 | verbs: ["*"] 204 | - apiGroups: 205 | - "extensions" 206 | resources: 207 | - ingresses 208 | - ingresses/status 209 | verbs: ["*"] 210 | - apiGroups: 211 | - flagger.app 212 | resources: 213 | - canaries 214 | - canaries/status 215 | verbs: ["*"] 216 | - apiGroups: 217 | - networking.istio.io 218 | resources: 219 | - virtualservices 220 | - virtualservices/status 221 | verbs: ["*"] 222 | - apiGroups: 223 | - appmesh.k8s.aws 224 | resources: 225 | - meshes 226 | - meshes/status 227 | - virtualnodes 228 | - virtualnodes/status 229 | - virtualservices 230 | - virtualservices/status 231 | verbs: ["*"] 232 | - apiGroups: 233 | - split.smi-spec.io 234 | resources: 235 | - trafficsplits 236 | verbs: ["*"] 237 | - nonResourceURLs: 238 | - /version 239 | verbs: 240 | - get 241 | --- 242 | apiVersion: rbac.authorization.k8s.io/v1beta1 243 | kind: ClusterRoleBinding 244 | metadata: 245 | name: flagger 246 | labels: 247 | app: flagger 248 | roleRef: 249 | apiGroup: rbac.authorization.k8s.io 250 | kind: ClusterRole 251 | name: flagger 252 | subjects: 253 | - kind: ServiceAccount 254 | name: flagger 255 | namespace: istio-system 256 | --- 257 | apiVersion: apps/v1 258 | kind: Deployment 259 | metadata: 260 | name: flagger 261 | namespace: istio-system 262 | labels: 263 | app: flagger 264 | spec: 265 | replicas: 1 266 | strategy: 267 | type: Recreate 268 | selector: 269 | matchLabels: 270 | app: flagger 271 | template: 272 | metadata: 273 | labels: 274 | app: flagger 275 | annotations: 276 | prometheus.io/scrape: "true" 277 | spec: 278 | serviceAccountName: flagger 279 | containers: 280 | - name: flagger 281 | image: weaveworks/flagger:master-12d84b2 282 | imagePullPolicy: IfNotPresent 283 | ports: 284 | - name: http 285 | containerPort: 8080 286 | command: 287 | - ./flagger 288 | - -log-level=info 289 | - -control-loop-interval=10s 290 | - -mesh-provider=smi:istio 291 | - -metrics-server=http://prometheus.istio-system.svc.cluster.local:9090 292 | livenessProbe: 293 | exec: 294 | command: 295 | - wget 296 | - --quiet 297 | - --tries=1 298 | - --timeout=2 299 | - --spider 300 | - http://localhost:8080/healthz 301 | timeoutSeconds: 5 302 | readinessProbe: 303 | exec: 304 | command: 305 | - wget 306 | - --quiet 307 | - --tries=1 308 | - --timeout=2 309 | - --spider 310 | - http://localhost:8080/healthz 311 | timeoutSeconds: 5 312 | resources: 313 | limits: 314 | memory: "512Mi" 315 | cpu: "1000m" 316 | requests: 317 | memory: "32Mi" 318 | cpu: "10m" 319 | securityContext: 320 | readOnlyRootFilesystem: true 321 | runAsUser: 10001 322 | -------------------------------------------------------------------------------- /pkg/apis/rbac/v1alpha1/rbac.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. 2 | // source: https://github.com/istio/api/blob/ece3d93c51b41adf415b38366ea642eb7c1bea93/rbac/v1alpha1/rbac.proto 3 | 4 | // Istio RBAC (Role Based Access Control) defines ServiceRole and ServiceRoleBinding 5 | // objects. 6 | // 7 | // A ServiceRole specification includes a list of rules (permissions). Each rule has 8 | // the following standard fields: 9 | // 10 | // * services: a list of services. 11 | // * methods: A list of HTTP methods. You can set the value to `*` to include all HTTP methods. 12 | // This field should not be set for TCP services. The policy will be ignored. 13 | // For gRPC services, only `POST` is allowed; other methods will result in denying services. 14 | // * paths: HTTP paths or gRPC methods. Note that gRPC methods should be 15 | // presented in the form of "/packageName.serviceName/methodName" and are case sensitive. 16 | // 17 | // In addition to the standard fields, operators can also use custom keys in the `constraints` field, 18 | // the supported keys are listed in the "constraints and properties" page. 19 | // 20 | // Below is an example of ServiceRole object "product-viewer", which has "read" ("GET" and "HEAD") 21 | // access to "products.svc.cluster.local" service at versions "v1" and "v2". "path" is not specified, 22 | // so it applies to any path in the service. 23 | // 24 | // ```yaml 25 | // apiVersion: "rbac.istio.io/v1alpha1" 26 | // kind: ServiceRole 27 | // metadata: 28 | // name: products-viewer 29 | // namespace: default 30 | // spec: 31 | // rules: 32 | // - services: ["products.svc.cluster.local"] 33 | // methods: ["GET", "HEAD"] 34 | // constraints: 35 | // - key: "destination.labels[version]" 36 | // values: ["v1", "v2"] 37 | // ``` 38 | // 39 | // A ServiceRoleBinding specification includes two parts: 40 | // 41 | // * The `roleRef` field that refers to a ServiceRole object in the same namespace. 42 | // * A list of `subjects` that are assigned the roles. 43 | // 44 | // In addition to a simple `user` field, operators can also use custom keys in the `properties` field, 45 | // the supported keys are listed in the "constraints and properties" page. 46 | // 47 | // Below is an example of ServiceRoleBinding object "test-binding-products", which binds two subjects 48 | // to ServiceRole "product-viewer": 49 | // 50 | // * User "alice@yahoo.com" 51 | // * Services in "abc" namespace. 52 | // 53 | // ```yaml 54 | // apiVersion: "rbac.istio.io/v1alpha1" 55 | // kind: ServiceRoleBinding 56 | // metadata: 57 | // name: test-binding-products 58 | // namespace: default 59 | // spec: 60 | // subjects: 61 | // - user: alice@yahoo.com 62 | // - properties: 63 | // source.namespace: "abc" 64 | // roleRef: 65 | // kind: ServiceRole 66 | // name: "products-viewer" 67 | // ``` 68 | 69 | package v1alpha1 70 | 71 | // AccessRule defines a permission to access a list of services. 72 | type AccessRule struct { 73 | // Required. A list of service names. 74 | // Exact match, prefix match, and suffix match are supported for service names. 75 | // For example, the service name "bookstore.mtv.cluster.local" matches 76 | // "bookstore.mtv.cluster.local" (exact match), or "bookstore*" (prefix match), 77 | // or "*.mtv.cluster.local" (suffix match). 78 | // If set to ["*"], it refers to all services in the namespace. 79 | Services []string `protobuf:"bytes,1,rep,name=services,proto3" json:"services,omitempty"` 80 | // Optional. A list of HTTP hosts. This is matched against the HOST header in 81 | // a HTTP request. Exact match, prefix match and suffix match are supported. 82 | // For example, the host "test.abc.com" matches "test.abc.com" (exact match), 83 | // or "*.abc.com" (prefix match), or "test.abc.*" (suffix match). 84 | // If not specified, it matches to any host. 85 | // This field should not be set for TCP services. The policy will be ignored. 86 | Hosts []string `protobuf:"bytes,5,rep,name=hosts,proto3" json:"hosts,omitempty"` 87 | // Optional. A list of HTTP hosts that must not be matched. 88 | NotHosts []string `protobuf:"bytes,6,rep,name=not_hosts,json=notHosts,proto3" json:"not_hosts,omitempty"` 89 | // Optional. A list of HTTP paths or gRPC methods. 90 | // gRPC methods must be presented as fully-qualified name in the form of 91 | // "/packageName.serviceName/methodName" and are case sensitive. 92 | // Exact match, prefix match, and suffix match are supported. For example, 93 | // the path "/books/review" matches "/books/review" (exact match), 94 | // or "/books/*" (prefix match), or "*/review" (suffix match). 95 | // If not specified, it matches to any path. 96 | // This field should not be set for TCP services. The policy will be ignored. 97 | Paths []string `protobuf:"bytes,2,rep,name=paths,proto3" json:"paths,omitempty"` 98 | // Optional. A list of HTTP paths or gRPC methods that must not be matched. 99 | NotPaths []string `protobuf:"bytes,7,rep,name=not_paths,json=notPaths,proto3" json:"not_paths,omitempty"` 100 | // Optional. A list of HTTP methods (e.g., "GET", "POST"). 101 | // If not specified or specified as "*", it matches to any methods. 102 | // This field should not be set for TCP services. The policy will be ignored. 103 | // For gRPC services, only `POST` is allowed; other methods will result in denying services. 104 | Methods []string `protobuf:"bytes,3,rep,name=methods,proto3" json:"methods,omitempty"` 105 | // Optional. A list of HTTP methods that must not be matched. 106 | // Note: It's an error to set methods and not_methods at the same time. 107 | NotMethods []string `protobuf:"bytes,8,rep,name=not_methods,json=notMethods,proto3" json:"not_methods,omitempty"` 108 | // Optional. A list of port numbers of the request. If not specified, it matches 109 | // to any port number. 110 | // Note: It's an error to set ports and not_ports at the same time. 111 | Ports []int32 `protobuf:"varint,9,rep,packed,name=ports,proto3" json:"ports,omitempty"` 112 | // Optional. A list of port numbers that must not be matched. 113 | // Note: It's an error to set ports and not_ports at the same time. 114 | NotPorts []int32 `protobuf:"varint,10,rep,packed,name=not_ports,json=notPorts,proto3" json:"not_ports,omitempty"` 115 | // Optional. Extra constraints in the ServiceRole specification. 116 | Constraints []*AccessRule_Constraint `protobuf:"bytes,4,rep,name=constraints,proto3" json:"constraints,omitempty"` 117 | } 118 | 119 | // Definition of a custom constraint. The supported keys are listed in the "constraint and properties" page. 120 | type AccessRule_Constraint struct { 121 | // Key of the constraint. 122 | Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` 123 | // List of valid values for the constraint. 124 | // Exact match, prefix match, and suffix match are supported. 125 | // For example, the value "v1alpha2" matches "v1alpha2" (exact match), 126 | // or "v1*" (prefix match), or "*alpha2" (suffix match). 127 | Values []string `protobuf:"bytes,2,rep,name=values,proto3" json:"values,omitempty"` 128 | } 129 | 130 | // Subject defines an identity. The identity is either a user or identified by a set of `properties`. 131 | // The supported keys in `properties` are listed in "constraint and properties" page. 132 | type Subject struct { 133 | // Optional. The user name/ID that the subject represents. 134 | User string `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` 135 | // Optional. A list of subject names. This is matched to the 136 | // `source.principal` attribute. If one of subject names is "*", it matches to a subject with any name. 137 | // Prefix and suffix matches are supported. 138 | Names []string `protobuf:"bytes,4,rep,name=names,proto3" json:"names,omitempty"` 139 | // Optional. A list of subject names that must not be matched. 140 | NotNames []string `protobuf:"bytes,5,rep,name=not_names,json=notNames,proto3" json:"not_names,omitempty"` 141 | // Optional. The group that the subject belongs to. 142 | // Deprecated. Use groups and not_groups instead. 143 | Group string `protobuf:"bytes,2,opt,name=group,proto3" json:"group,omitempty"` // Deprecated: Do not use. 144 | // Optional. A list of groups that the subject represents. This is matched to the 145 | // `request.auth.claims[groups]` attribute. If not specified, it applies to any groups. 146 | Groups []string `protobuf:"bytes,6,rep,name=groups,proto3" json:"groups,omitempty"` 147 | // Optional. A list of groups that must not be matched. 148 | NotGroups []string `protobuf:"bytes,7,rep,name=not_groups,json=notGroups,proto3" json:"not_groups,omitempty"` 149 | // Optional. A list of namespaces that the subject represents. This is matched to 150 | // the `source.namespace` attribute. If not specified, it applies to any namespaces. 151 | Namespaces []string `protobuf:"bytes,8,rep,name=namespaces,proto3" json:"namespaces,omitempty"` 152 | // Optional. A list of namespaces that must not be matched. 153 | NotNamespaces []string `protobuf:"bytes,9,rep,name=not_namespaces,json=notNamespaces,proto3" json:"not_namespaces,omitempty"` 154 | // Optional. A list of IP address or CIDR ranges that the subject represents. 155 | // E.g. 192.168.100.2 or 10.1.0.0/16. If not specified, it applies to any IP addresses. 156 | Ips []string `protobuf:"bytes,10,rep,name=ips,proto3" json:"ips,omitempty"` 157 | // Optional. A list of IP addresses or CIDR ranges that must not be matched. 158 | NotIps []string `protobuf:"bytes,11,rep,name=not_ips,json=notIps,proto3" json:"not_ips,omitempty"` 159 | // Optional. The set of properties that identify the subject. 160 | Properties map[string]string `protobuf:"bytes,3,rep,name=properties,proto3" json:"properties,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` 161 | } 162 | 163 | // RoleRef refers to a role object. 164 | type RoleRef struct { 165 | // Required. The type of the role being referenced. 166 | // Currently, "ServiceRole" is the only supported value for "kind". 167 | Kind string `protobuf:"bytes,1,opt,name=kind,proto3" json:"kind,omitempty"` 168 | // Required. The name of the ServiceRole object being referenced. 169 | // The ServiceRole object must be in the same namespace as the ServiceRoleBinding object. 170 | Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` 171 | } 172 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2020 Service Mesh Interface Authors. 191 | 192 | and others that have contributed code to the public domain. 193 | 194 | Licensed under the Apache License, Version 2.0 (the "License"); 195 | you may not use this file except in compliance with the License. 196 | You may obtain a copy of the License at 197 | 198 | http://www.apache.org/licenses/LICENSE-2.0 199 | 200 | Unless required by applicable law or agreed to in writing, software 201 | distributed under the License is distributed on an "AS IS" BASIS, 202 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 203 | See the License for the specific language governing permissions and 204 | limitations under the License. 205 | -------------------------------------------------------------------------------- /pkg/apis/rbac/v1alpha1/zz_generated.deepcopy.go: -------------------------------------------------------------------------------- 1 | // +build !ignore_autogenerated 2 | 3 | // Code generated by deepcopy-gen. DO NOT EDIT. 4 | 5 | package v1alpha1 6 | 7 | import ( 8 | runtime "k8s.io/apimachinery/pkg/runtime" 9 | ) 10 | 11 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 12 | func (in *AccessRule) DeepCopyInto(out *AccessRule) { 13 | *out = *in 14 | if in.Services != nil { 15 | in, out := &in.Services, &out.Services 16 | *out = make([]string, len(*in)) 17 | copy(*out, *in) 18 | } 19 | if in.Hosts != nil { 20 | in, out := &in.Hosts, &out.Hosts 21 | *out = make([]string, len(*in)) 22 | copy(*out, *in) 23 | } 24 | if in.NotHosts != nil { 25 | in, out := &in.NotHosts, &out.NotHosts 26 | *out = make([]string, len(*in)) 27 | copy(*out, *in) 28 | } 29 | if in.Paths != nil { 30 | in, out := &in.Paths, &out.Paths 31 | *out = make([]string, len(*in)) 32 | copy(*out, *in) 33 | } 34 | if in.NotPaths != nil { 35 | in, out := &in.NotPaths, &out.NotPaths 36 | *out = make([]string, len(*in)) 37 | copy(*out, *in) 38 | } 39 | if in.Methods != nil { 40 | in, out := &in.Methods, &out.Methods 41 | *out = make([]string, len(*in)) 42 | copy(*out, *in) 43 | } 44 | if in.NotMethods != nil { 45 | in, out := &in.NotMethods, &out.NotMethods 46 | *out = make([]string, len(*in)) 47 | copy(*out, *in) 48 | } 49 | if in.Ports != nil { 50 | in, out := &in.Ports, &out.Ports 51 | *out = make([]int32, len(*in)) 52 | copy(*out, *in) 53 | } 54 | if in.NotPorts != nil { 55 | in, out := &in.NotPorts, &out.NotPorts 56 | *out = make([]int32, len(*in)) 57 | copy(*out, *in) 58 | } 59 | if in.Constraints != nil { 60 | in, out := &in.Constraints, &out.Constraints 61 | *out = make([]*AccessRule_Constraint, len(*in)) 62 | for i := range *in { 63 | if (*in)[i] != nil { 64 | in, out := &(*in)[i], &(*out)[i] 65 | *out = new(AccessRule_Constraint) 66 | (*in).DeepCopyInto(*out) 67 | } 68 | } 69 | } 70 | return 71 | } 72 | 73 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessRule. 74 | func (in *AccessRule) DeepCopy() *AccessRule { 75 | if in == nil { 76 | return nil 77 | } 78 | out := new(AccessRule) 79 | in.DeepCopyInto(out) 80 | return out 81 | } 82 | 83 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 84 | func (in *AccessRule_Constraint) DeepCopyInto(out *AccessRule_Constraint) { 85 | *out = *in 86 | if in.Values != nil { 87 | in, out := &in.Values, &out.Values 88 | *out = make([]string, len(*in)) 89 | copy(*out, *in) 90 | } 91 | return 92 | } 93 | 94 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessRule_Constraint. 95 | func (in *AccessRule_Constraint) DeepCopy() *AccessRule_Constraint { 96 | if in == nil { 97 | return nil 98 | } 99 | out := new(AccessRule_Constraint) 100 | in.DeepCopyInto(out) 101 | return out 102 | } 103 | 104 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 105 | func (in *RoleRef) DeepCopyInto(out *RoleRef) { 106 | *out = *in 107 | return 108 | } 109 | 110 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleRef. 111 | func (in *RoleRef) DeepCopy() *RoleRef { 112 | if in == nil { 113 | return nil 114 | } 115 | out := new(RoleRef) 116 | in.DeepCopyInto(out) 117 | return out 118 | } 119 | 120 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 121 | func (in *ServiceRole) DeepCopyInto(out *ServiceRole) { 122 | *out = *in 123 | out.TypeMeta = in.TypeMeta 124 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 125 | in.Spec.DeepCopyInto(&out.Spec) 126 | out.Status = in.Status 127 | return 128 | } 129 | 130 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceRole. 131 | func (in *ServiceRole) DeepCopy() *ServiceRole { 132 | if in == nil { 133 | return nil 134 | } 135 | out := new(ServiceRole) 136 | in.DeepCopyInto(out) 137 | return out 138 | } 139 | 140 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 141 | func (in *ServiceRole) DeepCopyObject() runtime.Object { 142 | if c := in.DeepCopy(); c != nil { 143 | return c 144 | } 145 | return nil 146 | } 147 | 148 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 149 | func (in *ServiceRoleBinding) DeepCopyInto(out *ServiceRoleBinding) { 150 | *out = *in 151 | out.TypeMeta = in.TypeMeta 152 | in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) 153 | in.Spec.DeepCopyInto(&out.Spec) 154 | out.Status = in.Status 155 | return 156 | } 157 | 158 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceRoleBinding. 159 | func (in *ServiceRoleBinding) DeepCopy() *ServiceRoleBinding { 160 | if in == nil { 161 | return nil 162 | } 163 | out := new(ServiceRoleBinding) 164 | in.DeepCopyInto(out) 165 | return out 166 | } 167 | 168 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 169 | func (in *ServiceRoleBinding) DeepCopyObject() runtime.Object { 170 | if c := in.DeepCopy(); c != nil { 171 | return c 172 | } 173 | return nil 174 | } 175 | 176 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 177 | func (in *ServiceRoleBindingList) DeepCopyInto(out *ServiceRoleBindingList) { 178 | *out = *in 179 | out.TypeMeta = in.TypeMeta 180 | out.ListMeta = in.ListMeta 181 | if in.Items != nil { 182 | in, out := &in.Items, &out.Items 183 | *out = make([]ServiceRoleBinding, len(*in)) 184 | for i := range *in { 185 | (*in)[i].DeepCopyInto(&(*out)[i]) 186 | } 187 | } 188 | return 189 | } 190 | 191 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceRoleBindingList. 192 | func (in *ServiceRoleBindingList) DeepCopy() *ServiceRoleBindingList { 193 | if in == nil { 194 | return nil 195 | } 196 | out := new(ServiceRoleBindingList) 197 | in.DeepCopyInto(out) 198 | return out 199 | } 200 | 201 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 202 | func (in *ServiceRoleBindingList) DeepCopyObject() runtime.Object { 203 | if c := in.DeepCopy(); c != nil { 204 | return c 205 | } 206 | return nil 207 | } 208 | 209 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 210 | func (in *ServiceRoleBindingSpec) DeepCopyInto(out *ServiceRoleBindingSpec) { 211 | *out = *in 212 | if in.Subjects != nil { 213 | in, out := &in.Subjects, &out.Subjects 214 | *out = make([]*Subject, len(*in)) 215 | for i := range *in { 216 | if (*in)[i] != nil { 217 | in, out := &(*in)[i], &(*out)[i] 218 | *out = new(Subject) 219 | (*in).DeepCopyInto(*out) 220 | } 221 | } 222 | } 223 | if in.RoleRef != nil { 224 | in, out := &in.RoleRef, &out.RoleRef 225 | *out = new(RoleRef) 226 | **out = **in 227 | } 228 | return 229 | } 230 | 231 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceRoleBindingSpec. 232 | func (in *ServiceRoleBindingSpec) DeepCopy() *ServiceRoleBindingSpec { 233 | if in == nil { 234 | return nil 235 | } 236 | out := new(ServiceRoleBindingSpec) 237 | in.DeepCopyInto(out) 238 | return out 239 | } 240 | 241 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 242 | func (in *ServiceRoleBindingStatus) DeepCopyInto(out *ServiceRoleBindingStatus) { 243 | *out = *in 244 | return 245 | } 246 | 247 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceRoleBindingStatus. 248 | func (in *ServiceRoleBindingStatus) DeepCopy() *ServiceRoleBindingStatus { 249 | if in == nil { 250 | return nil 251 | } 252 | out := new(ServiceRoleBindingStatus) 253 | in.DeepCopyInto(out) 254 | return out 255 | } 256 | 257 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 258 | func (in *ServiceRoleList) DeepCopyInto(out *ServiceRoleList) { 259 | *out = *in 260 | out.TypeMeta = in.TypeMeta 261 | out.ListMeta = in.ListMeta 262 | if in.Items != nil { 263 | in, out := &in.Items, &out.Items 264 | *out = make([]ServiceRole, len(*in)) 265 | for i := range *in { 266 | (*in)[i].DeepCopyInto(&(*out)[i]) 267 | } 268 | } 269 | return 270 | } 271 | 272 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceRoleList. 273 | func (in *ServiceRoleList) DeepCopy() *ServiceRoleList { 274 | if in == nil { 275 | return nil 276 | } 277 | out := new(ServiceRoleList) 278 | in.DeepCopyInto(out) 279 | return out 280 | } 281 | 282 | // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. 283 | func (in *ServiceRoleList) DeepCopyObject() runtime.Object { 284 | if c := in.DeepCopy(); c != nil { 285 | return c 286 | } 287 | return nil 288 | } 289 | 290 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 291 | func (in *ServiceRoleSpec) DeepCopyInto(out *ServiceRoleSpec) { 292 | *out = *in 293 | if in.Rules != nil { 294 | in, out := &in.Rules, &out.Rules 295 | *out = make([]*AccessRule, len(*in)) 296 | for i := range *in { 297 | if (*in)[i] != nil { 298 | in, out := &(*in)[i], &(*out)[i] 299 | *out = new(AccessRule) 300 | (*in).DeepCopyInto(*out) 301 | } 302 | } 303 | } 304 | return 305 | } 306 | 307 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceRoleSpec. 308 | func (in *ServiceRoleSpec) DeepCopy() *ServiceRoleSpec { 309 | if in == nil { 310 | return nil 311 | } 312 | out := new(ServiceRoleSpec) 313 | in.DeepCopyInto(out) 314 | return out 315 | } 316 | 317 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 318 | func (in *ServiceRoleStatus) DeepCopyInto(out *ServiceRoleStatus) { 319 | *out = *in 320 | return 321 | } 322 | 323 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceRoleStatus. 324 | func (in *ServiceRoleStatus) DeepCopy() *ServiceRoleStatus { 325 | if in == nil { 326 | return nil 327 | } 328 | out := new(ServiceRoleStatus) 329 | in.DeepCopyInto(out) 330 | return out 331 | } 332 | 333 | // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. 334 | func (in *Subject) DeepCopyInto(out *Subject) { 335 | *out = *in 336 | if in.Names != nil { 337 | in, out := &in.Names, &out.Names 338 | *out = make([]string, len(*in)) 339 | copy(*out, *in) 340 | } 341 | if in.NotNames != nil { 342 | in, out := &in.NotNames, &out.NotNames 343 | *out = make([]string, len(*in)) 344 | copy(*out, *in) 345 | } 346 | if in.Groups != nil { 347 | in, out := &in.Groups, &out.Groups 348 | *out = make([]string, len(*in)) 349 | copy(*out, *in) 350 | } 351 | if in.NotGroups != nil { 352 | in, out := &in.NotGroups, &out.NotGroups 353 | *out = make([]string, len(*in)) 354 | copy(*out, *in) 355 | } 356 | if in.Namespaces != nil { 357 | in, out := &in.Namespaces, &out.Namespaces 358 | *out = make([]string, len(*in)) 359 | copy(*out, *in) 360 | } 361 | if in.NotNamespaces != nil { 362 | in, out := &in.NotNamespaces, &out.NotNamespaces 363 | *out = make([]string, len(*in)) 364 | copy(*out, *in) 365 | } 366 | if in.Ips != nil { 367 | in, out := &in.Ips, &out.Ips 368 | *out = make([]string, len(*in)) 369 | copy(*out, *in) 370 | } 371 | if in.NotIps != nil { 372 | in, out := &in.NotIps, &out.NotIps 373 | *out = make([]string, len(*in)) 374 | copy(*out, *in) 375 | } 376 | if in.Properties != nil { 377 | in, out := &in.Properties, &out.Properties 378 | *out = make(map[string]string, len(*in)) 379 | for key, val := range *in { 380 | (*out)[key] = val 381 | } 382 | } 383 | return 384 | } 385 | 386 | // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Subject. 387 | func (in *Subject) DeepCopy() *Subject { 388 | if in == nil { 389 | return nil 390 | } 391 | out := new(Subject) 392 | in.DeepCopyInto(out) 393 | return out 394 | } 395 | -------------------------------------------------------------------------------- /pkg/controller/traffictarget/traffictarget_controller.go: -------------------------------------------------------------------------------- 1 | package traffictarget 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | accessv1alpha1 "github.com/deislabs/smi-sdk-go/pkg/apis/access/v1alpha1" 8 | specsv1alpha1 "github.com/deislabs/smi-sdk-go/pkg/apis/specs/v1alpha1" 9 | "github.com/go-logr/logr" 10 | "github.com/google/go-cmp/cmp" 11 | "k8s.io/apimachinery/pkg/api/errors" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "k8s.io/apimachinery/pkg/types" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | "sigs.k8s.io/controller-runtime/pkg/controller" 17 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 18 | "sigs.k8s.io/controller-runtime/pkg/handler" 19 | "sigs.k8s.io/controller-runtime/pkg/manager" 20 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 21 | logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" 22 | "sigs.k8s.io/controller-runtime/pkg/source" 23 | 24 | rbacv1alpha1 "github.com/deislabs/smi-adapter-istio/pkg/apis/rbac/v1alpha1" 25 | ) 26 | 27 | var log = logf.Log.WithName("controller_traffictarget") 28 | 29 | // Add creates a new TrafficTarget Controller and adds it to the Manager. 30 | // The Manager will set fields on the Controller and Start it when the Manager 31 | // is Started. 32 | func Add(mgr manager.Manager) error { 33 | return add(mgr, newReconciler(mgr)) 34 | } 35 | 36 | // newReconciler returns a new reconcile.Reconciler 37 | func newReconciler(mgr manager.Manager) reconcile.Reconciler { 38 | return &ReconcileTrafficTarget{client: mgr.GetClient(), 39 | scheme: mgr.GetScheme()} 40 | } 41 | 42 | // add adds a new Controller to mgr with r as the reconcile.Reconciler 43 | func add(mgr manager.Manager, r reconcile.Reconciler) error { 44 | // Create a new controller 45 | c, err := controller.New("traffictarget-controller", mgr, 46 | controller.Options{Reconciler: r}) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | // Watch for changes to primary resource TrafficTarget 52 | err = c.Watch(&source.Kind{Type: &accessv1alpha1.TrafficTarget{}}, 53 | &handler.EnqueueRequestForObject{}) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | // Watch for changes to secondary resource ServiceRoleBindings and requeue 59 | // the owner TrafficTarget 60 | return c.Watch(&source.Kind{Type: &rbacv1alpha1.ServiceRoleBinding{}}, 61 | &handler.EnqueueRequestForOwner{ 62 | IsController: true, 63 | OwnerType: &accessv1alpha1.TrafficTarget{}, 64 | }) 65 | } 66 | 67 | var _ reconcile.Reconciler = &ReconcileTrafficTarget{} 68 | 69 | // ReconcileTrafficTarget reconciles a TrafficTarget object 70 | type ReconcileTrafficTarget struct { 71 | // This client, initialized using mgr.Client() above, is a split client 72 | // that reads objects from the cache and writes to the apiserver 73 | client client.Client 74 | scheme *runtime.Scheme 75 | } 76 | 77 | // Reconcile reads that state of the cluster for a TrafficTarget object and 78 | // makes changes based on the state read and what is in the TrafficTarget.Spec 79 | // 80 | // The Controller will requeue the Request to be processed again if the returned 81 | // error is non-nil or Result.Requeue is true, otherwise upon completion it will 82 | // remove the work from the queue. 83 | func (r *ReconcileTrafficTarget) Reconcile( 84 | request reconcile.Request, 85 | ) (reconcile.Result, error) { 86 | reqLogger := log.WithValues( 87 | "Request.Namespace", request.Namespace, 88 | "Request.Name", request.Name) 89 | reqLogger.Info("Reconciling TrafficTarget") 90 | 91 | // Fetch the TrafficTarget instance 92 | trafficTarget := &accessv1alpha1.TrafficTarget{} 93 | err := r.client.Get(context.TODO(), request.NamespacedName, trafficTarget) 94 | if err != nil { 95 | if errors.IsNotFound(err) { 96 | // Request object not found, could have been deleted after reconcile 97 | // request. Owned objects are automatically garbage collected. For 98 | // additional cleanup logic use finalizers. Return and don't requeue 99 | reqLogger.Info("TrafficTarget object not found.") 100 | return reconcile.Result{}, nil 101 | } 102 | // Error reading the object - requeue the request. 103 | reqLogger.Error(err, 104 | "Failed to get TrafficTarget. Request will be requeued.") 105 | return reconcile.Result{}, err 106 | } 107 | 108 | return r.reconcileTrafficTarget(trafficTarget, reqLogger) 109 | } 110 | 111 | func (r *ReconcileTrafficTarget) reconcileTrafficTarget(trafficTarget *accessv1alpha1.TrafficTarget, reqLogger logr.Logger) (reconcile.Result, error) { 112 | 113 | svcRole, svcRoleBinding, err := r.createRBAC(trafficTarget) 114 | if err != nil { 115 | return reconcile.Result{}, err 116 | } 117 | 118 | // Set TrafficTarget instance as the owner and controller 119 | if err := controllerutil.SetControllerReference(trafficTarget, 120 | svcRole, r.scheme); err != nil { 121 | return reconcile.Result{}, err 122 | } 123 | if err := controllerutil.SetControllerReference(trafficTarget, 124 | svcRoleBinding, r.scheme); err != nil { 125 | return reconcile.Result{}, err 126 | } 127 | 128 | recSvcRole, errSvcRole := r.createServiceRole(svcRole, reqLogger) 129 | recSvcRoleBinding, errSvcRoleBinding := r.createServiceRoleBinding( 130 | svcRoleBinding, reqLogger, 131 | ) 132 | if errSvcRole != nil { 133 | return recSvcRole, errSvcRole 134 | } 135 | if errSvcRoleBinding != nil { 136 | return recSvcRoleBinding, errSvcRoleBinding 137 | } 138 | return reconcile.Result{}, nil 139 | } 140 | 141 | func (r *ReconcileTrafficTarget) createServiceRole( 142 | svcRole *rbacv1alpha1.ServiceRole, 143 | reqLogger logr.Logger, 144 | ) (reconcile.Result, error) { 145 | // Check if this ServiceRole already exists 146 | foundSvcRole := &rbacv1alpha1.ServiceRole{} 147 | err := r.client.Get( 148 | context.TODO(), 149 | types.NamespacedName{Name: svcRole.Name, Namespace: svcRole.Namespace}, 150 | foundSvcRole, 151 | ) 152 | 153 | // Create ServiceRole 154 | if err != nil && errors.IsNotFound(err) { 155 | reqLogger.Info("Creating a new ServiceRole", 156 | "ServiceRole.Namespace", svcRole.Namespace, 157 | "ServiceRole.Name", svcRole.Name) 158 | err = r.client.Create(context.TODO(), svcRole) 159 | if err != nil { 160 | return reconcile.Result{}, err 161 | } 162 | 163 | // ServiceRole created successfully - don't requeue 164 | return reconcile.Result{}, nil 165 | } else if err != nil { 166 | reqLogger.Error(err, "Failed to get ServiceRole", 167 | "ServiceRole.Namespace", svcRole.Namespace, 168 | "ServiceRole.Name", svcRole.Name) 169 | return reconcile.Result{}, err 170 | } 171 | 172 | // Update ServiceRole 173 | if diff := cmp.Diff(svcRole.Spec, foundSvcRole.Spec); diff != "" { 174 | reqLogger.Info("Updating ServiceRole", 175 | "ServiceRole.Namespace", svcRole.Namespace, 176 | "ServiceRole.Name", svcRole.Name) 177 | clone := foundSvcRole.DeepCopy() 178 | clone.Spec = svcRole.Spec 179 | err = r.client.Update(context.TODO(), clone) 180 | if err != nil { 181 | return reconcile.Result{}, err 182 | } 183 | } 184 | return reconcile.Result{}, nil 185 | } 186 | 187 | func (r *ReconcileTrafficTarget) createServiceRoleBinding( 188 | svcRoleBinding *rbacv1alpha1.ServiceRoleBinding, 189 | reqLogger logr.Logger, 190 | ) (reconcile.Result, error) { 191 | // Check if this ServiceRoleBinding already exists 192 | foundSvcRoleBinding := &rbacv1alpha1.ServiceRoleBinding{} 193 | err := r.client.Get( 194 | context.TODO(), 195 | types.NamespacedName{ 196 | Name: svcRoleBinding.Name, Namespace: svcRoleBinding.Namespace, 197 | }, 198 | foundSvcRoleBinding, 199 | ) 200 | 201 | // Create ServiceRoleBinding 202 | if err != nil && errors.IsNotFound(err) { 203 | reqLogger.Info("Creating a new ServiceRoleBinding", 204 | "ServiceRoleBinding.Namespace", svcRoleBinding.Namespace, 205 | "ServiceRoleBinding.Name", svcRoleBinding.Name) 206 | err = r.client.Create(context.TODO(), svcRoleBinding) 207 | if err != nil { 208 | return reconcile.Result{}, err 209 | } 210 | 211 | // ServiceRoleBinding created successfully - don't requeue 212 | return reconcile.Result{}, err 213 | } else if err != nil { 214 | reqLogger.Error(err, "Failed to get ServiceRoleBinding", 215 | "ServiceRoleBinding.Namespace", svcRoleBinding.Namespace, 216 | "ServiceRoleBinding.Name", svcRoleBinding.Name) 217 | return reconcile.Result{}, err 218 | } 219 | 220 | // Update ServiceRoleBinding 221 | if diff := cmp.Diff( 222 | svcRoleBinding.Spec, foundSvcRoleBinding.Spec, 223 | ); diff != "" { 224 | reqLogger.Info("Updating ServiceRoleBinding", 225 | "ServiceRoleBinding.Namespace", svcRoleBinding.Namespace, 226 | "ServiceRoleBinding.Name", svcRoleBinding.Name) 227 | clone := foundSvcRoleBinding.DeepCopy() 228 | clone.Spec = svcRoleBinding.Spec 229 | err = r.client.Update(context.TODO(), clone) 230 | if err != nil { 231 | return reconcile.Result{}, err 232 | } 233 | } 234 | 235 | return reconcile.Result{}, nil 236 | } 237 | 238 | // createRBAC creates a ServiceRole and ServiceRoleBinding for each 239 | // TrafficTarget. For all the HTTPRouteGroup objects referred in the 240 | // TrafficTarget will also be queried. 241 | func (r *ReconcileTrafficTarget) createRBAC(trafficTarget *accessv1alpha1.TrafficTarget) (*rbacv1alpha1.ServiceRole, *rbacv1alpha1.ServiceRoleBinding, error) { 242 | var subjects []*rbacv1alpha1.Subject 243 | for _, src := range trafficTarget.Sources { 244 | // TODO: 245 | // Remove the hardcoded value of `cluster.local` 246 | subjects = append(subjects, &rbacv1alpha1.Subject{ 247 | User: fmt.Sprintf("cluster.local/ns/%s/sa/%s", src.Namespace, src.Name), 248 | }) 249 | } 250 | // same set of constraints generated from a TrafficTarget apply to all the 251 | // AccessRules so build them early itself 252 | constraints := getConstraints(trafficTarget.Destination) 253 | 254 | var rules []*rbacv1alpha1.AccessRule 255 | for _, spec := range trafficTarget.Specs { 256 | matches, err := r.findMatches(spec, trafficTarget.Namespace) 257 | if err != nil { 258 | return nil, nil, err 259 | } 260 | for _, match := range matches { 261 | rules = append(rules, &rbacv1alpha1.AccessRule{ 262 | // Apply to all the services, hardcoded because the 263 | // authorization of traffic is done at constraints level 264 | Services: []string{"*"}, 265 | Methods: match.Methods, 266 | Paths: []string{match.PathRegex}, 267 | Constraints: constraints, 268 | }) 269 | } 270 | } 271 | 272 | svcRole := &rbacv1alpha1.ServiceRole{ 273 | ObjectMeta: metav1.ObjectMeta{ 274 | Name: trafficTarget.Name, 275 | Namespace: trafficTarget.Namespace, 276 | }, 277 | Spec: rbacv1alpha1.ServiceRoleSpec{ 278 | Rules: rules, 279 | }, 280 | } 281 | svcRB := &rbacv1alpha1.ServiceRoleBinding{ 282 | ObjectMeta: metav1.ObjectMeta{ 283 | Name: trafficTarget.Name, 284 | Namespace: trafficTarget.Namespace, 285 | }, 286 | Spec: rbacv1alpha1.ServiceRoleBindingSpec{ 287 | Subjects: subjects, 288 | RoleRef: &rbacv1alpha1.RoleRef{ 289 | Kind: "ServiceRole", 290 | Name: trafficTarget.Name, 291 | }, 292 | }, 293 | } 294 | 295 | return svcRole, svcRB, nil 296 | } 297 | 298 | // getConstraints reads the information from the destination object of 299 | // TrafficTarget and creates correspoding constraints to be applied in the 300 | // Istio ServiceRole object 301 | func getConstraints( 302 | dst accessv1alpha1.IdentityBindingSubject, 303 | ) []*rbacv1alpha1.AccessRule_Constraint { 304 | constraints := []*rbacv1alpha1.AccessRule_Constraint{ 305 | {Key: "destination.user", Values: []string{dst.Name}}, 306 | {Key: "destination.namespace", Values: []string{dst.Namespace}}, 307 | } 308 | if dst.Port != "" { 309 | constraints = append(constraints, &rbacv1alpha1.AccessRule_Constraint{ 310 | Key: "destination.port", 311 | Values: []string{dst.Port}, 312 | }) 313 | } 314 | return constraints 315 | } 316 | 317 | // findMatches finds and returns a "match" object for a HTTPRouteGroup referred 318 | // in the TrafficTarget, if the referred HTTPRouteGroup does not exists then it 319 | // returns an error. If the specific match name referred in the 320 | // `TrafficTarget.specs.matches` does not exist in the given HTTPRouteGroup 321 | // object, then it returns an error. 322 | func (r *ReconcileTrafficTarget) findMatches( 323 | spec accessv1alpha1.TrafficTargetSpec, ns string, 324 | ) ([]specsv1alpha1.HTTPMatch, error) { 325 | httpRouteGroup := &specsv1alpha1.HTTPRouteGroup{} 326 | if err := r.client.Get( 327 | context.TODO(), 328 | types.NamespacedName{Namespace: ns, Name: spec.Name}, 329 | httpRouteGroup, 330 | ); err != nil { 331 | if errors.IsNotFound(err) { 332 | return nil, fmt.Errorf("HTTPRouteGroup not found: %v", err) 333 | } 334 | return nil, fmt.Errorf("Failed to get HTTPRouteGroup: %v", err) 335 | } 336 | 337 | // Create a map to make it easier to search later 338 | matches := make(map[string]specsv1alpha1.HTTPMatch) 339 | for _, match := range httpRouteGroup.Matches { 340 | matches[match.Name] = match 341 | } 342 | 343 | var ret []specsv1alpha1.HTTPMatch 344 | for _, matchName := range spec.Matches { 345 | if _, ok := matches[matchName]; !ok { 346 | return nil, fmt.Errorf( 347 | "Match with name %s not found in HTTPRouteGroup %s", 348 | matchName, spec.Name) 349 | } 350 | ret = append(ret, matches[matchName]) 351 | } 352 | return ret, nil 353 | } 354 | --------------------------------------------------------------------------------