├── .dockerignore ├── .github └── workflows │ ├── lint-helm-chart.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .goreleaser.yml ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.txt ├── Makefile ├── README.md ├── SECURITY.md ├── go.mod ├── go.sum ├── helm └── sloop │ ├── .helmignore │ ├── Chart.yaml │ ├── README.md │ ├── templates │ ├── clusterrole.yaml │ ├── clusterrolebinding.yaml │ ├── configmap.yaml │ ├── ingress.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── statefulset.yaml │ └── values.yaml ├── other ├── architecture.png ├── data_distribution.png ├── screenshot1.png ├── sloop-test.png ├── sloop_logo_black.eps ├── sloop_logo_black.png ├── sloop_logo_color.eps ├── sloop_logo_color.png ├── sloop_logo_color_small_notext.png ├── sloop_logo_white.eps └── sloop_logo_white.png ├── pkg └── sloop │ ├── common │ ├── constants.go │ ├── db_utilities.go │ ├── db_utilities_test.go │ ├── partition_utilities.go │ ├── utilities.go │ └── utilities_test.go │ ├── ingress │ ├── dbrestore.go │ ├── fileplayback.go │ ├── filerecorder.go │ ├── kubeclient.go │ ├── kubeclient_test.go │ ├── kubewatcher.go │ ├── kubewatcher_test.go │ └── types.go │ ├── kubeextractor │ ├── events.go │ ├── events_test.go │ ├── kinds.go │ ├── kubeextractor.go │ ├── kubeextractor_test.go │ ├── nodededupe.go │ └── nodededupe_test.go │ ├── main.go │ ├── processing │ ├── eventcount.go │ ├── eventcount_test.go │ ├── processing.go │ ├── resourcesummary.go │ ├── watch.go │ ├── watch_test.go │ ├── watchactivity.go │ └── watchactivity_test.go │ ├── queries │ ├── eventquery.go │ ├── eventquery_test.go │ ├── init_test.go │ ├── params.go │ ├── query.go │ ├── queryd3heatmap.go │ ├── queryd3heatmap_test.go │ ├── queryfilter.go │ ├── queryfilter_test.go │ ├── rangereadfilters.go │ ├── rangereadfilters_test.go │ ├── respayloadquery.go │ ├── respayloadquery_test.go │ ├── ressumquery.go │ ├── ressumquery_test.go │ ├── timerange.go │ ├── timerange_test.go │ └── types.go │ ├── server │ ├── internal │ │ ├── config │ │ │ ├── config.go │ │ │ └── config_test.go │ │ └── testFiles │ │ │ ├── testConfig.txt │ │ │ ├── testconfig.json │ │ │ └── testconfig.yaml │ └── server.go │ ├── store │ ├── typed │ │ ├── README.md │ │ ├── eventcounttable.go │ │ ├── eventcounttable_test.go │ │ ├── eventcounttablegen.go │ │ ├── eventcounttablegen_test.go │ │ ├── protobufhelper.go │ │ ├── protobufhelper_test.go │ │ ├── resourcesummarytable.go │ │ ├── resourcesummarytable_test.go │ │ ├── resourcesummarytablegen.go │ │ ├── resourcesummarytablegen_test.go │ │ ├── schema.pb.go │ │ ├── schema.proto │ │ ├── tables.go │ │ ├── tabletemplate.go │ │ ├── tabletemplate_test.go │ │ ├── tabletemplatehelper.go │ │ ├── watchactivitytable.go │ │ ├── watchactivitytable_test.go │ │ ├── watchactivitytablegen.go │ │ ├── watchactivitytablegen_test.go │ │ ├── watchtable.go │ │ ├── watchtable_test.go │ │ ├── watchtablegen.go │ │ └── watchtablegen_test.go │ └── untyped │ │ ├── badgerwrap │ │ ├── api.go │ │ ├── badger.go │ │ ├── mock.go │ │ └── mock_test.go │ │ ├── partition.go │ │ ├── partition_test.go │ │ └── store.go │ ├── storemanager │ ├── sleepwithcancel.go │ ├── sleepwithcancel_test.go │ ├── stats.go │ ├── stats_test.go │ ├── storemanager.go │ └── storemanager_test.go │ ├── test │ └── assertex │ │ ├── jsonequal.go │ │ └── protoequal.go │ └── webserver │ ├── bindata.go │ ├── debug.go │ ├── indexhandler.go │ ├── links.go │ ├── links_test.go │ ├── middleware.go │ ├── params.go │ ├── params_test.go │ ├── resourcehandler.go │ ├── webfiles.go │ ├── webfiles │ ├── debug.html │ ├── debug.js │ ├── debugconfig.html │ ├── debughistogram.html │ ├── debuglistkeys.html │ ├── debugtables.html │ ├── debugviewkey.html │ ├── favicon.ico │ ├── filter.js │ ├── index.html │ ├── resource.css │ ├── resource.html │ ├── sloop.css │ └── sloop_ui.js │ ├── webfiles_test.go │ ├── webserver.go │ └── webserver_test.go ├── prometheus.yml └── providers └── aws └── sloop-to-eks.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | data/ 3 | .git/ 4 | -------------------------------------------------------------------------------- /.github/workflows/lint-helm-chart.yml: -------------------------------------------------------------------------------- 1 | name: Lint helm chart 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - 'helm/sloop/**' 9 | pull_request: 10 | branches: 11 | - "*" 12 | paths: 13 | - 'helm/sloop/**' 14 | 15 | jobs: 16 | lint-helm-chart: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Check out code 21 | uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Setup helm 26 | uses: azure/setup-helm@v3 27 | with: 28 | version: 'v3.12.0' 29 | 30 | - name: Lint chart 31 | run: | 32 | helm lint helm/sloop 33 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | branches: 5 | - 'master' 6 | tags: 7 | - 'v*' 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Check out the repo 14 | uses: actions/checkout@v2 15 | 16 | - name: Set up QEMU 17 | uses: docker/setup-qemu-action@v1 18 | 19 | - name: Set up Docker Buildx 20 | uses: docker/setup-buildx-action@v1 21 | 22 | - name: Docker meta 23 | id: meta 24 | uses: docker/metadata-action@v3 25 | with: 26 | images: ghcr.io/${{ github.repository }} 27 | # generate Docker tags based on the following events/attributes 28 | tags: | 29 | type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} 30 | type=ref,event=branch 31 | type=match,pattern=(v\d+\.\d+) 32 | type=sha 33 | 34 | - name: Log in to GitHub Docker Registry 35 | uses: docker/login-action@v1 36 | with: 37 | registry: ghcr.io 38 | username: ${{ github.actor }} 39 | password: ${{ secrets.GITHUB_TOKEN }} 40 | 41 | - name: Build container image 42 | uses: docker/build-push-action@v2 43 | with: 44 | push: true 45 | platforms: linux/amd64,linux/arm64, linux/arm 46 | file: Dockerfile 47 | tags: ${{ steps.meta.outputs.tags }} 48 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Check out code 17 | uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v3 23 | with: 24 | go-version-file: go.mod 25 | 26 | - name: Format with goimports 27 | run: | 28 | go install golang.org/x/tools/cmd/goimports@v0.19.0 29 | if [ "$(goimports -l --format-only . | wc -l)" -gt 0 ]; 30 | then 31 | goimports -l --format-only . 32 | exit 1 33 | fi 34 | 35 | - name: Run go vet 36 | # TODO: Enforce when possible. 37 | run: | 38 | go vet ./... || true 39 | 40 | - name: Run tests 41 | run: | 42 | go test -v -race ./... 43 | 44 | - name: Run go mod tidy 45 | run: | 46 | go mod tidy 47 | git diff --exit-code -- go.mod go.sum 48 | 49 | - name: Run go build 50 | run: go build ./pkg/sloop/main.go 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .DS_Store 3 | /store/ 4 | /data/ 5 | coverage.out 6 | sloop.iml 7 | dist -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: sloop 2 | builds: 3 | - id: sloop 4 | main: ./pkg/sloop/main.go 5 | binary: sloop 6 | ldflags: 7 | - -s -installsuffix=cgo 8 | goos: 9 | - linux 10 | - darwin 11 | - windows 12 | goarch: 13 | - amd64 14 | archives: 15 | - id: sloop 16 | builds: 17 | - sloop 18 | files: 19 | - ./pkg/sloop/webserver/webfiles/**/* 20 | dockers: 21 | - goos: linux 22 | goarch: amd64 23 | ids: 24 | - sloop 25 | extra_files: 26 | - pkg/sloop/webserver/webfiles 27 | image_templates: 28 | - "salesforce/sloop:latest" 29 | - "salesforce/sloop:{{ .Tag }}" 30 | dockerfile: Dockerfile 31 | build_flag_templates: 32 | - "--pull" 33 | - "--label=org.opencontainers.image.created={{.Date}}" 34 | - "--label=org.opencontainers.image.name={{.ProjectName}}" 35 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 36 | - "--label=org.opencontainers.image.version={{.Version}}" 37 | checksum: 38 | name_template: 'checksums.txt' 39 | snapshot: 40 | name_template: "{{ .Tag }}-next" 41 | changelog: 42 | sort: desc 43 | filters: 44 | exclude: 45 | - typo 46 | - fix 47 | - '^docs:' 48 | - '^test:' 49 | - Merge pull request 50 | - Merge branch 51 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Comment line immediately above ownership line is reserved for related gus information. Please be careful while editing. 2 | #ECCN:Open Source 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.19 as build 2 | RUN apt-get update && apt-get install -y curl make 3 | 4 | # https://github.com/kubernetes-sigs/aws-iam-authenticator/releases 5 | RUN curl -o /aws-iam-authenticator https://amazon-eks.s3-us-west-2.amazonaws.com/1.14.6/2019-08-22/bin/linux/amd64/aws-iam-authenticator \ 6 | && wait \ 7 | && chmod +x /aws-iam-authenticator 8 | 9 | COPY . /build/ 10 | WORKDIR /build 11 | 12 | RUN make 13 | 14 | FROM gcr.io/distroless/base 15 | COPY --from=build /go/bin/sloop /sloop 16 | # The copy statement below can be uncommented to reflect changes to any webfiles as compared 17 | # to the binary version of the files in use. 18 | # COPY pkg/sloop/webserver/webfiles /webfiles 19 | COPY --from=build /aws-iam-authenticator /aws-iam-authenticator 20 | ENV PATH="/:${PATH}" 21 | CMD ["/sloop"] 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Salesforce.com, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | * Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY:all run linux docker generate tidy protobuf cover docker-push 2 | 3 | all: 4 | go get ./pkg/... 5 | go fmt ./pkg/... 6 | go install ./pkg/... 7 | go test -cover ./pkg/... 8 | 9 | run: 10 | go install ./pkg/... 11 | $(GOPATH)/bin/sloop 12 | 13 | linux: 14 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install -ldflags "-s" -installsuffix cgo -v ./pkg/... 15 | 16 | goreleaser: 17 | @if [ ! -f "$(GOPATH)/bin/goreleaser" ];then \ 18 | curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh -s -- -b "$(GOPATH)/bin/"; \ 19 | fi 20 | 21 | docker-snapshot: goreleaser 22 | $(GOPATH)/bin/goreleaser release --snapshot --rm-dist 23 | 24 | docker: goreleaser 25 | $(GOPATH)/bin/goreleaser release --rm-dist --skip-publish 26 | 27 | generate: 28 | go generate ./pkg/... 29 | 30 | tidy: 31 | # Run tidy whenever go.mod is changed 32 | go mod tidy 33 | 34 | protobuf: 35 | # Make sure you `brew install protobuf` first 36 | # go get -u github.com/golang/protobuf/protoc-gen-go 37 | protoc -I=./pkg/sloop/store/typed/ --go_out=./pkg/sloop/store/typed/ ./pkg/sloop/store/typed/schema.proto 38 | 39 | cover: 40 | go test ./pkg/... -coverprofile=coverage.out 41 | go tool cover -html=coverage.out 42 | 43 | release: 44 | @if [ ! -z "$(GITHUB_TOKEN)" ];then \ 45 | curl -sfL https://git.io/goreleaser | sh -s -- release --rm-dist;\ 46 | else \ 47 | curl -sfL https://git.io/goreleaser | sh -s -- release --rm-dist --skip-publish && \ 48 | docker push sloopimage/sloop;\ 49 | fi 50 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security 2 | 3 | Please report any security issue to [security@salesforce.com](mailto:security@salesforce.com) 4 | as soon as it is discovered. This application limits its runtime dependencies in 5 | order to reduce the total cost of ownership as much as can be, but all consumers 6 | should remain vigilant and have their security stakeholders review all third-party 7 | products (3PP) like this one and their dependencies. 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/salesforce/sloop 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/Jeffail/gabs/v2 v2.2.0 7 | github.com/dgraph-io/badger/v2 v2.0.3 8 | github.com/diegoholiveira/jsonlogic/v3 v3.5.3 9 | github.com/ghodss/yaml v1.0.0 10 | github.com/golang/glog v1.0.0 11 | github.com/golang/protobuf v1.5.3 12 | github.com/jteeuwen/go-bindata v3.0.7+incompatible 13 | github.com/klauspost/compress v1.16.5 14 | github.com/nsf/jsondiff v0.0.0-20190712045011-8443391ee9b6 15 | github.com/pkg/errors v0.9.1 16 | github.com/prometheus/client_golang v1.14.0 17 | github.com/spf13/afero v1.2.2 18 | github.com/stretchr/testify v1.8.2 19 | golang.org/x/net v0.27.0 20 | k8s.io/api v0.28.6 21 | k8s.io/apiextensions-apiserver v0.0.0-20230112083153-33db789573b1 22 | k8s.io/apimachinery v0.28.6 23 | k8s.io/client-go v0.28.6 24 | ) 25 | 26 | require ( 27 | github.com/DataDog/zstd v1.4.5 // indirect 28 | github.com/barkimedes/go-deepcopy v0.0.0-20220514131651-17c30cfc62df // indirect 29 | github.com/beorn7/perks v1.0.1 // indirect 30 | github.com/cespare/xxhash v1.1.0 // indirect 31 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 32 | github.com/davecgh/go-spew v1.1.1 // indirect 33 | github.com/dgraph-io/ristretto v0.0.2 // indirect 34 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect 35 | github.com/dustin/go-humanize v1.0.0 // indirect 36 | github.com/emicklei/go-restful/v3 v3.9.0 // indirect 37 | github.com/evanphx/json-patch v4.12.0+incompatible // indirect 38 | github.com/go-logr/logr v1.2.4 // indirect 39 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 40 | github.com/go-openapi/jsonreference v0.20.2 // indirect 41 | github.com/go-openapi/swag v0.22.3 // indirect 42 | github.com/gogo/protobuf v1.3.2 // indirect 43 | github.com/golang/snappy v0.0.1 // indirect 44 | github.com/google/gnostic-models v0.6.8 // indirect 45 | github.com/google/go-cmp v0.5.9 // indirect 46 | github.com/google/gofuzz v1.2.0 // indirect 47 | github.com/google/uuid v1.3.0 // indirect 48 | github.com/imdario/mergo v0.3.8 // indirect 49 | github.com/josharian/intern v1.0.0 // indirect 50 | github.com/json-iterator/go v1.1.12 // indirect 51 | github.com/mailru/easyjson v0.7.7 // indirect 52 | github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect 53 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 54 | github.com/modern-go/reflect2 v1.0.2 // indirect 55 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 56 | github.com/pmezard/go-difflib v1.0.0 // indirect 57 | github.com/prometheus/client_model v0.3.0 // indirect 58 | github.com/prometheus/common v0.37.0 // indirect 59 | github.com/prometheus/procfs v0.8.0 // indirect 60 | github.com/spf13/pflag v1.0.5 // indirect 61 | golang.org/x/oauth2 v0.8.0 // indirect 62 | golang.org/x/sys v0.22.0 // indirect 63 | golang.org/x/term v0.22.0 // indirect 64 | golang.org/x/text v0.16.0 // indirect 65 | golang.org/x/time v0.3.0 // indirect 66 | google.golang.org/appengine v1.6.7 // indirect 67 | google.golang.org/protobuf v1.31.0 // indirect 68 | gopkg.in/inf.v0 v0.9.1 // indirect 69 | gopkg.in/yaml.v2 v2.4.0 // indirect 70 | gopkg.in/yaml.v3 v3.0.1 // indirect 71 | k8s.io/klog/v2 v2.100.1 // indirect 72 | k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect 73 | k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect 74 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 75 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 76 | sigs.k8s.io/yaml v1.3.0 // indirect 77 | ) 78 | -------------------------------------------------------------------------------- /helm/sloop/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /helm/sloop/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | appVersion: "1.0" 3 | description: Sloop is a kubernetes history visualization tool. 4 | name: sloop 5 | version: 0.2.0 6 | icon: https://raw.githubusercontent.com/salesforce/sloop/master/other/sloop_logo_color.png 7 | -------------------------------------------------------------------------------- /helm/sloop/README.md: -------------------------------------------------------------------------------- 1 | # Helm with tiller installation instructions 2 | 1. `cd ./helm` 3 | 1. `helm install ./sloop` 4 | 5 | # This is instructions for folks NOT using tiller (or Spinnaker) for deployment 6 | 1. Build image and tag, and replace the current ones in values.yaml, we are working on a common image for people to use, it will be release in a few days. 7 | 1. (Optional) Create a namespace in your cluster for sloop to run if you dont have any yet, for example: `kubectl create namespace sloop ` 8 | 1. (Optional) Examines a chart for possible issues: `helm lint .` 9 | 1. Validate helm chart in local when making any helm changes:`helm template .` 10 | 1. Write to yaml file: `helm template . --namespace sloop> sloop-test.yaml` 11 | 1. Apply the yaml file in cluster: `kubectl -n sloop apply -f sloop-test.yaml` 12 | 1. Check if the service is running: `kubectl -n sloop get pods` 13 | 1. (Optional) Use port-forward for debugging: `kubectl port-forward -n sloop service/sloop 8080:80` 14 | 1. In your browser, hit `localhost:8080` to see the result, you can use sloop test data to check the view 15 | 16 | ![SloopTestData](/other/sloop-test.png?raw=true "SloopTestData") 17 | -------------------------------------------------------------------------------- /helm/sloop/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: {{ .Values.name }} 6 | helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} 7 | app.kubernetes.io/managed-by: {{ .Release.Service }} 8 | app.kubernetes.io/instance: {{ .Release.Name }} 9 | name: {{ .Values.name }} 10 | rules: 11 | - apiGroups: 12 | - "" 13 | resources: 14 | - configmaps 15 | - endpoints 16 | - events 17 | - limitranges 18 | - namespaces 19 | - nodes 20 | - persistentvolumeclaims 21 | - persistentvolumes 22 | - pods 23 | - replicationcontrollers 24 | - resourcequotas 25 | - services 26 | verbs: 27 | - list 28 | - watch 29 | - apiGroups: 30 | - batch 31 | resources: 32 | - cronjobs 33 | - jobs 34 | verbs: 35 | - list 36 | - watch 37 | - apiGroups: 38 | - extensions 39 | - apps 40 | resources: 41 | - daemonsets 42 | - deployments 43 | - replicasets 44 | - statefulsets 45 | verbs: 46 | - list 47 | - watch 48 | - apiGroups: 49 | - autoscaling 50 | resources: 51 | - horizontalpodautoscalers 52 | verbs: 53 | - list 54 | - watch 55 | - apiGroups: 56 | - extensions 57 | - networking.k8s.io 58 | resources: 59 | - ingress 60 | verbs: 61 | - list 62 | - watch 63 | - apiGroups: 64 | - policy 65 | resources: 66 | - poddisruptionbudgets 67 | verbs: 68 | - list 69 | - watch 70 | - apiGroups: 71 | - storage.k8s.io 72 | resources: 73 | - storageclasses 74 | verbs: 75 | - list 76 | - watch 77 | - apiGroups: 78 | - autoscaling.k8s.io 79 | resources: 80 | - verticalpodautoscalers 81 | verbs: 82 | - list 83 | - watch 84 | - apiGroups: 85 | - apiextensions.k8s.io 86 | resources: 87 | - customresourcedefinitions 88 | verbs: 89 | - list 90 | - watch 91 | {{- with .Values.clusterRole.apiGroups }} 92 | - apiGroups: 93 | {{- range . }} 94 | - {{ . | quote }} 95 | {{- end }} 96 | resources: 97 | - '*' 98 | verbs: 99 | - list 100 | - watch 101 | {{- end }} 102 | -------------------------------------------------------------------------------- /helm/sloop/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: {{ .Values.name }} 6 | helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} 7 | app.kubernetes.io/managed-by: {{ .Release.Service }} 8 | app.kubernetes.io/instance: {{ .Release.Name }} 9 | name: {{ .Values.name }} 10 | roleRef: 11 | apiGroup: rbac.authorization.k8s.io 12 | kind: ClusterRole 13 | name: {{ .Values.name }} 14 | subjects: 15 | - 16 | kind: ServiceAccount 17 | name: {{ .Values.serviceAccountName }} 18 | namespace: {{ .Release.Namespace }} 19 | -------------------------------------------------------------------------------- /helm/sloop/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: {{ .Values.name }} 6 | helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 7 | app.kubernetes.io/instance: "{{ .Release.Name }}" 8 | app.kubernetes.io/managed-by: "{{ .Release.Service }}" 9 | name: {{ .Values.name }} 10 | data: 11 | sloop.json: |- 12 | {{ .Values.config | indent 4 }} 13 | -------------------------------------------------------------------------------- /helm/sloop/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled }} 2 | apiVersion: networking.k8s.io/v1 3 | kind: Ingress 4 | metadata: 5 | annotations: 6 | {{- range $key, $value := .Values.ingress.annotations }} 7 | {{ $key }}: {{ $value | quote }} 8 | {{- end }} 9 | labels: 10 | app.kubernetes.io/name: {{ .Values.name }} 11 | helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 12 | app.kubernetes.io/instance: "{{ .Release.Name }}" 13 | app.kubernetes.io/managed-by: "{{ .Release.Service }}" 14 | name: {{ .Values.name }} 15 | spec: 16 | {{- if .Values.ingress.className }} 17 | ingressClassName: {{ .Values.ingress.className }} 18 | {{- end }} 19 | rules: 20 | - host: {{ .Values.ingress.host }} 21 | http: 22 | paths: 23 | - backend: 24 | service: 25 | name: {{ .Values.name }} 26 | port: 27 | number: {{ .Values.service.port }} 28 | path: / 29 | pathType: Prefix 30 | {{- end -}} 31 | {{- if .Values.ingress.tls }} 32 | tls: 33 | {{ toYaml .Values.ingress.tls | indent 4 }} 34 | {{- end -}} 35 | -------------------------------------------------------------------------------- /helm/sloop/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: {{ .Values.name }} 6 | helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 7 | app.kubernetes.io/instance: "{{ .Release.Name }}" 8 | app.kubernetes.io/managed-by: "{{ .Release.Service }}" 9 | name: {{ .Values.name }} 10 | spec: 11 | ports: 12 | - name: sloop-port 13 | port: {{ .Values.service.port }} 14 | protocol: TCP 15 | targetPort: {{ .Values.service.targetPort }} 16 | selector: 17 | app.kubernetes.io/name: {{ .Values.name }} 18 | type: {{ .Values.service.type }} 19 | -------------------------------------------------------------------------------- /helm/sloop/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: {{ .Values.name }} 6 | helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }} 7 | app.kubernetes.io/managed-by: {{ .Release.Service }} 8 | app.kubernetes.io/instance: {{ .Release.Name }} 9 | name: {{ .Values.name }} 10 | -------------------------------------------------------------------------------- /helm/sloop/templates/statefulset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: {{ .Values.name }} 6 | helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" 7 | app.kubernetes.io/instance: "{{ .Release.Name }}" 8 | app.kubernetes.io/managed-by: "{{ .Release.Service }}" 9 | name: {{ .Values.name }} 10 | spec: 11 | serviceName: {{ .Values.name }} 12 | replicas: {{ .Values.replicas }} 13 | selector: 14 | matchLabels: 15 | app.kubernetes.io/name: {{ .Values.name }} 16 | template: 17 | metadata: 18 | labels: 19 | app.kubernetes.io/name: {{ .Values.name }} 20 | app.kubernetes.io/instance: "{{ .Release.Name }}" 21 | {{- with .Values.podLabels -}} 22 | {{ toYaml . | nindent 8 }} 23 | {{- end }} 24 | annotations: 25 | {{ toYaml .Values.podAnnotations | indent 8 }} 26 | spec: 27 | tolerations: 28 | {{ toYaml .Values.tolerations | indent 8 }} 29 | containers: 30 | - args: 31 | - --config=/sloopconfig/sloop.json 32 | command: 33 | - /sloop 34 | image: {{ .Values.image.repository }}:{{ .Values.image.tag }} 35 | imagePullPolicy: {{ .Values.image.pullPolicy }} 36 | name: sloop 37 | ports: 38 | - containerPort: {{ .Values.service.targetPort }} 39 | protocol: TCP 40 | {{- if .Values.livenessProbe.enabled }} 41 | livenessProbe: 42 | httpGet: 43 | path: /healthz 44 | port: {{ .Values.service.targetPort }} 45 | initialDelaySeconds: {{ .Values.livenessProbe.initialDelaySeconds }} 46 | periodSeconds: {{ .Values.livenessProbe.periodSeconds }} 47 | timeoutSeconds: {{ .Values.livenessProbe.timeoutSeconds }} 48 | successThreshold: {{ .Values.livenessProbe.successThreshold }} 49 | failureThreshold: {{ .Values.livenessProbe.failureThreshold }} 50 | {{- end }} 51 | {{- if .Values.readinessProbe.enabled }} 52 | readinessProbe: 53 | httpGet: 54 | path: /healthz 55 | port: {{ .Values.service.targetPort }} 56 | initialDelaySeconds: {{ .Values.readinessProbe.initialDelaySeconds }} 57 | periodSeconds: {{ .Values.readinessProbe.periodSeconds }} 58 | timeoutSeconds: {{ .Values.readinessProbe.timeoutSeconds }} 59 | successThreshold: {{ .Values.readinessProbe.successThreshold }} 60 | failureThreshold: {{ .Values.readinessProbe.failureThreshold }} 61 | {{- end }} 62 | resources: 63 | {{ toYaml .Values.resources | indent 10 }} 64 | volumeMounts: 65 | - mountPath: /data/ 66 | name: sloop-data 67 | - mountPath: /sloopconfig/ 68 | name: sloopconfig 69 | {{- with .Values.image.pullSecrets }} 70 | imagePullSecrets: 71 | {{- toYaml . | nindent 8 }} 72 | {{- end }} 73 | volumes: 74 | - emptyDir: 75 | sizeLimit: {{ .Values.persistentVolume.sizeLimit }} 76 | name: sloop-data 77 | - configMap: 78 | name: {{ .Values.name }} 79 | name: sloopconfig 80 | serviceAccountName: {{ .Values.serviceAccountName }} 81 | {{- if .Values.persistentVolume.enabled }} 82 | volumeClaimTemplates: 83 | - metadata: 84 | name: sloop-data 85 | spec: 86 | accessModes: 87 | - {{ .Values.persistentVolume.dataAccessMode | quote }} 88 | {{- if .Values.persistentVolume.storageClass }} 89 | storageClassName: "{{ .Values.persistentVolume.storageClass }}" 90 | {{- end }} 91 | resources: 92 | requests: 93 | storage: {{ .Values.persistentVolume.size }} 94 | {{- end }} 95 | -------------------------------------------------------------------------------- /helm/sloop/values.yaml: -------------------------------------------------------------------------------- 1 | serviceAccountName: sloop 2 | ## If defined, specifies the tolerations to attach to the pods 3 | tolerations: [] 4 | ## If defined, specifies the labels and/or annotations to attach to the pods 5 | podLabels: {} 6 | podAnnotations: {} 7 | name: sloop 8 | replicas: 1 9 | image: 10 | tag: latest 11 | repository: ghcr.io/salesforce/sloop 12 | pullPolicy: IfNotPresent 13 | # pullSecrets: 14 | # - name: example-secret 15 | persistentVolume: 16 | # If false then an emptyDir will be used. 17 | enabled: true 18 | ## If defined it will specify the storageClass for the statefulSet 19 | ## If undefined it will use the cluster default (typically gp2 on AWS, standard on GKE) 20 | # storageClass: 21 | size: 10Gi 22 | dataAccessMode: ReadWriteOnce 23 | sizeLimit: 12Gi 24 | resources: {} 25 | service: 26 | port: 80 27 | targetPort: 8080 28 | type: ClusterIP 29 | ## Configure extra options for liveness and readiness probes 30 | livenessProbe: 31 | enabled: true 32 | initialDelaySeconds: 30 33 | periodSeconds: 10 34 | timeoutSeconds: 5 35 | failureThreshold: 3 36 | successThreshold: 1 37 | readinessProbe: 38 | enabled: true 39 | initialDelaySeconds: 5 40 | periodSeconds: 10 41 | timeoutSeconds: 5 42 | failureThreshold: 3 43 | successThreshold: 1 44 | ingress: 45 | enabled: false 46 | host: sloop.example.com 47 | annotations: {} 48 | tls: {} 49 | # className: nginx 50 | config: |- 51 | { 52 | "displayContext": "cluster", 53 | "defaultNamespace": "default", 54 | "defaultKind": "Pod", 55 | "defaultLookback": "1h", 56 | "leftBarLinks": [ 57 | ], 58 | "resourceLinks": [ 59 | ] 60 | } 61 | 62 | clusterRole: 63 | # List CRD apiGroups that you want to watch here 64 | # Istio is just here as an example. Alternatively if you want to get all CRDs its possible to add '*' 65 | # to apiGroups, but that would give sloop permissions to read secrets which may not be desirable 66 | apiGroups: 67 | - authentication.istio.io 68 | - config.istio.io 69 | - networking.istio.io 70 | - rbac.istio.io 71 | - security.istio.io 72 | -------------------------------------------------------------------------------- /other/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/sloop/588295c21b968ce93e461103d0d55f149f6bc94c/other/architecture.png -------------------------------------------------------------------------------- /other/data_distribution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/sloop/588295c21b968ce93e461103d0d55f149f6bc94c/other/data_distribution.png -------------------------------------------------------------------------------- /other/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/sloop/588295c21b968ce93e461103d0d55f149f6bc94c/other/screenshot1.png -------------------------------------------------------------------------------- /other/sloop-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/sloop/588295c21b968ce93e461103d0d55f149f6bc94c/other/sloop-test.png -------------------------------------------------------------------------------- /other/sloop_logo_black.eps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/sloop/588295c21b968ce93e461103d0d55f149f6bc94c/other/sloop_logo_black.eps -------------------------------------------------------------------------------- /other/sloop_logo_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/sloop/588295c21b968ce93e461103d0d55f149f6bc94c/other/sloop_logo_black.png -------------------------------------------------------------------------------- /other/sloop_logo_color.eps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/sloop/588295c21b968ce93e461103d0d55f149f6bc94c/other/sloop_logo_color.eps -------------------------------------------------------------------------------- /other/sloop_logo_color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/sloop/588295c21b968ce93e461103d0d55f149f6bc94c/other/sloop_logo_color.png -------------------------------------------------------------------------------- /other/sloop_logo_color_small_notext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/sloop/588295c21b968ce93e461103d0d55f149f6bc94c/other/sloop_logo_color_small_notext.png -------------------------------------------------------------------------------- /other/sloop_logo_white.eps: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/sloop/588295c21b968ce93e461103d0d55f149f6bc94c/other/sloop_logo_white.eps -------------------------------------------------------------------------------- /other/sloop_logo_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/sloop/588295c21b968ce93e461103d0d55f149f6bc94c/other/sloop_logo_white.png -------------------------------------------------------------------------------- /pkg/sloop/common/constants.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | const ( 4 | GlogVerbose = 10 5 | ) 6 | -------------------------------------------------------------------------------- /pkg/sloop/common/db_utilities.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/dgraph-io/badger/v2" 5 | "github.com/golang/glog" 6 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 7 | ) 8 | 9 | func deleteKeys(db badgerwrap.DB, keysForDelete [][]byte) (error, uint64) { 10 | var deletedKeysInThisBatch uint64 = 0 11 | err := db.Update(func(txn badgerwrap.Txn) error { 12 | for _, key := range keysForDelete { 13 | err := txn.Delete(key) 14 | if err != nil { 15 | return err 16 | } 17 | deletedKeysInThisBatch++ 18 | } 19 | return nil 20 | }) 21 | 22 | if err != nil { 23 | return err, deletedKeysInThisBatch 24 | } 25 | 26 | return nil, deletedKeysInThisBatch 27 | } 28 | 29 | // deletes the keys with a given prefix 30 | func DeleteKeysWithPrefix(keyPrefix string, db badgerwrap.DB, deletionBatchSize int, numOfKeysToDelete uint64) (error, uint64, uint64) { 31 | 32 | // as deletion does not lock db there is a possibility that the keys for a given prefix are added while old ones are deleted. In this case it can get into a race condition. 33 | // In order to avoid this, count of existing keys is used which match the given prefix and deletion ends when this number of keys have been deleted 34 | 35 | var numOfKeysDeleted uint64 = 0 36 | for numOfKeysDeleted < numOfKeysToDelete { 37 | 38 | keysThisBatch := make([][]byte, 0, deletionBatchSize) 39 | 40 | // getting the keys to delete that have the given prefix 41 | _ = db.View(func(txn badgerwrap.Txn) error { 42 | iterOpt := badger.DefaultIteratorOptions 43 | iterOpt.PrefetchValues = false 44 | iterOpt.InternalAccess = true 45 | it := txn.NewIterator(iterOpt) 46 | defer it.Close() 47 | 48 | // TODO: Investigate if Seek() can be used instead of rewind 49 | for it.Rewind(); it.ValidForPrefix([]byte(keyPrefix)) || it.ValidForPrefix([]byte("!badger!move"+keyPrefix)); it.Next() { 50 | keyToDel := it.Item().KeyCopy(nil) 51 | keysThisBatch = append(keysThisBatch, keyToDel) 52 | if len(keysThisBatch) == deletionBatchSize { 53 | break 54 | } 55 | 56 | } 57 | return nil 58 | }) 59 | 60 | // deleting the keys in batch 61 | if len(keysThisBatch) > 0 { 62 | err, deletedKeysInThisBatch := deleteKeys(db, keysThisBatch) 63 | numOfKeysDeleted += deletedKeysInThisBatch 64 | if err != nil { 65 | glog.Errorf("Error encountered while deleting keys with prefix: '%v', numberOfKeysDeleted: '%v' numOfKeysToDelete: '%v'", keyPrefix, numOfKeysDeleted, numOfKeysToDelete) 66 | return err, numOfKeysDeleted, numOfKeysToDelete 67 | } 68 | } 69 | } 70 | return nil, numOfKeysDeleted, numOfKeysToDelete 71 | } 72 | 73 | // returns the number of keys in DB with given prefix. If prefix is not provided it gives count of all keys 74 | func GetTotalKeyCount(db badgerwrap.DB, keyPrefix string) uint64 { 75 | var totalKeyCount uint64 = 0 76 | keyPrefixToMatch := []byte(keyPrefix) 77 | _ = db.View(func(txn badgerwrap.Txn) error { 78 | iterOpt := badger.DefaultIteratorOptions 79 | iterOpt.PrefetchValues = false 80 | if len(keyPrefixToMatch) != 0 { 81 | iterOpt.Prefix = keyPrefixToMatch 82 | } 83 | it := txn.NewIterator(iterOpt) 84 | defer it.Close() 85 | for it.Rewind(); it.ValidForPrefix(keyPrefixToMatch); it.Next() { 86 | totalKeyCount++ 87 | } 88 | return nil 89 | }) 90 | return totalKeyCount 91 | } 92 | -------------------------------------------------------------------------------- /pkg/sloop/common/db_utilities_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dgraph-io/badger/v2" 7 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | var commonPrefix = "/commonprefix/001546405200/" 12 | 13 | func Test_Db_Utilities_DeleteKeysWithPrefix_DeleteAllKeys(t *testing.T) { 14 | db := helper_get_db(t) 15 | helper_add_keys_to_db(t, db, helper_testKeys_with_common_prefix(commonPrefix)) 16 | err, numOfDeletedKeys, numOfKeysToDelete := DeleteKeysWithPrefix(commonPrefix, db, 10, 4) 17 | assert.Nil(t, err) 18 | assert.Equal(t, uint64(4), numOfDeletedKeys) 19 | assert.Equal(t, uint64(4), numOfKeysToDelete) 20 | } 21 | 22 | func Test_Db_Utilities_DeleteKeysWithPrefix_DeleteNoKeys(t *testing.T) { 23 | db := helper_get_db(t) 24 | helper_add_keys_to_db(t, db, helper_testKeys_with_common_prefix(commonPrefix)) 25 | err, numOfDeletedKeys, numOfKeysToDelete := DeleteKeysWithPrefix(commonPrefix+"random", db, 10, 0) 26 | assert.Nil(t, err) 27 | assert.Equal(t, uint64(0), numOfDeletedKeys) 28 | assert.Equal(t, uint64(0), numOfKeysToDelete) 29 | } 30 | 31 | func Test_Db_Utilities_DeleteKeysWithPrefix_DeleteSomeKeys(t *testing.T) { 32 | db := helper_get_db(t) 33 | // DB has 8 keys 34 | helper_add_keys_to_db(t, db, helper_testKeys_with_common_prefix(commonPrefix)) 35 | helper_add_keys_to_db(t, db, helper_testKeys_with_common_prefix("randomStuff"+commonPrefix)) 36 | err, numOfDeletedKeys, numOfKeysToDelete := DeleteKeysWithPrefix(commonPrefix, db, 10, 4) 37 | assert.Nil(t, err) 38 | assert.Equal(t, uint64(4), numOfDeletedKeys) 39 | assert.Equal(t, uint64(4), numOfKeysToDelete) 40 | } 41 | 42 | func helper_get_db(t *testing.T) badgerwrap.DB { 43 | db, err := (&badgerwrap.MockFactory{}).Open(badger.DefaultOptions("")) 44 | assert.Nil(t, err) 45 | return db 46 | } 47 | 48 | func helper_add_keys_to_db(t *testing.T, db badgerwrap.DB, keys []string) badgerwrap.DB { 49 | err := db.Update(func(txn badgerwrap.Txn) error { 50 | var txerr error 51 | for _, key := range keys { 52 | txerr = txn.Set([]byte(key), []byte{}) 53 | if txerr != nil { 54 | return txerr 55 | } 56 | } 57 | return nil 58 | }) 59 | assert.Nil(t, err) 60 | return db 61 | } 62 | 63 | func helper_testKeys_with_common_prefix(prefix string) []string { 64 | return []string{ 65 | // someMaxTs partition 66 | prefix + "Pod/user-j/sync-123/sam-partition-testdata", 67 | prefix + "Pod/user-j/sync-123/sam-partition-test", 68 | prefix + "Pod/user-t/sync-123/sam-partition-testdata", 69 | prefix + "Pod/user-w/sync-123/sam-partition-test", 70 | } 71 | } 72 | 73 | func Test_Db_Utilities_GetTotalKeyCount_SomeKeys(t *testing.T) { 74 | db := helper_get_db(t) 75 | helper_add_keys_to_db(t, db, helper_testKeys_with_common_prefix(commonPrefix)) 76 | helper_add_keys_to_db(t, db, helper_testKeys_with_common_prefix("randomStuff"+commonPrefix)) 77 | 78 | numberOfKeys := GetTotalKeyCount(db, commonPrefix) 79 | // expected count is 4 as each call to helper_add_keys_to_db adds keys in 4 tables, only the common prefix ones would return 80 | expectedNumberOfKeys := 4 81 | assert.Equal(t, uint64(expectedNumberOfKeys), numberOfKeys) 82 | } 83 | 84 | func Test_Db_Utilities_GetTotalKeyCount_NoKeys(t *testing.T) { 85 | db := helper_get_db(t) 86 | numberOfKeys := GetTotalKeyCount(db, "") 87 | assert.Equal(t, uint64(0), numberOfKeys) 88 | } 89 | -------------------------------------------------------------------------------- /pkg/sloop/common/partition_utilities.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/dgraph-io/badger/v2" 7 | "github.com/golang/glog" 8 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 9 | ) 10 | 11 | type SloopKey struct { 12 | TableName string 13 | PartitionID string 14 | } 15 | 16 | // returns TableName, PartitionId, error. 17 | func GetSloopKey(item badgerwrap.Item) (SloopKey, error) { 18 | key := item.Key() 19 | err, parts := ParseKey(string(key)) 20 | if err != nil { 21 | return SloopKey{}, err 22 | } 23 | 24 | var tableName = parts[1] 25 | var partitionId = parts[2] 26 | return SloopKey{tableName, partitionId}, nil 27 | } 28 | 29 | type PartitionInfo struct { 30 | TotalKeyCount uint64 31 | TableNameToKeyCountMap map[string]uint64 32 | } 33 | 34 | // prints all the keys histogram. It can help debugging when needed. 35 | func PrintKeyHistogram(db badgerwrap.DB) { 36 | partitionTableNameToKeyCountMap, totalKeyCount := GetPartitionsInfo(db) 37 | glog.V(2).Infof("TotalkeyCount: %v", totalKeyCount) 38 | 39 | for partitionID, partitionInfo := range partitionTableNameToKeyCountMap { 40 | for tableName, keyCount := range partitionInfo.TableNameToKeyCountMap { 41 | glog.V(2).Infof("TableName: %v, PartitionId: %v, keyCount: %v", tableName, partitionID, keyCount) 42 | } 43 | } 44 | } 45 | 46 | // Returns the sorted list of partitionIDs from the given partitions Info map 47 | func GetSortedPartitionIDs(partitionsInfoMap map[string]*PartitionInfo) []string { 48 | var sortedListOfPartitionIds []string 49 | 50 | for partitionID, _ := range partitionsInfoMap { 51 | sortedListOfPartitionIds = append(sortedListOfPartitionIds, partitionID) 52 | } 53 | 54 | // Sorted numbered strings here is ok since they are all of the same length 55 | sort.Strings(sortedListOfPartitionIds) 56 | return sortedListOfPartitionIds 57 | } 58 | 59 | // Gets the Information for partitions to key Count Map 60 | // Returns Partitions to KeyCount Map, Partitions TableName to Key Count and total key count 61 | func GetPartitionsInfo(db badgerwrap.DB) (map[string]*PartitionInfo, uint64) { 62 | var totalKeyCount uint64 = 0 63 | partitionIDToPartitionInfoMap := make(map[string]*PartitionInfo) 64 | 65 | _ = db.View(func(txn badgerwrap.Txn) error { 66 | iterOpt := badger.DefaultIteratorOptions 67 | iterOpt.PrefetchValues = false 68 | it := txn.NewIterator(iterOpt) 69 | defer it.Close() 70 | for it.Rewind(); it.Valid(); it.Next() { 71 | item := it.Item() 72 | sloopKey, err := GetSloopKey(item) 73 | if err != nil { 74 | glog.Errorf("failed to parse information about key: %x", item.Key()) 75 | continue 76 | } 77 | if partitionIDToPartitionInfoMap[sloopKey.PartitionID] == nil { 78 | partitionIDToPartitionInfoMap[sloopKey.PartitionID] = &PartitionInfo{0, make(map[string]uint64)} 79 | } 80 | 81 | partitionIDToPartitionInfoMap[sloopKey.PartitionID].TotalKeyCount++ 82 | partitionIDToPartitionInfoMap[sloopKey.PartitionID].TableNameToKeyCountMap[sloopKey.TableName]++ 83 | totalKeyCount++ 84 | } 85 | return nil 86 | }) 87 | 88 | return partitionIDToPartitionInfoMap, totalKeyCount 89 | } 90 | 91 | // Return all keys within a partition with the given keyPrefix 92 | func GetKeysForPrefix(db badgerwrap.DB, keyPrefix string) []string { 93 | var keys []string 94 | keyPrefixToMatch := []byte(keyPrefix) 95 | _ = db.View(func(txn badgerwrap.Txn) error { 96 | iterOpt := badger.DefaultIteratorOptions 97 | iterOpt.PrefetchValues = false 98 | if len(keyPrefixToMatch) != 0 { 99 | iterOpt.Prefix = keyPrefixToMatch 100 | } 101 | it := txn.NewIterator(iterOpt) 102 | defer it.Close() 103 | for it.Rewind(); it.ValidForPrefix(keyPrefixToMatch); it.Next() { 104 | keys = append(keys, string(it.Item().Key())) 105 | } 106 | return nil 107 | }) 108 | return keys 109 | } 110 | -------------------------------------------------------------------------------- /pkg/sloop/common/utilities.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "strings" 7 | ) 8 | 9 | func BoolToFloat(value bool) float64 { 10 | if value { 11 | return 1 12 | } 13 | return 0 14 | } 15 | 16 | func ParseKey(key string) (error, []string) { 17 | parts := strings.Split(key, "/") 18 | if len(parts) != 7 { 19 | return fmt.Errorf("key should have 6 parts: %v", key), parts 20 | } 21 | if parts[0] != "" { 22 | return fmt.Errorf("key should start with /: %v", key), parts 23 | } 24 | 25 | return nil, parts 26 | } 27 | 28 | func Contains(stringList []string, elem string) bool { 29 | for _, str := range stringList { 30 | if str == elem { 31 | return true 32 | } 33 | } 34 | return false 35 | } 36 | 37 | func GetFilePath(filePath string, fileName string) string { 38 | return path.Join(filePath, fileName) 39 | } 40 | 41 | func Max(x int, y int) int { 42 | if x < y { 43 | return y 44 | } 45 | return x 46 | } 47 | 48 | func Truncate(text string, width int, delimiter ...string) (string, error) { 49 | d := "..." 50 | if len(delimiter) > 0 { 51 | d = delimiter[0] 52 | } 53 | d_len := len(d) 54 | if width < 0 { 55 | return "", fmt.Errorf("invalid width") 56 | } 57 | if len(text) <= width { 58 | return text, nil 59 | } 60 | r := []rune(text) 61 | truncated := r[:(Max(width, d_len) - d_len)] 62 | return string(truncated) + d, nil 63 | } 64 | -------------------------------------------------------------------------------- /pkg/sloop/common/utilities_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | var files = []string{"webfiles/index.html", "webfiles/debug.html", "webfiles/filter.js", "webfiles/sloop.cs"} 12 | var filePath = "webfiles/index.html" 13 | var filePath2 = "webfiles/sloop.css" 14 | var fileName = "index.html" 15 | var filePrefix = "webfiles/" 16 | 17 | func Test_boolToFloat(t *testing.T) { 18 | assert.Equal(t, float64(1), BoolToFloat(true)) 19 | assert.Equal(t, float64(0), BoolToFloat(false)) 20 | } 21 | 22 | func Test_ParseKey_2_Parts(t *testing.T) { 23 | keyWith2Parts := "/part1/part2" 24 | err, _ := ParseKey(keyWith2Parts) 25 | 26 | assert.NotNil(t, err) 27 | assert.Equal(t, fmt.Errorf("key should have 6 parts: %v", keyWith2Parts), err) 28 | } 29 | 30 | func Test_ParseKey_Start_Parts(t *testing.T) { 31 | keyWith2Parts := "part1/part2/part3/part4/part5/part6/part7" 32 | err, _ := ParseKey(keyWith2Parts) 33 | 34 | assert.NotNil(t, err) 35 | assert.Equal(t, fmt.Errorf("key should start with /: %v", keyWith2Parts), err) 36 | } 37 | 38 | func Test_ParseKey_Success(t *testing.T) { 39 | keyWith2Parts := "/part1/part2/part3/part4/part5/part6" 40 | err, parts := ParseKey(keyWith2Parts) 41 | 42 | assert.Nil(t, err) 43 | assert.Equal(t, 7, len(parts)) 44 | } 45 | 46 | func Test_FileExistsInList_True(t *testing.T) { 47 | expectedOutput := true 48 | actualOutput := Contains(files, filePath) 49 | assert.Equal(t, expectedOutput, actualOutput) 50 | } 51 | 52 | func Test_FileExistsInList_False(t *testing.T) { 53 | expectedOutput := false 54 | actualOutput := Contains(files, filePath2) 55 | assert.Equal(t, expectedOutput, actualOutput) 56 | } 57 | 58 | func Test_GetFilePath(t *testing.T) { 59 | expectedOutput := path.Join(filePrefix, fileName) 60 | actualOutput := GetFilePath(filePrefix, fileName) 61 | assert.Equal(t, expectedOutput, actualOutput) 62 | } 63 | 64 | func Test_Truncate_StringLongerThanWidth(t *testing.T) { 65 | stringLong := "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec eget odio quis felis laoreet dictum." 66 | expectedOutput := "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec eget odio quis..." 67 | actualOutput, _ := Truncate(stringLong, 80) 68 | assert.Equal(t, expectedOutput, actualOutput) 69 | } 70 | 71 | func Test_Truncate_StringShorterThanWidth(t *testing.T) { 72 | stringMedium := "Lorem ipsum dolor" 73 | expectedOutput := "Lorem ipsum dolor" 74 | actualOutput, _ := Truncate(stringMedium, 80) 75 | assert.Equal(t, expectedOutput, actualOutput) 76 | } 77 | 78 | func Test_Truncate_WidthShorterThanDelimiter(t *testing.T) { 79 | stringShort := "Lorem" 80 | expectedOutput := "..." 81 | actualOutput, _ := Truncate(stringShort, 1) 82 | assert.Equal(t, expectedOutput, actualOutput) 83 | } 84 | 85 | func Test_Truncate_StringEmpty(t *testing.T) { 86 | stringEmpty := "" 87 | expectedOutput := "" 88 | actualOutput, _ := Truncate(stringEmpty, 1) 89 | assert.Equal(t, expectedOutput, actualOutput) 90 | } 91 | -------------------------------------------------------------------------------- /pkg/sloop/ingress/dbrestore.go: -------------------------------------------------------------------------------- 1 | package ingress 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | 7 | "github.com/pkg/errors" 8 | 9 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 10 | ) 11 | 12 | // DatabaseRestore restores the DB from a backup file created by webserver.backupHandler 13 | func DatabaseRestore(db badgerwrap.DB, filename string) error { 14 | file, err := os.Open(filename) 15 | if err != nil { 16 | return errors.Wrapf(err, "failed to load database restore file: %q", filename) 17 | } 18 | defer file.Close() 19 | 20 | err = db.Load(file, runtime.NumCPU()) 21 | if err != nil { 22 | return errors.Wrapf(err, "failed to restore database from file: %q", filename) 23 | } 24 | 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /pkg/sloop/ingress/fileplayback.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package ingress 9 | 10 | import ( 11 | "io/ioutil" 12 | 13 | "github.com/ghodss/yaml" 14 | "github.com/golang/glog" 15 | "github.com/salesforce/sloop/pkg/sloop/store/typed" 16 | ) 17 | 18 | func PlayFile(outChan chan typed.KubeWatchResult, filename string) error { 19 | b, err := ioutil.ReadFile(filename) 20 | if err != nil { 21 | panic(err) 22 | } 23 | var playbackFile KubePlaybackFile 24 | err = yaml.Unmarshal(b, &playbackFile) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | glog.Infof("Loaded %v resources from file source %v", len(playbackFile.Data), filename) 30 | 31 | for _, watchRecord := range playbackFile.Data { 32 | outChan <- watchRecord 33 | } 34 | glog.Infof("Done writing kubeWatch events to channel") 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/sloop/ingress/filerecorder.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package ingress 9 | 10 | import ( 11 | "io/ioutil" 12 | "sync" 13 | 14 | "github.com/ghodss/yaml" 15 | "github.com/golang/glog" 16 | "github.com/salesforce/sloop/pkg/sloop/store/typed" 17 | ) 18 | 19 | type FileRecorder struct { 20 | inChan chan typed.KubeWatchResult 21 | data []typed.KubeWatchResult 22 | filename string 23 | wg sync.WaitGroup // Ensure we don't call close at the same time we are taking in events 24 | } 25 | 26 | func NewFileRecorder(filename string, inChan chan typed.KubeWatchResult) *FileRecorder { 27 | fr := &FileRecorder{filename: filename, inChan: inChan} 28 | return fr 29 | } 30 | 31 | func (fr *FileRecorder) Start() { 32 | fr.wg.Add(1) 33 | go fr.listen(fr.inChan) 34 | } 35 | 36 | func (fr *FileRecorder) listen(inChan chan typed.KubeWatchResult) { 37 | for { 38 | newRecord, more := <-inChan 39 | if !more { 40 | fr.wg.Done() 41 | return 42 | } 43 | fr.data = append(fr.data, newRecord) 44 | } 45 | } 46 | 47 | func (fr *FileRecorder) Close() error { 48 | fr.wg.Wait() 49 | f := KubePlaybackFile{Data: fr.data} 50 | byteData, err := yaml.Marshal(f) 51 | if err != nil { 52 | return err 53 | } 54 | err = ioutil.WriteFile(fr.filename, byteData, 0755) 55 | glog.Infof("Wrote %v records to %v. err %v", len(fr.data), fr.filename, err) 56 | return err 57 | } 58 | -------------------------------------------------------------------------------- /pkg/sloop/ingress/kubeclient.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package ingress 9 | 10 | import ( 11 | "github.com/golang/glog" 12 | "k8s.io/client-go/kubernetes" 13 | _ "k8s.io/client-go/plugin/pkg/client/auth/azure" 14 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 15 | "k8s.io/client-go/rest" 16 | "k8s.io/client-go/tools/clientcmd" 17 | "k8s.io/client-go/tools/clientcmd/api" 18 | ) 19 | 20 | // Variables assigned to facilitate unit testing 21 | 22 | var BuildConfigFromFlags = clientcmd.BuildConfigFromFlags 23 | var ClientConfig = clientcmd.ClientConfig.ClientConfig 24 | var RawConfig = clientcmd.ClientConfig.RawConfig 25 | 26 | // GetKubernetesContext takes optional user preferences and returns the Kubernetes context in use 27 | func GetKubernetesContext(masterURL string, kubeContextPreference string, privilegedAccess bool) (string, error) { 28 | glog.Infof("Getting k8s context with user-defined config masterURL=%v, kubeContextPreference=%v.", masterURL, kubeContextPreference) 29 | contextInUse := kubeContextPreference 30 | if privilegedAccess { 31 | clientConfig := getConfig(masterURL, kubeContextPreference) 32 | // This tells us the currentContext defined in the kubeConfig which gets used if we dont have an override 33 | rawConfig, err := RawConfig(clientConfig) 34 | if err != nil { 35 | return "", err 36 | } 37 | 38 | contextInUse = rawConfig.CurrentContext 39 | if kubeContextPreference != "" { 40 | contextInUse = kubeContextPreference 41 | } 42 | } 43 | 44 | glog.Infof("Get k8s context with context=%v", contextInUse) 45 | return contextInUse, nil 46 | } 47 | 48 | // MakeKubernetesClient takes masterURL and kubeContext (user preference should have already been resolved before calling this) 49 | // and returns a K8s client 50 | func MakeKubernetesClient(masterURL string, kubeContext string, privilegedAccess bool) (kubernetes.Interface, error) { 51 | glog.Infof("Creating k8sclient with user-defined config masterURL=%v, kubeContext=%v.", masterURL, kubeContext) 52 | var config *rest.Config 53 | var err error 54 | if privilegedAccess { 55 | clientConfig := getConfig(masterURL, kubeContext) 56 | config, err = ClientConfig(clientConfig) 57 | glog.Infof("Building k8sclient with context=%v, masterURL=%v, configFile=%v.", kubeContext, config.Host, clientConfig.ConfigAccess().GetLoadingPrecedence()) 58 | } else { 59 | glog.Infof("Creating Config using BuildConfigFromFlags") 60 | config, err = BuildConfigFromFlags(masterURL, "") 61 | if err != nil { 62 | glog.Errorf("Cannot create config using BuildConfigFromFlags") 63 | return nil, err 64 | } 65 | glog.Infof("Building k8sclient with context=%v, masterURL=%v.", kubeContext, config.Host) 66 | } 67 | 68 | clientset, err := kubernetes.NewForConfig(config) 69 | if err != nil { 70 | glog.Errorf("Cannot Initialize Kubernetes Client API: %v", err) 71 | return nil, err 72 | } 73 | 74 | glog.Infof("Created k8sclient with above configurations") 75 | return clientset, nil 76 | } 77 | 78 | func getConfig(masterURL string, kubeContext string) clientcmd.ClientConfig { 79 | loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 80 | return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( 81 | loadingRules, 82 | &clientcmd.ConfigOverrides{CurrentContext: kubeContext, ClusterInfo: api.Cluster{Server: masterURL}}) 83 | } 84 | -------------------------------------------------------------------------------- /pkg/sloop/ingress/kubeclient_test.go: -------------------------------------------------------------------------------- 1 | package ingress 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "k8s.io/client-go/rest" 8 | "k8s.io/client-go/tools/clientcmd" 9 | "k8s.io/client-go/tools/clientcmd/api" 10 | ) 11 | 12 | func TestGetKubernetesContext(t *testing.T) { 13 | oldRawConfig := RawConfig 14 | defer func() { RawConfig = oldRawConfig }() 15 | var privilegedAccess bool 16 | privilegedAccess = true 17 | methodInvoked := false 18 | RawConfig = func(config clientcmd.ClientConfig) (api.Config, error) { 19 | methodInvoked = true 20 | if !privilegedAccess { 21 | t.Errorf("Got unexpected flag") 22 | } 23 | return api.Config{}, nil 24 | } 25 | GetKubernetesContext("", "", privilegedAccess) 26 | if !methodInvoked { 27 | t.Errorf("RawConfig not invoked") 28 | } 29 | } 30 | 31 | func TestGetKubernetesContextNoPrivilegedAccess(t *testing.T) { 32 | var context string 33 | context, _ = GetKubernetesContext("", "", false) 34 | assert.Equal(t, context, "") 35 | } 36 | 37 | func TestMakeKubernetesClientByGetConfig(t *testing.T) { 38 | oldClientConfig := ClientConfig 39 | defer func() { ClientConfig = oldClientConfig }() 40 | var privilegedAccess bool 41 | privilegedAccess = true 42 | methodInvoked := false 43 | ClientConfig = func(config clientcmd.ClientConfig) (*rest.Config, error) { 44 | methodInvoked = true 45 | if !privilegedAccess { 46 | t.Errorf("Got unexpected flag") 47 | } 48 | return &rest.Config{}, nil 49 | } 50 | MakeKubernetesClient("", "", privilegedAccess) 51 | if !methodInvoked { 52 | t.Errorf("ClientConfig not invoked") 53 | } 54 | } 55 | 56 | func TestMakeKubernetesClientByBuildConfigFromFlag(t *testing.T) { 57 | oldBuildConfig := BuildConfigFromFlags 58 | defer func() { BuildConfigFromFlags = oldBuildConfig }() 59 | var privilegedAccess bool 60 | privilegedAccess = false 61 | methodInvoked := false 62 | BuildConfigFromFlags = func(masterUrl, kubeconfigPath string) (*rest.Config, error) { 63 | methodInvoked = true 64 | if privilegedAccess { 65 | t.Errorf("Got unexpected flag") 66 | } 67 | return &rest.Config{}, nil 68 | } 69 | MakeKubernetesClient("", "", privilegedAccess) 70 | if !methodInvoked { 71 | t.Errorf("BuildConfigFromFlags not invoked") 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /pkg/sloop/ingress/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package ingress 9 | 10 | import ( 11 | "github.com/salesforce/sloop/pkg/sloop/store/typed" 12 | ) 13 | 14 | type KubeResourceSource interface { 15 | Init() (chan typed.KubeWatchResult, error) 16 | Stop() 17 | } 18 | 19 | type KubePlaybackFile struct { 20 | Data []typed.KubeWatchResult `json:"Data"` 21 | } 22 | -------------------------------------------------------------------------------- /pkg/sloop/kubeextractor/events.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package kubeextractor 9 | 10 | import ( 11 | "encoding/json" 12 | "fmt" 13 | "strings" 14 | "time" 15 | 16 | "github.com/golang/glog" 17 | ) 18 | 19 | // Example Event 20 | // 21 | // Name: na1-mist61app-prd-676c5b7dd4-h7x6r.15bf81c8df2bce2c 22 | // Kind: Event 23 | // Namespace: somens 24 | // Payload: 25 | /* 26 | { 27 | "metadata": { 28 | "name": "na1-mist61app-prd-676c5b7dd4-h7x6r.15bf81c8df2bce2c", 29 | "namespace": "somens", 30 | "selfLink": "/api/v1/namespaces/somens/events/na1-mist61app-prd-676c5b7dd4-h7x6r.15bf81c8df2bce2c", 31 | "uid": "d73fbbd4-caa3-11e9-a836-5e785cdb595d", 32 | "resourceVersion": "2623487073", 33 | "creationTimestamp": "2019-08-29T21:27:45Z" 34 | }, 35 | "involvedObject": { 36 | "kind": "Pod", 37 | "namespace": "somens", 38 | "name": "na1-mist61app-prd-676c5b7dd4-h7x6r", 39 | "uid": "2358ba5b-caa3-11e9-a863-14187760f413", 40 | "apiVersion": "v1", 41 | "resourceVersion": "2621648750", 42 | "fieldPath": "spec.containers{coreapp}" 43 | }, 44 | "reason": "Unhealthy", 45 | "message": "Readiness probe failed for some reason", 46 | "source": { 47 | "component": "kubelet", 48 | "host": "somehostname" 49 | }, 50 | "firstTimestamp": "2019-08-29T21:24:55Z", 51 | "lastTimestamp": "2019-08-30T16:47:45Z", 52 | "count": 13954, 53 | "type": "Warning" 54 | } 55 | */ 56 | 57 | // Extracts involved object from kube watch event payload. 58 | func ExtractInvolvedObject(payload string) (KubeInvolvedObject, error) { 59 | resource := struct { 60 | InvolvedObject KubeInvolvedObject 61 | }{} 62 | err := json.Unmarshal([]byte(payload), &resource) 63 | if err != nil { 64 | return KubeInvolvedObject{}, err 65 | } 66 | return resource.InvolvedObject, nil 67 | } 68 | 69 | type EventInfo struct { 70 | Reason string `json:"reason"` 71 | Type string `json:"type"` 72 | FirstTimestamp time.Time `json:"firstTimestamp"` 73 | LastTimestamp time.Time `json:"lastTimestamp"` 74 | Count int `json:"count"` 75 | } 76 | 77 | // Extracts event reason from kube watch event payload 78 | func ExtractEventInfo(payload string) (*EventInfo, error) { 79 | internalResource := struct { 80 | Reason string `json:"reason"` 81 | FirstTimestamp string `json:"firstTimestamp"` 82 | LastTimestamp string `json:"lastTimestamp"` 83 | Count int `json:"count"` 84 | Type string `json:"type"` 85 | }{} 86 | err := json.Unmarshal([]byte(payload), &internalResource) 87 | if err != nil { 88 | return nil, err 89 | } 90 | // Convert timestamps 91 | 92 | fs, err := time.Parse(time.RFC3339, internalResource.FirstTimestamp) 93 | if err != nil { 94 | glog.Errorf("Could not parse first timestamp %v\n", internalResource.FirstTimestamp) 95 | fs = time.Time{} 96 | } 97 | 98 | ls, err := time.Parse(time.RFC3339, internalResource.LastTimestamp) 99 | if err != nil { 100 | glog.Errorf("Could not parse last timestamp %v\n", internalResource.LastTimestamp) 101 | fs = time.Time{} 102 | } 103 | 104 | return &EventInfo{ 105 | Reason: internalResource.Reason, 106 | FirstTimestamp: fs, 107 | LastTimestamp: ls, 108 | Count: internalResource.Count, 109 | Type: internalResource.Type, 110 | }, nil 111 | } 112 | 113 | // Events in kubernetes share the same namespace as the involved object, Kind=Event, and 114 | // Name is the involved object name + "." + some unique string 115 | // 116 | // Deployment name: some-deployment-name 117 | // Event name: some-deployment-name.15c37e2c4b7ff38e 118 | // 119 | // Pod name: some-deployment-name-d72v-5fd4f779f7-h4t6r 120 | // Event name: some-deployment-name-d72v-5fd4f779f7-h4t6r.15c37e4fcf9f159f 121 | func GetInvolvedObjectNameFromEventName(eventName string) (string, error) { 122 | dotIdx := strings.LastIndex(eventName, ".") 123 | if dotIdx < 0 { 124 | return "", fmt.Errorf("unexpected format for a k8s event name: %v", eventName) 125 | } 126 | return eventName[0:dotIdx], nil 127 | } 128 | 129 | func IsClustersScopedResource(selectedKind string) bool { 130 | if selectedKind == NodeKind || selectedKind == NamespaceKind { 131 | return true 132 | } 133 | return false 134 | } 135 | -------------------------------------------------------------------------------- /pkg/sloop/kubeextractor/events_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package kubeextractor 9 | 10 | import ( 11 | "testing" 12 | "time" 13 | 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func Test_ExtractInvolvedObject_OutputCorrect(t *testing.T) { 18 | payload := `{"involvedObject":{"kind":"ReplicaSet","namespace":"namespace1","name":"name1","uid":"uid1"}}` 19 | expectedResult := KubeInvolvedObject{ 20 | Kind: "ReplicaSet", 21 | Name: "name1", 22 | Namespace: "namespace1", 23 | Uid: "uid1", 24 | } 25 | result, err := ExtractInvolvedObject(payload) 26 | assert.Nil(t, err) 27 | assert.Equal(t, expectedResult, result) 28 | } 29 | 30 | func Test_ExtractInvolvedObject_InvalidPayload_ReturnsError(t *testing.T) { 31 | payload := `{"involvedObject":{"name":"name1","namespace":"namespace1","selfLink":"link1"}` 32 | result, err := ExtractInvolvedObject(payload) 33 | assert.NotNil(t, err) 34 | assert.Equal(t, KubeInvolvedObject{}, result) 35 | } 36 | 37 | func Test_ExtractInvolvedObject_PayloadHasAdditionalFields_OutputCorrect(t *testing.T) { 38 | payload := `{"metadata":{"name":"name2","namespace":"namespace2","uid":"uid2"},"involvedObject":{"kind":"Pod","name":"name1","namespace":"namespace1","uid":"uid1"}}` 39 | expectedResult := KubeInvolvedObject{ 40 | Kind: "Pod", 41 | Name: "name1", 42 | Namespace: "namespace1", 43 | Uid: "uid1", 44 | } 45 | result, err := ExtractInvolvedObject(payload) 46 | assert.Nil(t, err) 47 | assert.Equal(t, expectedResult, result) 48 | } 49 | 50 | var someFirstSeenTime = time.Date(2019, 8, 29, 21, 24, 55, 0, time.UTC) 51 | var someLastSeenTime = time.Date(2019, 8, 30, 16, 47, 45, 0, time.UTC) 52 | 53 | func Test_ExtractEventReason_OutputCorrect(t *testing.T) { 54 | payload := `{"reason":"failed","firstTimestamp": "2019-08-29T21:24:55Z","lastTimestamp": "2019-08-30T16:47:45Z","count": 13954}` 55 | result, err := ExtractEventInfo(payload) 56 | assert.Nil(t, err) 57 | assert.Equal(t, "failed", result.Reason) 58 | assert.Equal(t, someFirstSeenTime, result.FirstTimestamp) 59 | assert.Equal(t, someLastSeenTime, result.LastTimestamp) 60 | assert.Equal(t, 13954, result.Count) 61 | } 62 | 63 | func Test_ExtractEventReason_MissingFieldsAreIgnored(t *testing.T) { 64 | payload := `{"metadata":{"name":"name1","uid":"uid1","resourceVersion":"123","creationTimestamp":"2019-07-12T20:12:12Z"}}` 65 | expectedResult := "" 66 | result, err := ExtractEventInfo(payload) 67 | assert.Nil(t, err) 68 | assert.Equal(t, expectedResult, result.Reason) 69 | } 70 | 71 | func Test_GetInvolvedObjectNameFromEventName_invalid(t *testing.T) { 72 | eventName := "xxx" 73 | key, err := GetInvolvedObjectNameFromEventName(eventName) 74 | assert.NotNil(t, err) 75 | assert.Equal(t, key, "") 76 | } 77 | 78 | func Test_GetInvolvedObjectNameFromEventName_valid(t *testing.T) { 79 | eventName := "xxx.abc" 80 | key, err := GetInvolvedObjectNameFromEventName(eventName) 81 | assert.Nil(t, err) 82 | assert.Equal(t, "xxx", key) 83 | } 84 | 85 | func Test_GetInvolvedObjectNameFromEventName_HostName(t *testing.T) { 86 | eventName := "somehost.somedomain.com.abc" 87 | key, err := GetInvolvedObjectNameFromEventName(eventName) 88 | assert.Nil(t, err) 89 | assert.Equal(t, "somehost.somedomain.com", key) 90 | } 91 | 92 | func Test_IsClustersScopedResource_True(t *testing.T) { 93 | selectedKind := NodeKind 94 | res := IsClustersScopedResource(selectedKind) 95 | assert.True(t, res) 96 | 97 | res = IsClustersScopedResource(NamespaceKind) 98 | assert.True(t, res) 99 | } 100 | 101 | func Test_IsClustersScopedResource_False(t *testing.T) { 102 | selectedKind := "someKind" 103 | res := IsClustersScopedResource(selectedKind) 104 | assert.False(t, res) 105 | } 106 | -------------------------------------------------------------------------------- /pkg/sloop/kubeextractor/kinds.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package kubeextractor 9 | 10 | const ( 11 | NodeKind = "Node" 12 | NamespaceKind = "Namespace" 13 | PodKind = "Pod" 14 | EventKind = "Event" 15 | ) 16 | -------------------------------------------------------------------------------- /pkg/sloop/kubeextractor/kubeextractor.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package kubeextractor 9 | 10 | import ( 11 | "encoding/json" 12 | ) 13 | 14 | type KubeMetadata struct { 15 | Name string 16 | Namespace string 17 | Uid string 18 | SelfLink string 19 | ResourceVersion string 20 | CreationTimestamp string 21 | OwnerReferences []KubeMetadataOwnerReference 22 | } 23 | 24 | type KubeInvolvedObject struct { 25 | Kind string 26 | Name string 27 | Namespace string 28 | Uid string 29 | } 30 | 31 | type KubeMetadataOwnerReference struct { 32 | Kind string 33 | Name string 34 | Uid string 35 | } 36 | 37 | // Extracts metadata from kube watch event payload. 38 | func ExtractMetadata(payload string) (KubeMetadata, error) { 39 | resource := struct { 40 | Metadata KubeMetadata 41 | }{} 42 | err := json.Unmarshal([]byte(payload), &resource) 43 | if err != nil { 44 | return KubeMetadata{}, err 45 | } 46 | return resource.Metadata, nil 47 | } 48 | -------------------------------------------------------------------------------- /pkg/sloop/kubeextractor/kubeextractor_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package kubeextractor 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func Test_ExtractMetadata_OutputCorrect(t *testing.T) { 17 | payload := `{"metadata": 18 | { 19 | "name":"name1", 20 | "namespace":"namespace1", 21 | "selfLink":"link1", 22 | "uid":"uid1", 23 | "resourceVersion":"123", 24 | "creationTimestamp":"2019-07-12T20:12:12Z", 25 | "ownerReferences": [ 26 | { 27 | "kind": "Deployment", 28 | "name": "deployment1", 29 | "uid": "uid0" 30 | }] 31 | } 32 | }` 33 | expectedResult := KubeMetadata{ 34 | Name: "name1", 35 | Namespace: "namespace1", 36 | Uid: "uid1", 37 | SelfLink: "link1", 38 | ResourceVersion: "123", 39 | CreationTimestamp: "2019-07-12T20:12:12Z", 40 | OwnerReferences: []KubeMetadataOwnerReference{{Kind: "Deployment", Name: "deployment1", Uid: "uid0"}}, 41 | } 42 | result, err := ExtractMetadata(payload) 43 | assert.Nil(t, err) 44 | assert.Equal(t, expectedResult, result) 45 | } 46 | 47 | func Test_ExtractMetadata_MissingFieldsAreIgnored(t *testing.T) { 48 | payload := `{"metadata":{"name":"name1","uid":"uid1","resourceVersion":"123","creationTimestamp":"2019-07-12T20:12:12Z"}}` 49 | expectedResult := KubeMetadata{ 50 | Name: "name1", 51 | Namespace: "", 52 | Uid: "uid1", 53 | SelfLink: "", 54 | ResourceVersion: "123", 55 | CreationTimestamp: "2019-07-12T20:12:12Z", 56 | } 57 | result, err := ExtractMetadata(payload) 58 | assert.Nil(t, err) 59 | assert.Equal(t, expectedResult, result) 60 | } 61 | 62 | func Test_ExtractMetadata_InvalidPayload_ReturnsError(t *testing.T) { 63 | payload := `{"metadata":{"name":"name1","namespace":"namespace1","selfLink":"link1"}` 64 | result, err := ExtractMetadata(payload) 65 | assert.NotNil(t, err) 66 | assert.Equal(t, KubeMetadata{}, result) 67 | } 68 | 69 | func Test_ExtractMetadata_PayloadHasAdditionalFields_OutputCorrect(t *testing.T) { 70 | payload := `{"metadata":{"name":"name1","namespace":"namespace1","selfLink":"link1","uid":"uid1","resourceVersion":"123","creationTimestamp":"2019-07-12T20:12:12Z"},"meta2":{"kind":"Pod","namespace":"namespace2"}}` 71 | expectedResult := KubeMetadata{ 72 | Name: "name1", 73 | Namespace: "namespace1", 74 | Uid: "uid1", 75 | SelfLink: "link1", 76 | ResourceVersion: "123", 77 | CreationTimestamp: "2019-07-12T20:12:12Z", 78 | } 79 | result, err := ExtractMetadata(payload) 80 | assert.Nil(t, err) 81 | assert.Equal(t, expectedResult, result) 82 | } 83 | -------------------------------------------------------------------------------- /pkg/sloop/kubeextractor/nodededupe.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package kubeextractor 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/Jeffail/gabs/v2" 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | // In Kubernetes the node objects generally update frequently with new timestamps and resourceVersion 18 | // Because nodes are large resources, it can be desirable to drop updates without important state change 19 | func NodeHasMajorUpdate(node1 string, node2 string) (bool, error) { 20 | cleanNode1, err := removeResVerAndTimestamp(node1) 21 | if err != nil { 22 | return false, err 23 | } 24 | cleanNode2, err := removeResVerAndTimestamp(node2) 25 | if err != nil { 26 | return false, err 27 | } 28 | return !(cleanNode1 == cleanNode2), nil 29 | } 30 | 31 | func removeResVerAndTimestamp(nodeJson string) (string, error) { 32 | jsonParsed, err := gabs.ParseJSON([]byte(nodeJson)) 33 | if err != nil { 34 | return "", errors.Wrap(err, "Failed to parse json for node resource") 35 | } 36 | 37 | _, err = jsonParsed.Set("removed", "metadata", "resourceVersion") 38 | if err != nil { 39 | return "", errors.Wrap(err, "Could not replace metadata.resourceVersion in node resource") 40 | } 41 | 42 | numConditions := len(jsonParsed.S("status", "conditions").Children()) 43 | 44 | for idx := 0; idx < numConditions; idx += 1 { 45 | _, err = jsonParsed.Set("removed", "status", "conditions", fmt.Sprint(idx), "lastHeartbeatTime") 46 | if err != nil { 47 | return "", errors.Wrap(err, "Could not set node condition") 48 | } 49 | } 50 | 51 | return jsonParsed.String(), nil 52 | } 53 | -------------------------------------------------------------------------------- /pkg/sloop/kubeextractor/nodededupe_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package kubeextractor 9 | 10 | import ( 11 | "bytes" 12 | "fmt" 13 | "testing" 14 | "text/template" 15 | 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | const nodeTemplate = `{ 20 | "metadata": { 21 | "name": "somehostname", 22 | "uid": "1f9c4fdc-df86-11e6-8ec4-141877585f71", 23 | "resourceVersion": "{{.ResourceVersion}}" 24 | }, 25 | "status": { 26 | "conditions": [ 27 | { 28 | "type": "OutOfDisk", 29 | "status": "{{.OutOfDisk}}", 30 | "lastHeartbeatTime": "{{.LastHeartbeatTime}}", 31 | "lastTransitionTime": "2019-07-19T15:35:56Z", 32 | "reason": "KubeletHasSufficientDisk" 33 | }, 34 | { 35 | "type": "MemoryPressure", 36 | "status": "False", 37 | "lastHeartbeatTime": "{{.LastHeartbeatTime}}", 38 | "lastTransitionTime": "2019-07-19T15:35:56Z", 39 | "reason": "KubeletHasSufficientMemory" 40 | } 41 | ] 42 | } 43 | } 44 | ` 45 | 46 | const someResourceVersion1 = "873691308" 47 | const someResourceVersion2 = "873691358" 48 | const someHeartbeatTime1 = "2019-07-23T17:18:10Z" 49 | const someHeartbeatTime2 = "2019-07-23T17:18:20Z" 50 | 51 | type nodeData struct { 52 | ResourceVersion string 53 | LastHeartbeatTime string 54 | OutOfDisk string 55 | } 56 | 57 | func helper_makeNodeResource(t *testing.T, resVer string, heartbeat string, outOfDisk string) string { 58 | data := nodeData{ResourceVersion: resVer, LastHeartbeatTime: heartbeat, OutOfDisk: outOfDisk} 59 | tmp, err := template.New("test").Parse(nodeTemplate) 60 | assert.Nil(t, err) 61 | var tpl bytes.Buffer 62 | err = tmp.Execute(&tpl, data) 63 | assert.Nil(t, err) 64 | return tpl.String() 65 | } 66 | 67 | const expectedCleanNode = `{"metadata":{"name":"somehostname","resourceVersion":"removed","uid":"1f9c4fdc-df86-11e6-8ec4-141877585f71"},"status":{"conditions":[` + 68 | `{"lastHeartbeatTime":"removed","lastTransitionTime":"2019-07-19T15:35:56Z","reason":"KubeletHasSufficientDisk","status":"False","type":"OutOfDisk"},` + 69 | `{"lastHeartbeatTime":"removed","lastTransitionTime":"2019-07-19T15:35:56Z","reason":"KubeletHasSufficientMemory","status":"False","type":"MemoryPressure"}]}}` 70 | 71 | func Test_removeResVerAndTimestamp(t *testing.T) { 72 | nodeJson := helper_makeNodeResource(t, someResourceVersion1, someHeartbeatTime1, "False") 73 | cleanNode, err := removeResVerAndTimestamp(nodeJson) 74 | assert.Nil(t, err) 75 | fmt.Printf("%v\n", cleanNode) 76 | assert.Equal(t, expectedCleanNode, cleanNode) 77 | } 78 | 79 | func Test_nodesMeaningfullyDifferent_sameNode(t *testing.T) { 80 | nodeJson := helper_makeNodeResource(t, someResourceVersion1, someHeartbeatTime1, "False") 81 | diff, err := NodeHasMajorUpdate(nodeJson, nodeJson) 82 | assert.Nil(t, err) 83 | assert.False(t, diff) 84 | } 85 | 86 | func Test_nodesMeaningfullyDifferent_onlyDiffTimeAndRes(t *testing.T) { 87 | nodeJson1 := helper_makeNodeResource(t, someResourceVersion1, someHeartbeatTime1, "False") 88 | nodeJson2 := helper_makeNodeResource(t, someResourceVersion2, someHeartbeatTime2, "False") 89 | diff, err := NodeHasMajorUpdate(nodeJson1, nodeJson2) 90 | assert.Nil(t, err) 91 | assert.False(t, diff) 92 | } 93 | 94 | func Test_nodesMeaningfullyDifferent_diffOutOfDisk(t *testing.T) { 95 | nodeJson1 := helper_makeNodeResource(t, someResourceVersion1, someHeartbeatTime1, "False") 96 | nodeJson2 := helper_makeNodeResource(t, someResourceVersion1, someHeartbeatTime1, "True") 97 | diff, err := NodeHasMajorUpdate(nodeJson1, nodeJson2) 98 | assert.Nil(t, err) 99 | assert.True(t, diff) 100 | } 101 | -------------------------------------------------------------------------------- /pkg/sloop/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package main 9 | 10 | import ( 11 | "flag" 12 | "os" 13 | "runtime/pprof" 14 | 15 | "github.com/golang/glog" 16 | "github.com/salesforce/sloop/pkg/sloop/server" 17 | ) 18 | 19 | var cpuprofile = flag.String("cpuprofile", "", "write profile to file") 20 | 21 | func main() { 22 | if *cpuprofile != "" { 23 | f, err := os.Create(*cpuprofile) 24 | if err != nil { 25 | glog.Fatal(err) 26 | } 27 | pprof.StartCPUProfile(f) 28 | defer pprof.StopCPUProfile() 29 | } 30 | err := server.RealMain() 31 | if err != nil { 32 | glog.Errorf("Main exited with error: %v\n", err) 33 | os.Exit(1) 34 | } else { 35 | glog.Infof("Shutting down gracefully") 36 | os.Exit(0) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pkg/sloop/processing/processing.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package processing 9 | 10 | import ( 11 | "sync" 12 | "time" 13 | 14 | "github.com/golang/glog" 15 | "github.com/prometheus/client_golang/prometheus" 16 | "github.com/prometheus/client_golang/prometheus/promauto" 17 | "github.com/salesforce/sloop/pkg/sloop/kubeextractor" 18 | "github.com/salesforce/sloop/pkg/sloop/store/typed" 19 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 20 | ) 21 | 22 | type Runner struct { 23 | kubeWatchChan chan typed.KubeWatchResult 24 | tables typed.Tables 25 | inputWg *sync.WaitGroup 26 | keepMinorNodeUpdates bool 27 | maxLookback time.Duration 28 | } 29 | 30 | var ( 31 | metricProcessingWatchtableUpdatecount = promauto.NewCounter(prometheus.CounterOpts{Name: "sloop_processing_watchtable_updatecount"}) 32 | metricIngestionFailureCount = promauto.NewCounter(prometheus.CounterOpts{Name: "sloop_ingestion_failure_count"}) 33 | metricIngestionSuccessCount = promauto.NewCounter(prometheus.CounterOpts{Name: "sloop_ingestion_success_count"}) 34 | ) 35 | 36 | func NewProcessing(kubeWatchChan chan typed.KubeWatchResult, tables typed.Tables, keepMinorNodeUpdates bool, maxLookback time.Duration) *Runner { 37 | return &Runner{kubeWatchChan: kubeWatchChan, tables: tables, inputWg: &sync.WaitGroup{}, keepMinorNodeUpdates: keepMinorNodeUpdates, maxLookback: maxLookback} 38 | } 39 | 40 | func (r *Runner) processingFailed(name string, err error) { 41 | glog.Errorf("Processing for %v failed with error %v", name, err) 42 | metricIngestionFailureCount.Inc() 43 | } 44 | 45 | func (r *Runner) Start() { 46 | r.inputWg.Add(1) 47 | go func() { 48 | for { 49 | watchRec, more := <-r.kubeWatchChan 50 | if !more { 51 | r.inputWg.Done() 52 | return 53 | } 54 | 55 | resourceMetadata, err := kubeextractor.ExtractMetadata(watchRec.Payload) 56 | if err != nil { 57 | r.processingFailed("cannot extract resource metadata", err) 58 | } 59 | glog.V(99).Infof("watchRec metadata: %v", resourceMetadata) 60 | involvedObject, err := kubeextractor.ExtractInvolvedObject(watchRec.Payload) 61 | if err != nil { 62 | r.processingFailed("cannot extract involved object", err) 63 | } 64 | 65 | // Processing event count first so it can easily find the previous copy of the event 66 | // If we update watchTable first then this will see the new event and think it is a dupe 67 | err = r.tables.Db().Update(func(txn badgerwrap.Txn) error { 68 | return updateEventCountTable(r.tables, txn, &watchRec, &resourceMetadata, &involvedObject, r.maxLookback) 69 | }) 70 | if err != nil { 71 | r.processingFailed("updateEventCountTable", err) 72 | } 73 | 74 | err = r.tables.Db().Update(func(txn badgerwrap.Txn) error { 75 | return updateWatchActivityTable(r.tables, txn, &watchRec, &resourceMetadata) 76 | }) 77 | if err != nil { 78 | r.processingFailed("updateWatchActivityTable", err) 79 | } 80 | 81 | err = r.tables.Db().Update(func(txn badgerwrap.Txn) error { 82 | return updateKubeWatchTable(r.tables, txn, &watchRec, &resourceMetadata, r.keepMinorNodeUpdates) 83 | }) 84 | if err != nil { 85 | r.processingFailed("updateKubeWatchTable", err) 86 | } 87 | 88 | err = r.tables.Db().Update(func(txn badgerwrap.Txn) error { 89 | return updateResourceSummaryTable(r.tables, txn, &watchRec, &resourceMetadata) 90 | }) 91 | if err != nil { 92 | r.processingFailed("updateResourceSummaryTable", err) 93 | } 94 | } 95 | }() 96 | } 97 | 98 | func (r *Runner) Wait() { 99 | glog.Infof("Waiting for outstanding processing to finish") 100 | r.inputWg.Wait() 101 | } 102 | -------------------------------------------------------------------------------- /pkg/sloop/processing/resourcesummary.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package processing 9 | 10 | import ( 11 | "time" 12 | 13 | "github.com/dgraph-io/badger/v2" 14 | "github.com/golang/glog" 15 | "github.com/golang/protobuf/ptypes" 16 | "github.com/pkg/errors" 17 | "github.com/salesforce/sloop/pkg/sloop/kubeextractor" 18 | "github.com/salesforce/sloop/pkg/sloop/store/typed" 19 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 20 | ) 21 | 22 | // TODO: Split this up and add unit tests 23 | 24 | func updateResourceSummaryTable(tables typed.Tables, txn badgerwrap.Txn, watchRec *typed.KubeWatchResult, metadata *kubeextractor.KubeMetadata) error { 25 | if watchRec.Kind == kubeextractor.EventKind { 26 | glog.V(2).Infof("Skipping resource summary table update as kubewatch result is an event(selfLink: %v)", metadata.SelfLink) 27 | return nil 28 | } 29 | ts, err := ptypes.Timestamp(watchRec.Timestamp) 30 | if err != nil { 31 | return errors.Wrap(err, "could not convert timestamp") 32 | } 33 | 34 | key := typed.NewResourceSummaryKey(ts, watchRec.Kind, metadata.Namespace, metadata.Name, metadata.Uid).String() 35 | 36 | value, err := getResourceSummaryValue(tables, txn, key, metadata, watchRec) 37 | if err != nil { 38 | return errors.Wrapf(err, "could not get record for key %v", key) 39 | } 40 | 41 | value.Relationships = getRelationships(ts, metadata) 42 | 43 | err = tables.ResourceSummaryTable().Set(txn, key, value) 44 | if err != nil { 45 | return errors.Wrapf(err, "put for the key %v failed", key) 46 | } 47 | 48 | metricIngestionSuccessCount.Inc() 49 | return nil 50 | } 51 | 52 | func getResourceSummaryValue(tables typed.Tables, txn badgerwrap.Txn, key string, metadata *kubeextractor.KubeMetadata, watchRec *typed.KubeWatchResult) (*typed.ResourceSummary, error) { 53 | value, err := tables.ResourceSummaryTable().Get(txn, key) 54 | if err != nil { 55 | if err != badger.ErrKeyNotFound { 56 | return nil, errors.Wrap(err, "could not get record") 57 | } 58 | createTimeProto, err := typed.StringToProtobufTimestamp(metadata.CreationTimestamp) 59 | if err != nil { 60 | return nil, errors.Wrap(err, "could not convert string to timestamp") 61 | } 62 | value = &typed.ResourceSummary{ 63 | FirstSeen: watchRec.Timestamp, 64 | CreateTime: createTimeProto, 65 | DeletedAtEnd: false} 66 | } else if watchRec.WatchType == typed.KubeWatchResult_ADD { 67 | value.FirstSeen = watchRec.Timestamp 68 | } 69 | value.LastSeen = watchRec.Timestamp 70 | if watchRec.WatchType == typed.KubeWatchResult_DELETE { 71 | value.DeletedAtEnd = true 72 | } 73 | return value, nil 74 | } 75 | 76 | func getRelationships(timestamp time.Time, metadata *kubeextractor.KubeMetadata) []string { 77 | relationships := []string{} 78 | for _, value := range metadata.OwnerReferences { 79 | refKey := typed.NewResourceSummaryKey(timestamp, value.Kind, metadata.Namespace, value.Name, value.Uid).String() 80 | relationships = append(relationships, refKey) 81 | } 82 | return relationships 83 | } 84 | -------------------------------------------------------------------------------- /pkg/sloop/processing/watchactivity.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package processing 9 | 10 | import ( 11 | "time" 12 | 13 | "github.com/golang/protobuf/ptypes" 14 | "github.com/pkg/errors" 15 | "github.com/salesforce/sloop/pkg/sloop/kubeextractor" 16 | "github.com/salesforce/sloop/pkg/sloop/store/typed" 17 | "github.com/salesforce/sloop/pkg/sloop/store/untyped" 18 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 19 | ) 20 | 21 | func updateWatchActivityTable(tables typed.Tables, txn badgerwrap.Txn, watchRec *typed.KubeWatchResult, metadata *kubeextractor.KubeMetadata) error { 22 | 23 | if watchRec.Kind == kubeextractor.EventKind { 24 | return nil 25 | } 26 | 27 | resourceChanged, err := didKubeWatchResultChange(tables, txn, watchRec, metadata) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | timestamp, err := ptypes.Timestamp(watchRec.Timestamp) 33 | if err != nil { 34 | return errors.Wrapf(err, "Could not convert timestamp %v", watchRec.Timestamp) 35 | } 36 | 37 | activityRecord, key, err := getWatchActivity(tables, txn, timestamp, watchRec, metadata) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | if resourceChanged { 43 | activityRecord.ChangedAt = append(activityRecord.ChangedAt, timestamp.Unix()) 44 | } else { 45 | activityRecord.NoChangeAt = append(activityRecord.NoChangeAt, timestamp.Unix()) 46 | } 47 | 48 | metricIngestionSuccessCount.Inc() 49 | return putWatchActivity(tables, txn, activityRecord, key) 50 | } 51 | 52 | func didKubeWatchResultChange(tables typed.Tables, txn badgerwrap.Txn, watchRec *typed.KubeWatchResult, metadata *kubeextractor.KubeMetadata) (bool, error) { 53 | resourceChanged := false 54 | prevWatch, err := getLastKubeWatchResult(tables, txn, watchRec.Timestamp, watchRec.Kind, metadata.Namespace, metadata.Name) 55 | if err != nil { 56 | return false, errors.Wrap(err, "Could not get event info for previous event instance") 57 | } 58 | 59 | if prevWatch != nil { 60 | prevMetadata, err := kubeextractor.ExtractMetadata(prevWatch.Payload) 61 | if err != nil { 62 | return false, errors.Wrap(err, "Cannot extract resource metadata") 63 | } 64 | 65 | resourceChanged = metadata.ResourceVersion != prevMetadata.ResourceVersion 66 | } 67 | 68 | return resourceChanged, nil 69 | } 70 | 71 | func getWatchActivity(tables typed.Tables, txn badgerwrap.Txn, timestamp time.Time, watchRec *typed.KubeWatchResult, metadata *kubeextractor.KubeMetadata) (*typed.WatchActivity, *typed.WatchActivityKey, error) { 72 | partitionId := untyped.GetPartitionId(timestamp) 73 | key := typed.NewWatchActivityKey(partitionId, watchRec.Kind, metadata.Namespace, metadata.Name, metadata.Uid) 74 | 75 | activityRecord, err := tables.WatchActivityTable().GetOrDefault(txn, key.String()) 76 | if err != nil { 77 | return nil, nil, errors.Wrap(err, "Could not get watch activity record") 78 | } 79 | 80 | return activityRecord, key, nil 81 | } 82 | 83 | func putWatchActivity(tables typed.Tables, txn badgerwrap.Txn, activityRecord *typed.WatchActivity, key *typed.WatchActivityKey) error { 84 | err := tables.WatchActivityTable().Set(txn, key.String(), activityRecord) 85 | if err != nil { 86 | return errors.Wrap(err, "Failed to put watch activity record") 87 | } 88 | 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /pkg/sloop/processing/watchactivity_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package processing 9 | 10 | import ( 11 | "testing" 12 | "time" 13 | 14 | "github.com/dgraph-io/badger/v2" 15 | "github.com/golang/protobuf/ptypes" 16 | "github.com/salesforce/sloop/pkg/sloop/kubeextractor" 17 | "github.com/salesforce/sloop/pkg/sloop/store/typed" 18 | "github.com/salesforce/sloop/pkg/sloop/store/untyped" 19 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | const someNodePayload1 = `{ 24 | "metadata": { 25 | "name": "someName", 26 | "namespace": "someNamespace", 27 | "resourceVersion": "456" 28 | }, 29 | "status": { 30 | "conditions": [ 31 | { 32 | "type": "OutOfDisk", 33 | "status": "False", 34 | "lastHeartbeatTime": "2012-01-01T15:35:56Z", 35 | "lastTransitionTime": "2019-07-19T15:35:56Z", 36 | "reason": "KubeletHasSufficientDisk" 37 | } 38 | ] 39 | } 40 | }` 41 | const someNodePayload2 = `{ 42 | "metadata": { 43 | "name": "someName", 44 | "namespace": "someNamespace", 45 | "resourceVersion": "457" 46 | }, 47 | "status": { 48 | "conditions": [ 49 | { 50 | "type": "OutOfDisk", 51 | "status": "False", 52 | "lastHeartbeatTime": "2012-01-01T15:35:56Z", 53 | "lastTransitionTime": "2019-07-19T15:35:56Z", 54 | "reason": "KubeletHasSufficientDisk" 55 | } 56 | ] 57 | } 58 | }` 59 | 60 | func Test_updateWatchActivityTable(t *testing.T) { 61 | 62 | untyped.TestHookSetPartitionDuration(time.Hour) 63 | db, err := (&badgerwrap.MockFactory{}).Open(badger.DefaultOptions("")) 64 | assert.Nil(t, err) 65 | tables := typed.NewTableList(db) 66 | 67 | ts, err := ptypes.TimestampProto(someWatchTime) 68 | assert.Nil(t, err) 69 | watchRec := &typed.KubeWatchResult{Kind: kubeextractor.NodeKind, WatchType: typed.KubeWatchResult_UPDATE, Timestamp: ts, Payload: someNodePayload1} 70 | metadata, err := kubeextractor.ExtractMetadata(watchRec.Payload) 71 | assert.Nil(t, err) 72 | 73 | // add a WatchActivity (no matching KubeWatchResult) => no change 74 | err = tables.Db().Update(func(txn badgerwrap.Txn) error { 75 | err = updateWatchActivityTable(tables, txn, watchRec, &metadata) 76 | assert.Nil(t, err) 77 | 78 | activityRecord, _, err := getWatchActivity(tables, txn, someWatchTime, watchRec, &metadata) 79 | assert.Nil(t, err) 80 | assert.NotNil(t, activityRecord) 81 | assert.Equal(t, 0, len(activityRecord.ChangedAt)) 82 | assert.Equal(t, 1, len(activityRecord.NoChangeAt)) 83 | assert.Equal(t, someWatchTime.Unix(), activityRecord.NoChangeAt[0]) 84 | 85 | return nil 86 | }) 87 | assert.Nil(t, err) 88 | 89 | // add a KubeWatchResult 90 | err = tables.Db().Update(func(txn badgerwrap.Txn) error { 91 | return updateKubeWatchTable(tables, txn, watchRec, &metadata, true) 92 | }) 93 | assert.Nil(t, err) 94 | 95 | // add a WatchActivity => no change at timestamp 96 | timestamp2 := someWatchTime.Add(time.Minute) 97 | ts2, err := ptypes.TimestampProto(timestamp2) 98 | assert.Nil(t, err) 99 | watchRec.Timestamp = ts2 100 | err = tables.Db().Update(func(txn badgerwrap.Txn) error { 101 | err = updateWatchActivityTable(tables, txn, watchRec, &metadata) 102 | assert.Nil(t, err) 103 | 104 | activityRecord, _, err := getWatchActivity(tables, txn, timestamp2, watchRec, &metadata) 105 | assert.Nil(t, err) 106 | assert.NotNil(t, activityRecord) 107 | assert.Equal(t, 0, len(activityRecord.ChangedAt)) 108 | assert.Equal(t, 2, len(activityRecord.NoChangeAt)) 109 | assert.Equal(t, timestamp2.Unix(), activityRecord.NoChangeAt[1]) 110 | 111 | return nil 112 | }) 113 | assert.Nil(t, err) 114 | 115 | // add a changed WatchActivity => changed at timestamp 116 | watchRec.Payload = someNodePayload2 117 | metadata, err = kubeextractor.ExtractMetadata(watchRec.Payload) 118 | assert.Nil(t, err) 119 | err = tables.Db().Update(func(txn badgerwrap.Txn) error { 120 | err = updateWatchActivityTable(tables, txn, watchRec, &metadata) 121 | assert.Nil(t, err) 122 | 123 | activityRecord, _, err := getWatchActivity(tables, txn, timestamp2, watchRec, &metadata) 124 | assert.Nil(t, err) 125 | assert.NotNil(t, activityRecord) 126 | assert.Equal(t, 1, len(activityRecord.ChangedAt)) 127 | assert.Equal(t, 2, len(activityRecord.NoChangeAt)) 128 | assert.Equal(t, timestamp2.Unix(), activityRecord.ChangedAt[0]) 129 | 130 | return nil 131 | }) 132 | assert.Nil(t, err) 133 | } 134 | -------------------------------------------------------------------------------- /pkg/sloop/queries/eventquery.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package queries 9 | 10 | import ( 11 | "encoding/json" 12 | "fmt" 13 | "net/url" 14 | "time" 15 | 16 | "github.com/salesforce/sloop/pkg/sloop/kubeextractor" 17 | "github.com/salesforce/sloop/pkg/sloop/store/typed" 18 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 19 | ) 20 | 21 | type EventsData struct { 22 | EventsList []EventOutput `json:"eventsList"` 23 | } 24 | 25 | type EventOutput struct { 26 | PartitionId string `json:"partitionId"` 27 | Namespace string `json:"namespace"` 28 | Name string `json:"name"` 29 | WatchTimestamp time.Time `json:"watchTimestamp,omitempty"` 30 | Kind string `json:"kind,omitempty"` 31 | WatchType typed.KubeWatchResult_WatchType `json:"watchType,omitempty"` 32 | Payload string `json:"payload,omitempty"` 33 | EventKey string `json:"eventKey"` 34 | } 35 | 36 | func GetEventData(params url.Values, t typed.Tables, startTime time.Time, endTime time.Time, requestId string) ([]byte, error) { 37 | var watchEvents map[typed.WatchTableKey]*typed.KubeWatchResult 38 | err := t.Db().View(func(txn badgerwrap.Txn) error { 39 | var err2 error 40 | var stats typed.RangeReadStats 41 | selectedNamespace := params.Get(NamespaceParam) 42 | selectedName := params.Get(NameParam) 43 | selectedKind := params.Get(KindParam) 44 | 45 | // Events are stored with metadata name which are like InvolvedObjectName.XXXX 46 | // To ensure we only get events for this resource. Add a '.' delimiter in the end. 47 | selectedName = selectedName + "." 48 | 49 | if kubeextractor.IsClustersScopedResource(selectedKind) { 50 | selectedNamespace = DefaultNamespace 51 | } 52 | 53 | key := &typed.WatchTableKey{ 54 | // partition id will be rest, it is ok to leave it as empty string 55 | PartitionId: "", 56 | Kind: kubeextractor.EventKind, 57 | Namespace: selectedNamespace, 58 | Name: selectedName, 59 | Timestamp: time.Time{}, 60 | } 61 | 62 | // pass a few valPredFn filters: payload in time range and payload kind matched 63 | valPredFn := typed.KubeWatchResult_ValPredicateFns(isEventValInTimeRange(startTime, endTime), matchEventInvolvedObject(params)) 64 | watchEvents, stats, err2 = t.WatchTable().RangeRead(txn, key, nil, valPredFn, startTime, endTime) 65 | if err2 != nil { 66 | return err2 67 | } 68 | stats.Log(requestId) 69 | return nil 70 | }) 71 | if err != nil { 72 | return []byte{}, err 73 | } 74 | var res EventsData 75 | eventsList := []EventOutput{} 76 | for key, val := range watchEvents { 77 | output := EventOutput{ 78 | PartitionId: key.PartitionId, 79 | Namespace: key.Namespace, 80 | Name: key.Name, 81 | WatchTimestamp: key.Timestamp, 82 | Kind: key.Kind, 83 | WatchType: val.WatchType, 84 | Payload: val.Payload, 85 | EventKey: key.String(), 86 | } 87 | eventsList = append(eventsList, output) 88 | } 89 | 90 | if len(eventsList) == 0 { 91 | return []byte{}, nil 92 | } 93 | res.EventsList = eventsList 94 | bytes, err := json.MarshalIndent(res.EventsList, "", " ") 95 | if err != nil { 96 | return nil, fmt.Errorf("failed to marshal json %v", err) 97 | } 98 | return bytes, nil 99 | } 100 | -------------------------------------------------------------------------------- /pkg/sloop/queries/init_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package queries 9 | 10 | import ( 11 | "flag" 12 | "fmt" 13 | ) 14 | 15 | func init() { 16 | flag.Set("alsologtostderr", fmt.Sprintf("%t", true)) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/sloop/queries/params.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package queries 9 | 10 | // Parameters are shared between webserver and here 11 | // Keep this in sync with pkg/sloop/webserver/webfiles/filter.js 12 | const ( 13 | LookbackParam = "lookback" 14 | NamespaceParam = "namespace" 15 | KindParam = "kind" 16 | NameParam = "name" 17 | NameMatchParam = "namematch" // substring match on name 18 | UuidParam = "uuid" 19 | StartTimeParam = "start_time" 20 | EndTimeParam = "end_time" 21 | ClickTimeParam = "click_time" 22 | QueryParam = "query" 23 | SortParam = "sort" 24 | ) 25 | 26 | const ( 27 | AllKinds = "_all" 28 | AllNamespaces = "_all" 29 | DefaultNamespace = "default" 30 | ) 31 | -------------------------------------------------------------------------------- /pkg/sloop/queries/query.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package queries 9 | 10 | import ( 11 | "fmt" 12 | "net/url" 13 | "time" 14 | 15 | "github.com/golang/glog" 16 | "github.com/salesforce/sloop/pkg/sloop/store/typed" 17 | ) 18 | 19 | // Takes in arguments from the web page, runs the query, and returns json 20 | type ganttJsonQuery = func(params url.Values, tables typed.Tables, startTime time.Time, endTime time.Time, requestId string) ([]byte, error) 21 | 22 | var funcMap = map[string]ganttJsonQuery{ 23 | "EventHeatMap": EventHeatMap3Query, 24 | "GetEventData": GetEventData, 25 | "GetResPayload": GetResPayload, 26 | "Namespaces": NamespaceQuery, 27 | "Kinds": KindQuery, 28 | "Queries": QueryAvailableQueries, 29 | "GetResSummaryData": GetResSummaryData, 30 | } 31 | 32 | func Default() string { 33 | return "EventHeatMap" 34 | } 35 | 36 | func GetNamesOfQueries() []string { 37 | return []string{"EventHeatMap"} 38 | } 39 | 40 | func RunQuery(queryName string, params url.Values, tables typed.Tables, maxLookBack time.Duration, requestId string) ([]byte, error) { 41 | startTime, endTime, err := computeTimeRange(params, tables, maxLookBack) 42 | if err != nil { 43 | glog.Errorf("computeTimeRange failed with error: %v", err) 44 | return []byte{}, err 45 | } 46 | 47 | fn, ok := funcMap[queryName] 48 | if !ok { 49 | return []byte{}, fmt.Errorf("Query not found: " + queryName) 50 | } 51 | ret, err := fn(params, tables, startTime, endTime, requestId) 52 | if err != nil { 53 | glog.Errorf("Query %v failed with error: %v", queryName, err) 54 | } 55 | return ret, err 56 | } 57 | -------------------------------------------------------------------------------- /pkg/sloop/queries/queryfilter.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package queries 9 | 10 | import ( 11 | "encoding/json" 12 | "fmt" 13 | "net/url" 14 | "sort" 15 | "time" 16 | 17 | "github.com/salesforce/sloop/pkg/sloop/kubeextractor" 18 | "github.com/salesforce/sloop/pkg/sloop/store/typed" 19 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 20 | ) 21 | 22 | // Consider: Make use of resources to limit what namespaces we return. 23 | // For example, if kind == ConfigMap, only return namespaces that contain a ConfigMap 24 | func NamespaceQuery(params url.Values, tables typed.Tables, startTime time.Time, endTime time.Time, requestId string) ([]byte, error) { 25 | var resourcesNs map[typed.ResourceSummaryKey]*typed.ResourceSummary 26 | err := tables.Db().View(func(txn badgerwrap.Txn) error { 27 | var err2 error 28 | var stats typed.RangeReadStats 29 | resourcesNs, stats, err2 = tables.ResourceSummaryTable().RangeRead(txn, nil, isNamespace, nil, startTime, endTime) 30 | if err2 != nil { 31 | return err2 32 | } 33 | stats.Log(requestId) 34 | return nil 35 | }) 36 | if err != nil { 37 | return []byte{}, err 38 | } 39 | namespaces := resSumRowsToNamespaceStrings(resourcesNs) 40 | namespaces = append(namespaces, AllNamespaces) 41 | bytes, err := json.MarshalIndent(namespaces, "", " ") 42 | if err != nil { 43 | return nil, fmt.Errorf("Failed to marshal json %v", err) 44 | } 45 | return bytes, nil 46 | } 47 | 48 | // TODO: Only return kinds for the specified namespace 49 | func KindQuery(params url.Values, tables typed.Tables, startTime time.Time, endTime time.Time, requestId string) ([]byte, error) { 50 | kindExists := make(map[string]bool) 51 | err := tables.Db().View(func(txn badgerwrap.Txn) error { 52 | _, stats, err2 := tables.ResourceSummaryTable().RangeRead(txn, nil, isKind(kindExists), nil, startTime, endTime) 53 | if err2 != nil { 54 | return err2 55 | } 56 | stats.Log(requestId) 57 | return nil 58 | }) 59 | if err != nil { 60 | return []byte{}, err 61 | } 62 | kinds := []string{AllKinds} 63 | for k, _ := range kindExists { 64 | kinds = append(kinds, k) 65 | } 66 | sort.Strings(kinds) 67 | 68 | bytes, err := json.MarshalIndent(kinds, "", " ") 69 | if err != nil { 70 | return nil, fmt.Errorf("Failed to marshal json %v", err) 71 | } 72 | return bytes, nil 73 | } 74 | 75 | func QueryAvailableQueries(params url.Values, tables typed.Tables, startTime time.Time, endTime time.Time, requestId string) ([]byte, error) { 76 | queries := GetNamesOfQueries() 77 | bytes, err := json.MarshalIndent(queries, "", " ") 78 | if err != nil { 79 | return nil, fmt.Errorf("Failed to marshal json %v", err) 80 | } 81 | return bytes, nil 82 | } 83 | 84 | func resSumRowsToNamespaceStrings(resources map[typed.ResourceSummaryKey]*typed.ResourceSummary) []string { 85 | namespaceList := []string{} 86 | namespaceExists := make(map[string]bool) 87 | for key, _ := range resources { 88 | _, ok := namespaceExists[key.Name] 89 | if !ok { 90 | namespaceList = append(namespaceList, key.Name) 91 | namespaceExists[key.Name] = true 92 | } 93 | } 94 | sort.Strings(namespaceList) 95 | return namespaceList 96 | } 97 | 98 | func isNamespace(k string) bool { 99 | key := &typed.ResourceSummaryKey{} 100 | err := key.Parse(k) 101 | if err != nil { 102 | return false 103 | } 104 | return key.Kind == kubeextractor.NamespaceKind 105 | } 106 | 107 | func isKind(kindExists map[string]bool) func(string) bool { 108 | return func(key string) bool { 109 | return keepResourceSummaryKind(key, kindExists) 110 | } 111 | } 112 | 113 | func resSumRowsToKindStrings(resources map[typed.ResourceSummaryKey]*typed.ResourceSummary) []string { 114 | kindList := []string{""} 115 | KindExists := make(map[string]bool) 116 | for key, _ := range resources { 117 | if _, ok := KindExists[key.Kind]; !ok { 118 | kindList = append(kindList, key.Kind) 119 | KindExists[key.Kind] = true 120 | } 121 | } 122 | sort.Strings(kindList) 123 | return kindList 124 | } 125 | 126 | func keepResourceSummaryKind(key string, kindExists map[string]bool) bool { 127 | // parse the key and get its kind 128 | k := &typed.ResourceSummaryKey{} 129 | err := k.Parse(key) 130 | if err != nil { 131 | return false 132 | } 133 | kind := k.Kind 134 | 135 | // if it is the first time to see the kind, return true, 136 | _, ok := kindExists[kind] 137 | if !ok { 138 | kindExists[kind] = true 139 | return true 140 | } 141 | return false 142 | } 143 | -------------------------------------------------------------------------------- /pkg/sloop/queries/ressumquery.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package queries 9 | 10 | import ( 11 | "encoding/json" 12 | "fmt" 13 | "net/url" 14 | "reflect" 15 | "time" 16 | 17 | "github.com/salesforce/sloop/pkg/sloop/store/typed" 18 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 19 | ) 20 | 21 | type ResSummaryOutput struct { 22 | typed.ResourceSummaryKey 23 | typed.ResourceSummary 24 | } 25 | 26 | func (r ResSummaryOutput) IsEmpty() bool { 27 | return reflect.DeepEqual(ResSummaryOutput{}, r) 28 | } 29 | 30 | func GetResSummaryData(params url.Values, t typed.Tables, startTime time.Time, endTime time.Time, requestId string) ([]byte, error) { 31 | var resSummaries map[typed.ResourceSummaryKey]*typed.ResourceSummary 32 | err := t.Db().View(func(txn badgerwrap.Txn) error { 33 | var err2 error 34 | var stats typed.RangeReadStats 35 | resSummaries, stats, err2 = t.ResourceSummaryTable().RangeRead(txn, nil, paramFilterResSumFn(params), isResSummaryValInTimeRange(startTime, endTime), startTime, endTime) 36 | if err2 != nil { 37 | return err2 38 | } 39 | stats.Log(requestId) 40 | return nil 41 | }) 42 | if err != nil { 43 | return []byte{}, err 44 | } 45 | 46 | output := ResSummaryOutput{} 47 | for key, val := range resSummaries { 48 | output.PartitionId = key.PartitionId 49 | output.Name = key.Name 50 | output.Namespace = key.Namespace 51 | output.Uid = key.Uid 52 | output.Kind = key.Kind 53 | output.FirstSeen = val.FirstSeen 54 | output.LastSeen = val.LastSeen 55 | output.CreateTime = val.CreateTime 56 | output.DeletedAtEnd = val.DeletedAtEnd 57 | output.Relationships = val.Relationships 58 | 59 | // we only need to get one resSummary 60 | break 61 | } 62 | 63 | if output.IsEmpty() { 64 | return []byte{}, nil 65 | } 66 | 67 | bytes, err := json.MarshalIndent(output, "", " ") 68 | if err != nil { 69 | return nil, fmt.Errorf("failed to marshal json %v", err) 70 | } 71 | return bytes, nil 72 | } 73 | -------------------------------------------------------------------------------- /pkg/sloop/queries/ressumquery_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package queries 9 | 10 | import ( 11 | "testing" 12 | "time" 13 | 14 | "github.com/golang/protobuf/ptypes" 15 | "github.com/salesforce/sloop/pkg/sloop/store/typed" 16 | "github.com/salesforce/sloop/pkg/sloop/store/untyped" 17 | "github.com/salesforce/sloop/pkg/sloop/test/assertex" 18 | "github.com/stretchr/testify/assert" 19 | ) 20 | 21 | func helper_getResSum(t *testing.T) *typed.ResourceSummary { 22 | firstTimeProto, err := ptypes.TimestampProto(someFirstSeenTime) 23 | assert.Nil(t, err) 24 | lastTimeProto, err := ptypes.TimestampProto(someLastSeenTime) 25 | assert.Nil(t, err) 26 | createTimeProto, err := ptypes.TimestampProto(someCreateTime) 27 | assert.Nil(t, err) 28 | val := &typed.ResourceSummary{ 29 | FirstSeen: firstTimeProto, 30 | LastSeen: lastTimeProto, 31 | CreateTime: createTimeProto, 32 | DeletedAtEnd: false, 33 | } 34 | return val 35 | } 36 | 37 | func Test_GetResSummaryData_False(t *testing.T) { 38 | untyped.TestHookSetPartitionDuration(time.Hour) 39 | values := helper_get_params() 40 | values[KindParam] = []string{"someKind"} 41 | values[NamespaceParam] = []string{"someNamespace"} 42 | values[NameParam] = []string{"someName"} 43 | values[UuidParam] = []string{"someuid"} 44 | keys := make([]*typed.ResourceSummaryKey, 2) 45 | keys[0] = typed.NewResourceSummaryKey(someTs, "someKind", "someNs", "mynamespace", "68510937-4ffc-11e9-8e26-1418775557c8") 46 | keys[1] = typed.NewResourceSummaryKey(someTs, "SomeKind", "namespace-b", "somename-b", "45510937-d4fc-11e9-8e26-14187754567") 47 | tables := helper_get_resSumtable(keys, t) 48 | res, err := GetResSummaryData(values, tables, someTs.Add(-60*time.Minute), someTs.Add(60*time.Minute), someRequestId) 49 | assert.Equal(t, string(res), "") 50 | assert.Nil(t, err) 51 | } 52 | 53 | func Test_GetResSummaryData_NotInTimeRange(t *testing.T) { 54 | untyped.TestHookSetPartitionDuration(time.Hour) 55 | values := helper_get_params() 56 | values[KindParam] = []string{"someKind"} 57 | values[NamespaceParam] = []string{"someNamespace"} 58 | values[NameParam] = []string{"someName"} 59 | values[UuidParam] = []string{"someuid"} 60 | keys := make([]*typed.ResourceSummaryKey, 1) 61 | keys[0] = typed.NewResourceSummaryKey(someTs, "someKind", "someNamespace", "someName", "someuid") 62 | tables := helper_get_resSumtable(keys, t) 63 | res, err := GetResSummaryData(values, tables, someTs.Add(60*time.Minute), someTs.Add(160*time.Minute), someRequestId) 64 | assert.Nil(t, err) 65 | assert.Equal(t, string(res), "") 66 | } 67 | 68 | func Test_GetResSummaryData_True(t *testing.T) { 69 | untyped.TestHookSetPartitionDuration(time.Hour) 70 | values := helper_get_params() 71 | values[KindParam] = []string{"someKind"} 72 | values[NamespaceParam] = []string{"someNamespace"} 73 | values[NameParam] = []string{"someName"} 74 | values[UuidParam] = []string{"someuid"} 75 | keys := make([]*typed.ResourceSummaryKey, 1) 76 | keys[0] = typed.NewResourceSummaryKey(someFirstSeenTime, "someKind", "someNamespace", "someName", "someuid") 77 | tables := helper_get_resSumtable(keys, t) 78 | res, err := GetResSummaryData(values, tables, someFirstSeenTime.Add(-1*time.Hour), someLastSeenTime.Add(6*time.Hour), someRequestId) 79 | assert.Nil(t, err) 80 | expectedRes := `{ 81 | "PartitionId": "001551668400", 82 | "Kind": "someKind", 83 | "Namespace": "someNamespace", 84 | "Name": "someName", 85 | "Uid": "someuid", 86 | "firstSeen": { 87 | "seconds": 1551668645, 88 | "nanos": 6 89 | }, 90 | "lastSeen": { 91 | "seconds": 1551837840 92 | } 93 | }` 94 | assertex.JsonEqual(t, expectedRes, string(res)) 95 | } 96 | -------------------------------------------------------------------------------- /pkg/sloop/queries/types.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package queries 9 | 10 | type TimelineRoot struct { 11 | ViewOpt ViewOptions `json:"view_options"` 12 | Rows []TimelineRow `json:"rows"` 13 | } 14 | 15 | type TimelineRow struct { 16 | Text string `json:"text"` 17 | Duration int64 `json:"duration"` 18 | Kind string `json:"kind"` 19 | Namespace string `json:"namespace"` 20 | Overlays []Overlay `json:"overlays"` 21 | ChangedAt []int64 `json:"changedat"` 22 | NoChangeAt []int64 `json:"nochangeat"` 23 | StartDate int64 `json:"start_date"` 24 | EndDate int64 `json:"end_date"` 25 | } 26 | 27 | type ViewOptions struct { 28 | Sort string `json:"sort"` 29 | } 30 | 31 | type Overlay struct { 32 | Text string `json:"text"` 33 | StartDate int64 `json:"start_date"` 34 | Duration int64 `json:"duration"` 35 | EndDate int64 `json:"end_date"` 36 | } 37 | -------------------------------------------------------------------------------- /pkg/sloop/server/internal/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/ghodss/yaml" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func Test_loadFromJSONFile_Success(t *testing.T) { 14 | var expectedconfig SloopConfig 15 | configfilename, _ := filepath.Abs("../testFiles/testconfig.json") 16 | configFile, err := ioutil.ReadFile(configfilename) 17 | err = json.Unmarshal(configFile, &expectedconfig) 18 | assert.Nil(t, err) 19 | 20 | var outConfig *SloopConfig 21 | outConfig = loadFromFile(configfilename, outConfig) 22 | assert.Equal(t, &expectedconfig, outConfig) 23 | } 24 | 25 | func Test_loadFromYAMLFile_Success(t *testing.T) { 26 | var expectedconfig SloopConfig 27 | configfilename, _ := filepath.Abs("../testFiles/testconfig.yaml") 28 | configFile, err := ioutil.ReadFile(configfilename) 29 | err = yaml.Unmarshal(configFile, &expectedconfig) 30 | assert.Nil(t, err) 31 | 32 | var outConfig *SloopConfig 33 | outConfig = loadFromFile(configfilename, outConfig) 34 | assert.Equal(t, &expectedconfig, outConfig) 35 | } 36 | 37 | func Test_loadFromTxtFile_shouldPanic(t *testing.T) { 38 | var config *SloopConfig 39 | configfilename, _ := filepath.Abs("../testFiles/testconfig.txt") 40 | assert.Panics(t, func() { loadFromFile(configfilename, config) }, "The code did not panic") 41 | } 42 | 43 | func Test_loadFromNoFile_shouldPanic(t *testing.T) { 44 | var config *SloopConfig 45 | configfilename, _ := filepath.Abs("../testconfig.json") 46 | assert.Panics(t, func() { loadFromFile(configfilename, config) }, "The code did not panic") 47 | } 48 | -------------------------------------------------------------------------------- /pkg/sloop/server/internal/testFiles/testConfig.txt: -------------------------------------------------------------------------------- 1 | { 2 | "ConfigFile": "", 3 | "leftBarLinks": null, 4 | "resourceLinks": null, 5 | "disableKubeWatch": false, 6 | "kubeWatchResyncInterval": 1800000000000, 7 | "webfilesPath": "./pkg/sloop/webserver/webfiles", 8 | "bindAddress": "", 9 | "port": 8080, 10 | "storeRoot": "./data", 11 | "maxLookBack": 1209600000000000, 12 | "maxDiskMb": 32768, 13 | "debugPlaybackFile": "", 14 | "debugRecordFile": "", 15 | "deletionBatchSize": 1000, 16 | "mockBadger": false, 17 | "disableStoreManager": false, 18 | "cleanupFrequency": 1800000000000, 19 | "keepMinorNodeUpdates": false, 20 | "defaultNamespace": "default", 21 | "defaultKind": "Pod", 22 | "defaultLookback": "6h", 23 | "context": "", 24 | "displayContext": "", 25 | "apiServerHost": "", 26 | "watchCrds": true, 27 | "threshold for GC": 0.8, 28 | "restoreDatabaseFile": "", 29 | "badgerDiscardRatio": 0.99, 30 | "badgerVLogGCFreq": 60000000000, 31 | "badgerMaxTableSize": 0, 32 | "badgerLevelOneSize": 0, 33 | "badgerLevSizeMultiplier": 0, 34 | "badgerKeepL0InMemory": true, 35 | "badgerVLogFileSize": 0, 36 | "badgerVLogMaxEntries": 200000, 37 | "badgerUseLSMOnlyOptions": true, 38 | "badgerEnableEventLogging": false, 39 | "badgerNumOfCompactors": 0, 40 | "badgerSyncWrites": true, 41 | "badgerVLogFileIOMapping": false, 42 | "badgerVLogTruncate": true, 43 | "enableDeleteKeys": false 44 | } -------------------------------------------------------------------------------- /pkg/sloop/server/internal/testFiles/testconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConfigFile": "", 3 | "leftBarLinks": null, 4 | "resourceLinks": null, 5 | "defaultNamespace": "_all", 6 | "defaultKind": "Pod", 7 | "defaultLookback": "12h", 8 | "context": "", 9 | "displayContext": "", 10 | "apiServerHost": "", 11 | "watchCrds": true, 12 | "threshold for GC": 0.8, 13 | "badgerDiscardRatio": 0.92, 14 | "maxLookBack": 110960000000000 15 | } -------------------------------------------------------------------------------- /pkg/sloop/server/internal/testFiles/testconfig.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | ConfigFile: '' 3 | leftBarLinks: 4 | resourceLinks: 5 | port: 9090 6 | maxLookBack: 1109600000000000 7 | keepMinorNodeUpdates: false 8 | defaultNamespace: default 9 | defaultKind: Deployment 10 | defaultLookback: 3h 11 | threshold for GC: 0.75 12 | badgerDiscardRatio: 0.80 13 | badgerVLogMaxEntries: 150000 14 | badgerUseLSMOnlyOptions: true 15 | -------------------------------------------------------------------------------- /pkg/sloop/store/typed/README.md: -------------------------------------------------------------------------------- 1 | # Tables in Sloop 2 | 3 | 4 | 5 | 6 | ---- 7 | 8 | There are four tables in Sloop to store data: 9 | 10 | 1. Watch table 11 | 1. Resources summary table 12 | 1. Event count table 13 | 1. Watch activity table 14 | 15 | ---- 16 | 17 | Details: 18 | 19 | 1. Watch table: 20 | It has the raw kube watch data. It is the source of truth for the whole data. 21 | 22 | 1. Resource Summary: It stores the resources information including name, creation date, deployment details and last update time. 23 | 24 | 1. Event table: It stores the event details that took place for a resource. The information includes event type, message and time stamp. 25 | 26 | 1. Watch Activity table: It stores any watch activity received. It has the information that was there a change from the last known state or not. 27 | 28 | 29 | ## Data Distribution 30 | 31 | The data distribution in terms of size among the tables is shown below. As expected, watch table occupies the most space as it contains the raw data. Rest of the tables are derived from it. 32 | ![DataDistribution](../../../../other/data_distribution.png?raw=true "Data Distribution among Sloop tables") 33 | -------------------------------------------------------------------------------- /pkg/sloop/store/typed/eventcounttable.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package typed 9 | 10 | import ( 11 | "fmt" 12 | "time" 13 | 14 | badger "github.com/dgraph-io/badger/v2" 15 | "github.com/salesforce/sloop/pkg/sloop/common" 16 | "github.com/salesforce/sloop/pkg/sloop/store/untyped" 17 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 18 | ) 19 | 20 | type EventCountKey struct { 21 | PartitionId string 22 | Kind string 23 | Namespace string 24 | Name string 25 | Uid string 26 | } 27 | 28 | func NewEventCountKey(timestamp time.Time, kind string, namespace string, name string, uid string) *EventCountKey { 29 | partitionId := untyped.GetPartitionId(timestamp) 30 | return &EventCountKey{PartitionId: partitionId, Kind: kind, Namespace: namespace, Name: name, Uid: uid} 31 | } 32 | 33 | func NewEventCountKeyComparator(kind string, namespace string, name string, uid string) *EventCountKey { 34 | return &EventCountKey{Kind: kind, Namespace: namespace, Name: name, Uid: uid} 35 | } 36 | 37 | func (*EventCountKey) TableName() string { 38 | return "eventcount" 39 | } 40 | 41 | func (k *EventCountKey) Parse(key string) error { 42 | err, parts := common.ParseKey(key) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | if parts[1] != k.TableName() { 48 | return fmt.Errorf("Second part of key (%v) should be %v", key, k.TableName()) 49 | } 50 | k.PartitionId = parts[2] 51 | k.Kind = parts[3] 52 | k.Namespace = parts[4] 53 | k.Name = parts[5] 54 | k.Uid = parts[6] 55 | return nil 56 | } 57 | 58 | // todo: need to make sure it can work as keyPrefix when some fields are empty 59 | func (k *EventCountKey) String() string { 60 | if k.Uid == "" { 61 | return fmt.Sprintf("/%v/%v/%v/%v/%v", k.TableName(), k.PartitionId, k.Kind, k.Namespace, k.Name) 62 | } else { 63 | return fmt.Sprintf("/%v/%v/%v/%v/%v/%v", k.TableName(), k.PartitionId, k.Kind, k.Namespace, k.Name, k.Uid) 64 | } 65 | } 66 | 67 | func (*EventCountKey) ValidateKey(key string) error { 68 | newKey := EventCountKey{} 69 | return newKey.Parse(key) 70 | } 71 | 72 | func (t *ResourceEventCountsTable) GetOrDefault(txn badgerwrap.Txn, key string) (*ResourceEventCounts, error) { 73 | rec, err := t.Get(txn, key) 74 | if err != nil { 75 | if err != badger.ErrKeyNotFound { 76 | return nil, err 77 | } else { 78 | return &ResourceEventCounts{MapMinToEvents: make(map[int64]*EventCounts)}, nil 79 | } 80 | } 81 | return rec, nil 82 | } 83 | 84 | func (k *EventCountKey) SetPartitionId(newPartitionId string) { 85 | k.PartitionId = newPartitionId 86 | } 87 | -------------------------------------------------------------------------------- /pkg/sloop/store/typed/eventcounttablegen_test.go: -------------------------------------------------------------------------------- 1 | // This file was automatically generated by genny. 2 | // Any changes will be lost if this file is regenerated. 3 | // see https://github.com/cheekybits/genny 4 | 5 | /* 6 | * Copyright (c) 2019, salesforce.com, inc. 7 | * All rights reserved. 8 | * SPDX-License-Identifier: BSD-3-Clause 9 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 10 | */ 11 | 12 | package typed 13 | 14 | import ( 15 | "fmt" 16 | "reflect" 17 | "testing" 18 | "time" 19 | 20 | "github.com/dgraph-io/badger/v2" 21 | "github.com/salesforce/sloop/pkg/sloop/store/untyped" 22 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func helper_ResourceEventCounts_ShouldSkip() bool { 27 | // Tests will not work on the fake types in the template, but we want to run tests on real objects 28 | if "typed.Value"+"Type" == fmt.Sprint(reflect.TypeOf(ResourceEventCounts{})) { 29 | fmt.Printf("Skipping unit test") 30 | return true 31 | } 32 | return false 33 | } 34 | 35 | func Test_ResourceEventCountsTable_SetWorks(t *testing.T) { 36 | if helper_ResourceEventCounts_ShouldSkip() { 37 | return 38 | } 39 | 40 | untyped.TestHookSetPartitionDuration(time.Hour * 24) 41 | db, err := (&badgerwrap.MockFactory{}).Open(badger.DefaultOptions("")) 42 | assert.Nil(t, err) 43 | err = db.Update(func(txn badgerwrap.Txn) error { 44 | k := (&EventCountKey{}).GetTestKey() 45 | vt := OpenResourceEventCountsTable() 46 | err2 := vt.Set(txn, k, (&EventCountKey{}).GetTestValue()) 47 | assert.Nil(t, err2) 48 | return nil 49 | }) 50 | assert.Nil(t, err) 51 | } 52 | 53 | func helper_update_ResourceEventCountsTable(t *testing.T, keys []string, val *ResourceEventCounts) (badgerwrap.DB, *ResourceEventCountsTable) { 54 | b, err := (&badgerwrap.MockFactory{}).Open(badger.DefaultOptions("")) 55 | assert.Nil(t, err) 56 | wt := OpenResourceEventCountsTable() 57 | err = b.Update(func(txn badgerwrap.Txn) error { 58 | var txerr error 59 | for _, key := range keys { 60 | txerr = wt.Set(txn, key, val) 61 | if txerr != nil { 62 | return txerr 63 | } 64 | } 65 | // Add some keys outside the range 66 | txerr = txn.Set([]byte("/a/123/"), []byte{}) 67 | if txerr != nil { 68 | return txerr 69 | } 70 | txerr = txn.Set([]byte("/zzz/123/"), []byte{}) 71 | if txerr != nil { 72 | return txerr 73 | } 74 | return nil 75 | }) 76 | assert.Nil(t, err) 77 | return b, wt 78 | } 79 | 80 | func Test_ResourceEventCountsTable_GetUniquePartitionList_Success(t *testing.T) { 81 | if helper_ResourceEventCounts_ShouldSkip() { 82 | return 83 | } 84 | 85 | db, wt := helper_update_ResourceEventCountsTable(t, (&EventCountKey{}).SetTestKeys(), (&EventCountKey{}).SetTestValue()) 86 | var partList []string 87 | var err1 error 88 | err := db.View(func(txn badgerwrap.Txn) error { 89 | partList, err1 = wt.GetUniquePartitionList(txn) 90 | return nil 91 | }) 92 | assert.Nil(t, err) 93 | assert.Nil(t, err1) 94 | assert.Len(t, partList, 3) 95 | assert.Contains(t, partList, someMinPartition) 96 | assert.Contains(t, partList, someMiddlePartition) 97 | assert.Contains(t, partList, someMaxPartition) 98 | } 99 | 100 | func Test_ResourceEventCountsTable_GetUniquePartitionList_EmptyPartition(t *testing.T) { 101 | if helper_ResourceEventCounts_ShouldSkip() { 102 | return 103 | } 104 | 105 | db, wt := helper_update_ResourceEventCountsTable(t, []string{}, &ResourceEventCounts{}) 106 | var partList []string 107 | var err1 error 108 | err := db.View(func(txn badgerwrap.Txn) error { 109 | partList, err1 = wt.GetUniquePartitionList(txn) 110 | return err1 111 | }) 112 | assert.Nil(t, err) 113 | assert.Len(t, partList, 0) 114 | } 115 | -------------------------------------------------------------------------------- /pkg/sloop/store/typed/protobufhelper.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package typed 9 | 10 | import ( 11 | "time" 12 | 13 | "github.com/golang/protobuf/ptypes" 14 | "github.com/golang/protobuf/ptypes/timestamp" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | func StringToProtobufTimestamp(ts string) (*timestamp.Timestamp, error) { 19 | t, err := time.Parse(time.RFC3339, ts) 20 | if err != nil { 21 | return nil, errors.Wrap(err, "could not parse timestamp") 22 | } 23 | 24 | tspb, err := ptypes.TimestampProto(t) 25 | if err != nil { 26 | return nil, errors.Wrap(err, "could not transform to proto timestamp") 27 | } 28 | 29 | return tspb, nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/sloop/store/typed/protobufhelper_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package typed 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/golang/protobuf/ptypes/timestamp" 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func Test_StringToProtobufTimestamp_Success(t *testing.T) { 18 | expectedResult := ×tamp.Timestamp{ 19 | Seconds: 1562962332, 20 | Nanos: 0, 21 | } 22 | ts, err := StringToProtobufTimestamp("2019-07-12T20:12:12Z") 23 | assert.Nil(t, err) 24 | assert.Equal(t, expectedResult, ts) 25 | } 26 | 27 | func Test_StringToProtobufTimestamp_FailureCannotParse(t *testing.T) { 28 | ts, err := StringToProtobufTimestamp("2019-070:12:12Z") 29 | assert.NotNil(t, err) 30 | assert.Contains(t, err.Error(), "could not parse timestamp") 31 | assert.Nil(t, ts) 32 | } 33 | 34 | func Test_StringToProtobufTimestamp_FailureCannotTransformToPB(t *testing.T) { 35 | ts, err := StringToProtobufTimestamp("0000-07-12T20:12:12Z") 36 | assert.NotNil(t, err) 37 | assert.Contains(t, err.Error(), "could not transform to proto timestamp") 38 | assert.Nil(t, ts) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/sloop/store/typed/resourcesummarytable.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package typed 9 | 10 | import ( 11 | "fmt" 12 | "time" 13 | 14 | "github.com/salesforce/sloop/pkg/sloop/common" 15 | "github.com/salesforce/sloop/pkg/sloop/store/untyped" 16 | ) 17 | 18 | // Key is ///// 19 | // 20 | // Partition is UnixSeconds rounded down to partition duration 21 | // Kind is kubernetes kind, starts with upper case 22 | // Namespace is kubernetes namespace, all lower 23 | // Name is kubernetes name, all lower 24 | // Uid is kubernetes $.metadata.uid 25 | 26 | type ResourceSummaryKey struct { 27 | PartitionId string 28 | Kind string 29 | Namespace string 30 | Name string 31 | Uid string 32 | } 33 | 34 | func NewResourceSummaryKey(timestamp time.Time, kind string, namespace string, name string, uid string) *ResourceSummaryKey { 35 | partitionId := untyped.GetPartitionId(timestamp) 36 | return &ResourceSummaryKey{PartitionId: partitionId, Kind: kind, Namespace: namespace, Name: name, Uid: uid} 37 | } 38 | 39 | func NewResourceSummaryKeyComparator(kind string, namespace string, name string, uid string) *ResourceSummaryKey { 40 | return &ResourceSummaryKey{Kind: kind, Namespace: namespace, Name: name, Uid: uid} 41 | } 42 | 43 | func (*ResourceSummaryKey) TableName() string { 44 | return "ressum" 45 | } 46 | 47 | func (k *ResourceSummaryKey) Parse(key string) error { 48 | err, parts := common.ParseKey(key) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if parts[1] != k.TableName() { 54 | return fmt.Errorf("Second part of key (%v) should be %v", key, k.TableName()) 55 | } 56 | k.PartitionId = parts[2] 57 | k.Kind = parts[3] 58 | k.Namespace = parts[4] 59 | k.Name = parts[5] 60 | k.Uid = parts[6] 61 | return nil 62 | } 63 | 64 | // todo: need to make sure it can work as keyPrefix when some fields are empty 65 | func (k *ResourceSummaryKey) String() string { 66 | if k.Uid == "" { 67 | return fmt.Sprintf("/%v/%v/%v/%v/%v", k.TableName(), k.PartitionId, k.Kind, k.Namespace, k.Name) 68 | } else { 69 | return fmt.Sprintf("/%v/%v/%v/%v/%v/%v", k.TableName(), k.PartitionId, k.Kind, k.Namespace, k.Name, k.Uid) 70 | } 71 | } 72 | 73 | func (k *ResourceSummaryKey) SetPartitionId(newPartitionId string) { 74 | k.PartitionId = newPartitionId 75 | } 76 | 77 | func (*ResourceSummaryKey) ValidateKey(key string) error { 78 | newKey := ResourceSummaryKey{} 79 | return newKey.Parse(key) 80 | } 81 | -------------------------------------------------------------------------------- /pkg/sloop/store/typed/resourcesummarytablegen_test.go: -------------------------------------------------------------------------------- 1 | // This file was automatically generated by genny. 2 | // Any changes will be lost if this file is regenerated. 3 | // see https://github.com/cheekybits/genny 4 | 5 | /* 6 | * Copyright (c) 2019, salesforce.com, inc. 7 | * All rights reserved. 8 | * SPDX-License-Identifier: BSD-3-Clause 9 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 10 | */ 11 | 12 | package typed 13 | 14 | import ( 15 | "fmt" 16 | "reflect" 17 | "testing" 18 | "time" 19 | 20 | badger "github.com/dgraph-io/badger/v2" 21 | "github.com/salesforce/sloop/pkg/sloop/store/untyped" 22 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func helper_ResourceSummary_ShouldSkip() bool { 27 | // Tests will not work on the fake types in the template, but we want to run tests on real objects 28 | if "typed.Value"+"Type" == fmt.Sprint(reflect.TypeOf(ResourceSummary{})) { 29 | fmt.Printf("Skipping unit test") 30 | return true 31 | } 32 | return false 33 | } 34 | 35 | func Test_ResourceSummaryTable_SetWorks(t *testing.T) { 36 | if helper_ResourceSummary_ShouldSkip() { 37 | return 38 | } 39 | 40 | untyped.TestHookSetPartitionDuration(time.Hour * 24) 41 | db, err := (&badgerwrap.MockFactory{}).Open(badger.DefaultOptions("")) 42 | assert.Nil(t, err) 43 | err = db.Update(func(txn badgerwrap.Txn) error { 44 | k := (&ResourceSummaryKey{}).GetTestKey() 45 | vt := OpenResourceSummaryTable() 46 | err2 := vt.Set(txn, k, (&ResourceSummaryKey{}).GetTestValue()) 47 | assert.Nil(t, err2) 48 | return nil 49 | }) 50 | assert.Nil(t, err) 51 | } 52 | 53 | func helper_update_ResourceSummaryTable(t *testing.T, keys []string, val *ResourceSummary) (badgerwrap.DB, *ResourceSummaryTable) { 54 | b, err := (&badgerwrap.MockFactory{}).Open(badger.DefaultOptions("")) 55 | assert.Nil(t, err) 56 | wt := OpenResourceSummaryTable() 57 | err = b.Update(func(txn badgerwrap.Txn) error { 58 | var txerr error 59 | for _, key := range keys { 60 | txerr = wt.Set(txn, key, val) 61 | if txerr != nil { 62 | return txerr 63 | } 64 | } 65 | // Add some keys outside the range 66 | txerr = txn.Set([]byte("/a/123/"), []byte{}) 67 | if txerr != nil { 68 | return txerr 69 | } 70 | txerr = txn.Set([]byte("/zzz/123/"), []byte{}) 71 | if txerr != nil { 72 | return txerr 73 | } 74 | return nil 75 | }) 76 | assert.Nil(t, err) 77 | return b, wt 78 | } 79 | 80 | func Test_ResourceSummaryTable_GetUniquePartitionList_Success(t *testing.T) { 81 | if helper_ResourceSummary_ShouldSkip() { 82 | return 83 | } 84 | 85 | db, wt := helper_update_ResourceSummaryTable(t, (&ResourceSummaryKey{}).SetTestKeys(), (&ResourceSummaryKey{}).SetTestValue()) 86 | var partList []string 87 | var err1 error 88 | err := db.View(func(txn badgerwrap.Txn) error { 89 | partList, err1 = wt.GetUniquePartitionList(txn) 90 | return nil 91 | }) 92 | assert.Nil(t, err) 93 | assert.Nil(t, err1) 94 | assert.Len(t, partList, 3) 95 | assert.Contains(t, partList, someMinPartition) 96 | assert.Contains(t, partList, someMiddlePartition) 97 | assert.Contains(t, partList, someMaxPartition) 98 | } 99 | 100 | func Test_ResourceSummaryTable_GetUniquePartitionList_EmptyPartition(t *testing.T) { 101 | if helper_ResourceSummary_ShouldSkip() { 102 | return 103 | } 104 | 105 | db, wt := helper_update_ResourceSummaryTable(t, []string{}, &ResourceSummary{}) 106 | var partList []string 107 | var err1 error 108 | err := db.View(func(txn badgerwrap.Txn) error { 109 | partList, err1 = wt.GetUniquePartitionList(txn) 110 | return err1 111 | }) 112 | assert.Nil(t, err) 113 | assert.Len(t, partList, 0) 114 | } 115 | -------------------------------------------------------------------------------- /pkg/sloop/store/typed/schema.proto: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | syntax = 'proto3'; 9 | 10 | package typed; 11 | 12 | import "google/protobuf/timestamp.proto"; 13 | 14 | message KubeWatchResult { 15 | enum WatchType { 16 | ADD = 0; 17 | UPDATE = 1; 18 | DELETE = 2; 19 | } 20 | 21 | google.protobuf.Timestamp timestamp = 1; // This is a bit of a pain to convert, but uint64 is just as bad 22 | string kind = 2; 23 | WatchType watchType = 3; 24 | string payload = 4; 25 | } 26 | 27 | // Enough information to draw a timeline and hierarchy 28 | // Key: //// 29 | message ResourceSummary { 30 | google.protobuf.Timestamp firstSeen = 1; // Scoped to this partition 31 | google.protobuf.Timestamp lastSeen = 2; // Also scoped to this partition 32 | google.protobuf.Timestamp createTime = 3; // Taken from in resource 33 | bool deletedAtEnd = 4; // Tells us that the lastSeen time is also when it was deleted 34 | // List of relationships. Direction does not matter. Examples: 35 | // A Pod has a relationship to its namespace, ReplicaSet or StatefulSet, node 36 | // A ReplicaSet has a relationship to deployment and namespace 37 | // A node might have a relationship to a rack (maybe latery, as this is virtual) 38 | // We dont need relationships in both directions. We can union them at query time 39 | // Uses same key format here as this overall table 40 | repeated string relationships = 5; 41 | } 42 | 43 | message EventCounts { 44 | map mapReasonToCount = 1; 45 | } 46 | 47 | message ResourceEventCounts { 48 | map mapMinToEvents = 1; 49 | } 50 | 51 | // Track when 'watch' occurred for a resource within partition 52 | message WatchActivity { 53 | // List of timestamps where `watch` event did not contain changes from previous event 54 | repeated int64 NoChangeAt = 1; 55 | // List of timestamps where 'watch' event contained a change from previous event 56 | repeated int64 ChangedAt = 2; 57 | } 58 | -------------------------------------------------------------------------------- /pkg/sloop/store/typed/tables.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package typed 9 | 10 | import ( 11 | "sort" 12 | 13 | "github.com/golang/glog" 14 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 15 | ) 16 | 17 | type Tables interface { 18 | ResourceSummaryTable() *ResourceSummaryTable 19 | EventCountTable() *ResourceEventCountsTable 20 | WatchTable() *KubeWatchResultTable 21 | WatchActivityTable() *WatchActivityTable 22 | Db() badgerwrap.DB 23 | GetMinAndMaxPartition() (bool, string, string, error) 24 | GetTableNames() []string 25 | GetTables() []interface{} 26 | GetMinAndMaxPartitionWithTxn(badgerwrap.Txn) (bool, string, string) 27 | } 28 | 29 | type MinMaxPartitionsGetter interface { 30 | GetMinMaxPartitions(badgerwrap.Txn) (bool, string, string) 31 | } 32 | 33 | type tablesImpl struct { 34 | resourceSummaryTable *ResourceSummaryTable 35 | eventCountTable *ResourceEventCountsTable 36 | watchTable *KubeWatchResultTable 37 | watchActivityTable *WatchActivityTable 38 | db badgerwrap.DB 39 | } 40 | 41 | func NewTableList(db badgerwrap.DB) Tables { 42 | t := &tablesImpl{} 43 | t.resourceSummaryTable = OpenResourceSummaryTable() 44 | t.eventCountTable = OpenResourceEventCountsTable() 45 | t.watchTable = OpenKubeWatchResultTable() 46 | t.watchActivityTable = OpenWatchActivityTable() 47 | t.db = db 48 | return t 49 | } 50 | 51 | func (t *tablesImpl) ResourceSummaryTable() *ResourceSummaryTable { 52 | return t.resourceSummaryTable 53 | } 54 | 55 | func (t *tablesImpl) EventCountTable() *ResourceEventCountsTable { 56 | return t.eventCountTable 57 | } 58 | 59 | func (t *tablesImpl) WatchTable() *KubeWatchResultTable { 60 | return t.watchTable 61 | } 62 | 63 | func (t *tablesImpl) WatchActivityTable() *WatchActivityTable { 64 | return t.watchActivityTable 65 | } 66 | 67 | func (t *tablesImpl) Db() badgerwrap.DB { 68 | return t.db 69 | } 70 | 71 | func (t *tablesImpl) GetMinAndMaxPartition() (bool, string, string, error) { 72 | var ok bool 73 | var minPartition string 74 | var maxPartition string 75 | err := t.db.View(func(txn badgerwrap.Txn) error { 76 | ok, minPartition, maxPartition = t.GetMinAndMaxPartitionWithTxn(txn) 77 | return nil 78 | }) 79 | 80 | if err != nil { 81 | return false, "", "", err 82 | } 83 | 84 | return ok, minPartition, maxPartition, nil 85 | } 86 | 87 | func (t *tablesImpl) GetMinAndMaxPartitionWithTxn(txn badgerwrap.Txn) (bool, string, string) { 88 | allPartitions := []string{} 89 | for _, table := range t.GetTables() { 90 | coerced, canCoerce := table.(MinMaxPartitionsGetter) 91 | if !canCoerce { 92 | glog.Errorf("Expected type to implement GetMinMaxPartitions but failed") 93 | continue 94 | } 95 | ok, minPar, maxPar := coerced.GetMinMaxPartitions(txn) 96 | if ok { 97 | allPartitions = append(allPartitions, minPar, maxPar) 98 | } 99 | } 100 | if len(allPartitions) == 0 { 101 | return false, "", "" 102 | } 103 | 104 | sort.Strings(allPartitions) 105 | return true, allPartitions[0], allPartitions[len(allPartitions)-1] 106 | } 107 | 108 | func (t *tablesImpl) GetTableNames() []string { 109 | return []string{t.watchTable.tableName, t.resourceSummaryTable.tableName, t.eventCountTable.tableName, t.watchActivityTable.tableName} 110 | } 111 | 112 | func (t *tablesImpl) GetTables() []interface{} { 113 | intfs := new([]interface{}) 114 | *intfs = append(*intfs, t.eventCountTable, t.resourceSummaryTable, t.watchTable, t.watchActivityTable) 115 | return *intfs 116 | } 117 | -------------------------------------------------------------------------------- /pkg/sloop/store/typed/tabletemplate_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package typed 9 | 10 | import ( 11 | "fmt" 12 | "reflect" 13 | "testing" 14 | "time" 15 | 16 | badger "github.com/dgraph-io/badger/v2" 17 | "github.com/salesforce/sloop/pkg/sloop/store/untyped" 18 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 19 | "github.com/stretchr/testify/assert" 20 | ) 21 | 22 | //go:generate genny -in=$GOFILE -out=watchtablegen_test.go gen "ValueType=KubeWatchResult KeyType=WatchTableKey" 23 | //go:generate genny -in=$GOFILE -out=resourcesummarytablegen_test.go gen "ValueType=ResourceSummary KeyType=ResourceSummaryKey" 24 | //go:generate genny -in=$GOFILE -out=eventcounttablegen_test.go gen "ValueType=ResourceEventCounts KeyType=EventCountKey" 25 | //go:generate genny -in=$GOFILE -out=watchactivitytablegen_test.go gen "ValueType=WatchActivity KeyType=WatchActivityKey" 26 | 27 | func helper_ValueType_ShouldSkip() bool { 28 | // Tests will not work on the fake types in the template, but we want to run tests on real objects 29 | if "typed.Value"+"Type" == fmt.Sprint(reflect.TypeOf(ValueType{})) { 30 | fmt.Printf("Skipping unit test") 31 | return true 32 | } 33 | return false 34 | } 35 | 36 | func Test_ValueTypeTable_SetWorks(t *testing.T) { 37 | if helper_ValueType_ShouldSkip() { 38 | return 39 | } 40 | 41 | untyped.TestHookSetPartitionDuration(time.Hour * 24) 42 | db, err := (&badgerwrap.MockFactory{}).Open(badger.DefaultOptions("")) 43 | assert.Nil(t, err) 44 | err = db.Update(func(txn badgerwrap.Txn) error { 45 | k := (&KeyType{}).GetTestKey() 46 | vt := OpenValueTypeTable() 47 | err2 := vt.Set(txn, k, (&KeyType{}).GetTestValue()) 48 | assert.Nil(t, err2) 49 | return nil 50 | }) 51 | assert.Nil(t, err) 52 | } 53 | 54 | func helper_update_ValueTypeTable(t *testing.T, keys []string, val *ValueType) (badgerwrap.DB, *ValueTypeTable) { 55 | b, err := (&badgerwrap.MockFactory{}).Open(badger.DefaultOptions("")) 56 | assert.Nil(t, err) 57 | wt := OpenValueTypeTable() 58 | err = b.Update(func(txn badgerwrap.Txn) error { 59 | var txerr error 60 | for _, key := range keys { 61 | txerr = wt.Set(txn, key, val) 62 | if txerr != nil { 63 | return txerr 64 | } 65 | } 66 | // Add some keys outside the range 67 | txerr = txn.Set([]byte("/a/123/"), []byte{}) 68 | if txerr != nil { 69 | return txerr 70 | } 71 | txerr = txn.Set([]byte("/zzz/123/"), []byte{}) 72 | if txerr != nil { 73 | return txerr 74 | } 75 | return nil 76 | }) 77 | assert.Nil(t, err) 78 | return b, wt 79 | } 80 | 81 | func Test_ValueTypeTable_GetUniquePartitionList_Success(t *testing.T) { 82 | if helper_ValueType_ShouldSkip() { 83 | return 84 | } 85 | 86 | db, wt := helper_update_ValueTypeTable(t, (&KeyType{}).SetTestKeys(), (&KeyType{}).SetTestValue()) 87 | var partList []string 88 | var err1 error 89 | err := db.View(func(txn badgerwrap.Txn) error { 90 | partList, err1 = wt.GetUniquePartitionList(txn) 91 | return nil 92 | }) 93 | assert.Nil(t, err) 94 | assert.Nil(t, err1) 95 | assert.Len(t, partList, 3) 96 | assert.Contains(t, partList, someMinPartition) 97 | assert.Contains(t, partList, someMiddlePartition) 98 | assert.Contains(t, partList, someMaxPartition) 99 | } 100 | 101 | func Test_ValueTypeTable_GetUniquePartitionList_EmptyPartition(t *testing.T) { 102 | if helper_ValueType_ShouldSkip() { 103 | return 104 | } 105 | 106 | db, wt := helper_update_ValueTypeTable(t, []string{}, &ValueType{}) 107 | var partList []string 108 | var err1 error 109 | err := db.View(func(txn badgerwrap.Txn) error { 110 | partList, err1 = wt.GetUniquePartitionList(txn) 111 | return err1 112 | }) 113 | assert.Nil(t, err) 114 | assert.Len(t, partList, 0) 115 | } 116 | -------------------------------------------------------------------------------- /pkg/sloop/store/typed/tabletemplatehelper.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package typed 9 | 10 | import ( 11 | "time" 12 | 13 | "github.com/golang/glog" 14 | "github.com/salesforce/sloop/pkg/sloop/common" 15 | ) 16 | 17 | // The code in this file is simply here to let us compile tabletemplate.go but these are 18 | // things we dont want in the generated output as they would conflict with functions on the real value and key types 19 | 20 | type ValueType struct { 21 | } 22 | 23 | func (p *ValueType) Reset() { 24 | } 25 | 26 | func (p *ValueType) String() string { 27 | return "" 28 | } 29 | 30 | func (p *ValueType) ProtoMessage() { 31 | } 32 | 33 | type KeyType struct { 34 | PartitionId string 35 | } 36 | 37 | func (*KeyType) ValidateKey(key string) error { 38 | panic("Placeholder key type should never be used") 39 | } 40 | 41 | func (*KeyType) TableName() string { 42 | panic("Placeholder key should not be used") 43 | } 44 | 45 | func (*KeyType) Parse(key string) error { 46 | panic("Placeholder key should not be used") 47 | } 48 | 49 | func (*KeyType) GetTestKey() string { 50 | panic("Placeholder key should not be used") 51 | } 52 | 53 | func (*KeyType) SetTestKeys() []string { 54 | panic("Placeholder key should not be used") 55 | } 56 | 57 | func (*KeyType) String() string { 58 | panic("Placeholder key should not be used") 59 | } 60 | 61 | func (*KeyType) GetTestValue() *ValueType { 62 | panic("Placeholder key should not be used") 63 | } 64 | 65 | func (p *KeyType) SetTestValue() *ValueType { 66 | return &ValueType{} 67 | } 68 | 69 | func (*KeyType) SetPartitionId(newPartitionId string) { 70 | panic("Placeholder key should not be used") 71 | } 72 | 73 | type RangeReadStats struct { 74 | TableName string 75 | PartitionCount int 76 | RowsVisitedCount int 77 | RowsPassedKeyPredicateCount int 78 | RowsPassedValuePredicateCount int 79 | Elapsed time.Duration 80 | } 81 | 82 | func (stats RangeReadStats) Log(requestId string) { 83 | glog.V(common.GlogVerbose).Infof("reqId: %v range read on table %v took %v. Partitions scanned %v. Rows scanned %v, past key predicate %v, past value predicate %v", 84 | requestId, stats.TableName, stats.Elapsed, stats.PartitionCount, stats.RowsVisitedCount, stats.RowsPassedKeyPredicateCount, stats.RowsPassedValuePredicateCount) 85 | } 86 | -------------------------------------------------------------------------------- /pkg/sloop/store/typed/watchactivitytable.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package typed 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/dgraph-io/badger/v2" 14 | "github.com/salesforce/sloop/pkg/sloop/common" 15 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 16 | ) 17 | 18 | // Key is //// 19 | // 20 | // Partition is UnixSeconds rounded down to partition duration 21 | // Kind is kubernetes kind, starts with upper case 22 | // Namespace is kubernetes namespace, all lower 23 | // Name is kubernetes name, all lower 24 | 25 | type WatchActivityKey struct { 26 | PartitionId string 27 | Kind string 28 | Namespace string 29 | Name string 30 | Uid string 31 | } 32 | 33 | func NewWatchActivityKey(partitionId string, kind string, namespace string, name string, uid string) *WatchActivityKey { 34 | return &WatchActivityKey{PartitionId: partitionId, Kind: kind, Namespace: namespace, Name: name, Uid: uid} 35 | } 36 | 37 | func NewWatchActivityKeyComparator(kind string, namespace string, name string, uid string) *WatchActivityKey { 38 | return &WatchActivityKey{Kind: kind, Namespace: namespace, Name: name, Uid: uid} 39 | } 40 | 41 | func (*WatchActivityKey) TableName() string { 42 | return "watchactivity" 43 | } 44 | 45 | func (k *WatchActivityKey) Parse(key string) error { 46 | err, parts := common.ParseKey(key) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | if parts[1] != k.TableName() { 52 | return fmt.Errorf("Second part of key (%v) should be %v", key, k.TableName()) 53 | } 54 | k.PartitionId = parts[2] 55 | k.Kind = parts[3] 56 | k.Namespace = parts[4] 57 | k.Name = parts[5] 58 | k.Uid = parts[6] 59 | return nil 60 | } 61 | 62 | // todo: need to make sure it can work as keyPrefix when some fields are empty 63 | func (k *WatchActivityKey) String() string { 64 | return fmt.Sprintf("/%v/%v/%v/%v/%v/%v", k.TableName(), k.PartitionId, k.Kind, k.Namespace, k.Name, k.Uid) 65 | } 66 | 67 | func (*WatchActivityKey) ValidateKey(key string) error { 68 | newKey := WatchActivityKey{} 69 | return newKey.Parse(key) 70 | } 71 | 72 | func (k *WatchActivityKey) SetPartitionId(newPartitionId string) { 73 | k.PartitionId = newPartitionId 74 | } 75 | 76 | func (t *WatchActivityTable) GetOrDefault(txn badgerwrap.Txn, key string) (*WatchActivity, error) { 77 | rec, err := t.Get(txn, key) 78 | if err != nil { 79 | if err != badger.ErrKeyNotFound { 80 | return nil, err 81 | } else { 82 | return &WatchActivity{}, nil 83 | } 84 | } 85 | return rec, nil 86 | } 87 | -------------------------------------------------------------------------------- /pkg/sloop/store/typed/watchactivitytablegen_test.go: -------------------------------------------------------------------------------- 1 | // This file was automatically generated by genny. 2 | // Any changes will be lost if this file is regenerated. 3 | // see https://github.com/cheekybits/genny 4 | 5 | /* 6 | * Copyright (c) 2019, salesforce.com, inc. 7 | * All rights reserved. 8 | * SPDX-License-Identifier: BSD-3-Clause 9 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 10 | */ 11 | 12 | package typed 13 | 14 | import ( 15 | "fmt" 16 | "reflect" 17 | "testing" 18 | "time" 19 | 20 | "github.com/dgraph-io/badger/v2" 21 | "github.com/salesforce/sloop/pkg/sloop/store/untyped" 22 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func helper_WatchActivity_ShouldSkip() bool { 27 | // Tests will not work on the fake types in the template, but we want to run tests on real objects 28 | if "typed.Value"+"Type" == fmt.Sprint(reflect.TypeOf(WatchActivity{})) { 29 | fmt.Printf("Skipping unit test") 30 | return true 31 | } 32 | return false 33 | } 34 | 35 | func Test_WatchActivityTable_SetWorks(t *testing.T) { 36 | if helper_WatchActivity_ShouldSkip() { 37 | return 38 | } 39 | 40 | untyped.TestHookSetPartitionDuration(time.Hour * 24) 41 | db, err := (&badgerwrap.MockFactory{}).Open(badger.DefaultOptions("")) 42 | assert.Nil(t, err) 43 | err = db.Update(func(txn badgerwrap.Txn) error { 44 | k := (&WatchActivityKey{}).GetTestKey() 45 | vt := OpenWatchActivityTable() 46 | err2 := vt.Set(txn, k, (&WatchActivityKey{}).GetTestValue()) 47 | assert.Nil(t, err2) 48 | return nil 49 | }) 50 | assert.Nil(t, err) 51 | } 52 | 53 | func helper_update_WatchActivityTable(t *testing.T, keys []string, val *WatchActivity) (badgerwrap.DB, *WatchActivityTable) { 54 | b, err := (&badgerwrap.MockFactory{}).Open(badger.DefaultOptions("")) 55 | assert.Nil(t, err) 56 | wt := OpenWatchActivityTable() 57 | err = b.Update(func(txn badgerwrap.Txn) error { 58 | var txerr error 59 | for _, key := range keys { 60 | txerr = wt.Set(txn, key, val) 61 | if txerr != nil { 62 | return txerr 63 | } 64 | } 65 | // Add some keys outside the range 66 | txerr = txn.Set([]byte("/a/123/"), []byte{}) 67 | if txerr != nil { 68 | return txerr 69 | } 70 | txerr = txn.Set([]byte("/zzz/123/"), []byte{}) 71 | if txerr != nil { 72 | return txerr 73 | } 74 | return nil 75 | }) 76 | assert.Nil(t, err) 77 | return b, wt 78 | } 79 | 80 | func Test_WatchActivityTable_GetUniquePartitionList_Success(t *testing.T) { 81 | if helper_WatchActivity_ShouldSkip() { 82 | return 83 | } 84 | 85 | db, wt := helper_update_WatchActivityTable(t, (&WatchActivityKey{}).SetTestKeys(), (&WatchActivityKey{}).SetTestValue()) 86 | var partList []string 87 | var err1 error 88 | err := db.View(func(txn badgerwrap.Txn) error { 89 | partList, err1 = wt.GetUniquePartitionList(txn) 90 | return nil 91 | }) 92 | assert.Nil(t, err) 93 | assert.Nil(t, err1) 94 | assert.Len(t, partList, 3) 95 | assert.Contains(t, partList, someMinPartition) 96 | assert.Contains(t, partList, someMiddlePartition) 97 | assert.Contains(t, partList, someMaxPartition) 98 | } 99 | 100 | func Test_WatchActivityTable_GetUniquePartitionList_EmptyPartition(t *testing.T) { 101 | if helper_WatchActivity_ShouldSkip() { 102 | return 103 | } 104 | 105 | db, wt := helper_update_WatchActivityTable(t, []string{}, &WatchActivity{}) 106 | var partList []string 107 | var err1 error 108 | err := db.View(func(txn badgerwrap.Txn) error { 109 | partList, err1 = wt.GetUniquePartitionList(txn) 110 | return err1 111 | }) 112 | assert.Nil(t, err) 113 | assert.Len(t, partList, 0) 114 | } 115 | -------------------------------------------------------------------------------- /pkg/sloop/store/typed/watchtable.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package typed 9 | 10 | import ( 11 | "fmt" 12 | "strconv" 13 | "time" 14 | 15 | "github.com/pkg/errors" 16 | "github.com/salesforce/sloop/pkg/sloop/common" 17 | ) 18 | 19 | // Key is ///// 20 | // 21 | // Partition is UnixSeconds rounded down to partition duration 22 | // Kind is kubernetes kind, starts with upper case 23 | // Namespace is kubernetes namespace, all lower 24 | // Name is kubernetes name, all lower 25 | // Timestamp is UnixNano in UTC 26 | 27 | type WatchTableKey struct { 28 | PartitionId string 29 | Kind string 30 | Namespace string 31 | Name string 32 | Timestamp time.Time 33 | } 34 | 35 | func NewWatchTableKey(partitionId string, kind string, namespace string, name string, timestamp time.Time) *WatchTableKey { 36 | return &WatchTableKey{PartitionId: partitionId, Kind: kind, Namespace: namespace, Name: name, Timestamp: timestamp} 37 | } 38 | 39 | func NewWatchTableKeyComparator(kind string, namespace string, name string, timestamp time.Time) *WatchTableKey { 40 | return &WatchTableKey{Kind: kind, Namespace: namespace, Name: name, Timestamp: timestamp} 41 | } 42 | 43 | func (*WatchTableKey) TableName() string { 44 | return "watch" 45 | } 46 | 47 | func (k *WatchTableKey) Parse(key string) error { 48 | err, parts := common.ParseKey(key) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if parts[1] != k.TableName() { 54 | return fmt.Errorf("Second part of key (%v) should be %v", key, k.TableName()) 55 | } 56 | k.PartitionId = parts[2] 57 | k.Kind = parts[3] 58 | k.Namespace = parts[4] 59 | k.Name = parts[5] 60 | tsint, err := strconv.ParseInt(parts[6], 10, 64) 61 | if err != nil { 62 | return errors.Wrapf(err, "Failed to parse timestamp from key: %v", key) 63 | } 64 | k.Timestamp = time.Unix(0, tsint).UTC() 65 | return nil 66 | } 67 | 68 | func (k *WatchTableKey) SetPartitionId(newPartitionId string) { 69 | k.PartitionId = newPartitionId 70 | } 71 | 72 | func (k *WatchTableKey) IsNameAlreadyDelimited() bool { 73 | // Currently only '.' or '/' are supported as delimiters 74 | nameLength := len(k.Name) 75 | if nameLength > 0 && (k.Name[nameLength-1:] == "." || k.Name[nameLength-1:] == "/") { 76 | return true 77 | } 78 | 79 | return false 80 | } 81 | 82 | // todo: need to make sure it can work as keyPrefix when some fields are empty 83 | func (k *WatchTableKey) String() string { 84 | if k.Name == "" && k.Timestamp.IsZero() { 85 | return fmt.Sprintf("/%v/%v/%v/%v/", k.TableName(), k.PartitionId, k.Kind, k.Namespace) 86 | } else if k.Timestamp.IsZero() { 87 | if k.IsNameAlreadyDelimited() { 88 | return fmt.Sprintf("/%v/%v/%v/%v/%v", k.TableName(), k.PartitionId, k.Kind, k.Namespace, k.Name) 89 | } else { 90 | return fmt.Sprintf("/%v/%v/%v/%v/%v/", k.TableName(), k.PartitionId, k.Kind, k.Namespace, k.Name) 91 | } 92 | } else { 93 | return fmt.Sprintf("/%v/%v/%v/%v/%v/%v", k.TableName(), k.PartitionId, k.Kind, k.Namespace, k.Name, k.Timestamp.UnixNano()) 94 | } 95 | } 96 | 97 | func (*WatchTableKey) ValidateKey(key string) error { 98 | newKey := WatchTableKey{} 99 | return newKey.Parse(key) 100 | } 101 | -------------------------------------------------------------------------------- /pkg/sloop/store/typed/watchtablegen_test.go: -------------------------------------------------------------------------------- 1 | // This file was automatically generated by genny. 2 | // Any changes will be lost if this file is regenerated. 3 | // see https://github.com/cheekybits/genny 4 | 5 | /* 6 | * Copyright (c) 2019, salesforce.com, inc. 7 | * All rights reserved. 8 | * SPDX-License-Identifier: BSD-3-Clause 9 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 10 | */ 11 | 12 | package typed 13 | 14 | import ( 15 | "fmt" 16 | "reflect" 17 | "testing" 18 | "time" 19 | 20 | badger "github.com/dgraph-io/badger/v2" 21 | "github.com/salesforce/sloop/pkg/sloop/store/untyped" 22 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 23 | "github.com/stretchr/testify/assert" 24 | ) 25 | 26 | func helper_KubeWatchResult_ShouldSkip() bool { 27 | // Tests will not work on the fake types in the template, but we want to run tests on real objects 28 | if "typed.Value"+"Type" == fmt.Sprint(reflect.TypeOf(KubeWatchResult{})) { 29 | fmt.Printf("Skipping unit test") 30 | return true 31 | } 32 | return false 33 | } 34 | 35 | func Test_KubeWatchResultTable_SetWorks(t *testing.T) { 36 | if helper_KubeWatchResult_ShouldSkip() { 37 | return 38 | } 39 | 40 | untyped.TestHookSetPartitionDuration(time.Hour * 24) 41 | db, err := (&badgerwrap.MockFactory{}).Open(badger.DefaultOptions("")) 42 | assert.Nil(t, err) 43 | err = db.Update(func(txn badgerwrap.Txn) error { 44 | k := (&WatchTableKey{}).GetTestKey() 45 | vt := OpenKubeWatchResultTable() 46 | err2 := vt.Set(txn, k, (&WatchTableKey{}).GetTestValue()) 47 | assert.Nil(t, err2) 48 | return nil 49 | }) 50 | assert.Nil(t, err) 51 | } 52 | 53 | func helper_update_KubeWatchResultTable(t *testing.T, keys []string, val *KubeWatchResult) (badgerwrap.DB, *KubeWatchResultTable) { 54 | b, err := (&badgerwrap.MockFactory{}).Open(badger.DefaultOptions("")) 55 | assert.Nil(t, err) 56 | wt := OpenKubeWatchResultTable() 57 | err = b.Update(func(txn badgerwrap.Txn) error { 58 | var txerr error 59 | for _, key := range keys { 60 | txerr = wt.Set(txn, key, val) 61 | if txerr != nil { 62 | return txerr 63 | } 64 | } 65 | // Add some keys outside the range 66 | txerr = txn.Set([]byte("/a/123/"), []byte{}) 67 | if txerr != nil { 68 | return txerr 69 | } 70 | txerr = txn.Set([]byte("/zzz/123/"), []byte{}) 71 | if txerr != nil { 72 | return txerr 73 | } 74 | return nil 75 | }) 76 | assert.Nil(t, err) 77 | return b, wt 78 | } 79 | 80 | func Test_KubeWatchResultTable_GetUniquePartitionList_Success(t *testing.T) { 81 | if helper_KubeWatchResult_ShouldSkip() { 82 | return 83 | } 84 | 85 | db, wt := helper_update_KubeWatchResultTable(t, (&WatchTableKey{}).SetTestKeys(), (&WatchTableKey{}).SetTestValue()) 86 | var partList []string 87 | var err1 error 88 | err := db.View(func(txn badgerwrap.Txn) error { 89 | partList, err1 = wt.GetUniquePartitionList(txn) 90 | return nil 91 | }) 92 | assert.Nil(t, err) 93 | assert.Nil(t, err1) 94 | assert.Len(t, partList, 3) 95 | assert.Contains(t, partList, someMinPartition) 96 | assert.Contains(t, partList, someMiddlePartition) 97 | assert.Contains(t, partList, someMaxPartition) 98 | } 99 | 100 | func Test_KubeWatchResultTable_GetUniquePartitionList_EmptyPartition(t *testing.T) { 101 | if helper_KubeWatchResult_ShouldSkip() { 102 | return 103 | } 104 | 105 | db, wt := helper_update_KubeWatchResultTable(t, []string{}, &KubeWatchResult{}) 106 | var partList []string 107 | var err1 error 108 | err := db.View(func(txn badgerwrap.Txn) error { 109 | partList, err1 = wt.GetUniquePartitionList(txn) 110 | return err1 111 | }) 112 | assert.Nil(t, err) 113 | assert.Len(t, partList, 0) 114 | } 115 | -------------------------------------------------------------------------------- /pkg/sloop/store/untyped/badgerwrap/api.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package badgerwrap 9 | 10 | import ( 11 | "io" 12 | 13 | "github.com/dgraph-io/badger/v2" 14 | ) 15 | 16 | // Need a factory we can pass into untyped store so it can open and close databases 17 | // with the proper impl 18 | type Factory interface { 19 | Open(opt badger.Options) (DB, error) 20 | } 21 | 22 | type DB interface { 23 | Close() error 24 | Sync() error 25 | Update(fn func(txn Txn) error) error 26 | View(fn func(txn Txn) error) error 27 | DropPrefix(prefix []byte) error 28 | Size() (lsm, vlog int64) 29 | Tables(withKeysCount bool) []badger.TableInfo 30 | Backup(w io.Writer, since uint64) (uint64, error) 31 | // DropAll() error 32 | Flatten(workers int) error 33 | // GetMergeOperator(key []byte, f MergeFunc, dur time.Duration) *MergeOperator 34 | // GetSequence(key []byte, bandwidth uint64) (*Sequence, error) 35 | // KeySplits(prefix []byte) []string 36 | Load(r io.Reader, maxPendingWrites int) error 37 | // MaxBatchCount() int64 38 | // MaxBatchSize() int64 39 | // NewKVLoader(maxPendingWrites int) *KVLoader 40 | // NewStream() *Stream 41 | // NewStreamAt(readTs uint64) *Stream 42 | // NewStreamWriter() *StreamWriter 43 | // NewTransaction(update bool) *Txn 44 | // NewTransactionAt(readTs uint64, update bool) *Txn 45 | // NewWriteBatch() *WriteBatch 46 | // PrintHistogram(keyPrefix []byte) 47 | RunValueLogGC(discardRatio float64) error 48 | // SetDiscardTs(ts uint64) 49 | // Subscribe(ctx context.Context, cb func(kv *KVList), prefixes ...[]byte) error 50 | // VerifyChecksum() error 51 | } 52 | 53 | type Txn interface { 54 | Get(key []byte) (Item, error) 55 | Set(key, val []byte) error 56 | Delete(key []byte) error 57 | NewIterator(opt badger.IteratorOptions) Iterator 58 | // NewKeyIterator(key []byte, opt badger.IteratorOptions) *badger.Iterator 59 | // ReadTs() uint64 60 | // SetEntry(e *badger.Entry) error 61 | // Discard() 62 | // Commit() error 63 | // CommitAt(commitTs uint64, callback func(error)) error 64 | // CommitWith(cb func(error)) 65 | } 66 | 67 | type Item interface { 68 | Key() []byte 69 | Value(fn func(val []byte) error) error 70 | ValueCopy(dst []byte) ([]byte, error) 71 | // DiscardEarlierVersions() bool 72 | EstimatedSize() int64 73 | // ExpiresAt() uint64 74 | IsDeletedOrExpired() bool 75 | KeyCopy(dst []byte) []byte 76 | // KeySize() int64 77 | // String() string 78 | // UserMeta() byte 79 | // ValueSize() int64 80 | // Version() uint64 81 | } 82 | 83 | type Iterator interface { 84 | Close() 85 | Item() Item 86 | Next() 87 | Seek(key []byte) 88 | Valid() bool 89 | ValidForPrefix(prefix []byte) bool 90 | Rewind() 91 | } 92 | -------------------------------------------------------------------------------- /pkg/sloop/store/untyped/badgerwrap/badger.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package badgerwrap 9 | 10 | import ( 11 | "io" 12 | 13 | "github.com/dgraph-io/badger/v2" 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | type BadgerFactory struct { 18 | } 19 | 20 | type BadgerDb struct { 21 | db *badger.DB 22 | } 23 | 24 | type BadgerTxn struct { 25 | txn *badger.Txn 26 | } 27 | 28 | type BadgerItem struct { 29 | item *badger.Item 30 | } 31 | 32 | type BadgerIterator struct { 33 | itr *badger.Iterator 34 | } 35 | 36 | func (f *BadgerFactory) Open(opt badger.Options) (DB, error) { 37 | db, err := badger.Open(opt) 38 | if err != nil { 39 | return nil, errors.Wrap(err, "Failed to open badger") 40 | } 41 | return &BadgerDb{db: db}, nil 42 | } 43 | 44 | // Database 45 | 46 | func (b *BadgerDb) Close() error { 47 | return b.db.Close() 48 | } 49 | 50 | func (b *BadgerDb) Sync() error { 51 | return b.db.Sync() 52 | } 53 | 54 | func (b *BadgerDb) Update(fn func(txn Txn) error) error { 55 | return b.db.Update(func(txn *badger.Txn) error { 56 | return fn(&BadgerTxn{txn: txn}) 57 | }) 58 | } 59 | 60 | func (b *BadgerDb) View(fn func(txn Txn) error) error { 61 | return b.db.View(func(txn *badger.Txn) error { 62 | return fn(&BadgerTxn{txn: txn}) 63 | }) 64 | } 65 | 66 | func (b *BadgerDb) DropPrefix(prefix []byte) error { 67 | err := b.db.DropPrefix(prefix) 68 | return err 69 | } 70 | 71 | func (b *BadgerDb) Size() (lsm, vlog int64) { 72 | return b.db.Size() 73 | } 74 | 75 | func (b *BadgerDb) Tables(withKeysCount bool) []badger.TableInfo { 76 | return b.db.Tables(withKeysCount) 77 | } 78 | 79 | func (b *BadgerDb) Backup(w io.Writer, since uint64) (uint64, error) { 80 | return b.db.Backup(w, since) 81 | } 82 | 83 | func (b *BadgerDb) Flatten(workers int) error { 84 | return b.db.Flatten(workers) 85 | } 86 | 87 | func (b *BadgerDb) Load(r io.Reader, maxPendingWrites int) error { 88 | return b.db.Load(r, maxPendingWrites) 89 | } 90 | 91 | func (b *BadgerDb) RunValueLogGC(discardRatio float64) error { 92 | return b.db.RunValueLogGC(discardRatio) 93 | } 94 | 95 | // Transaction 96 | 97 | func (t *BadgerTxn) Get(key []byte) (Item, error) { 98 | item, err := t.txn.Get(key) 99 | if err != nil { 100 | return nil, err 101 | } 102 | return &BadgerItem{item: item}, nil 103 | } 104 | 105 | func (t *BadgerTxn) Set(key, val []byte) error { 106 | return t.txn.Set(key, val) 107 | } 108 | 109 | func (t *BadgerTxn) Delete(key []byte) error { 110 | return t.txn.Delete(key) 111 | } 112 | 113 | func (t *BadgerTxn) NewIterator(opt badger.IteratorOptions) Iterator { 114 | return &BadgerIterator{itr: t.txn.NewIterator(opt)} 115 | } 116 | 117 | // Item 118 | 119 | func (i *BadgerItem) Key() []byte { 120 | return i.item.Key() 121 | } 122 | 123 | func (i *BadgerItem) Value(fn func(val []byte) error) error { 124 | return i.item.Value(fn) 125 | } 126 | 127 | func (i *BadgerItem) ValueCopy(dst []byte) ([]byte, error) { 128 | return i.item.ValueCopy(dst) 129 | } 130 | 131 | func (i *BadgerItem) KeyCopy(dst []byte) []byte { 132 | return i.item.KeyCopy(dst) 133 | } 134 | 135 | func (i *BadgerItem) EstimatedSize() int64 { 136 | return i.item.EstimatedSize() 137 | } 138 | 139 | func (i *BadgerItem) IsDeletedOrExpired() bool { 140 | return i.item.IsDeletedOrExpired() 141 | } 142 | 143 | // Iterator 144 | 145 | func (i *BadgerIterator) Close() { 146 | i.itr.Close() 147 | } 148 | 149 | func (i *BadgerIterator) Item() Item { 150 | return i.itr.Item() 151 | } 152 | 153 | func (i *BadgerIterator) Next() { 154 | i.itr.Next() 155 | } 156 | 157 | func (i *BadgerIterator) Seek(key []byte) { 158 | i.itr.Seek(key) 159 | } 160 | 161 | func (i *BadgerIterator) Valid() bool { 162 | return i.itr.Valid() 163 | } 164 | 165 | func (i *BadgerIterator) ValidForPrefix(prefix []byte) bool { 166 | return i.itr.ValidForPrefix(prefix) 167 | } 168 | 169 | func (i *BadgerIterator) Rewind() { 170 | i.itr.Rewind() 171 | } 172 | -------------------------------------------------------------------------------- /pkg/sloop/store/untyped/partition.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package untyped 9 | 10 | import ( 11 | "fmt" 12 | "strconv" 13 | "time" 14 | ) 15 | 16 | // For now we want the ability to try different durations, and this can not change during runtime 17 | // Keys need access to GetPartitionId() which needs this value, and we dont want to pass around config everywhere 18 | // that deals with keys. 19 | // TODO: Later when we figure out an ideal partition duration lets remove it from config so users dont change it 20 | // and end up with data that does not match the business logic 21 | var partitionDuration time.Duration 22 | 23 | // Partitions need to be in lexicographical sorted order, so zero pad to 12 digits 24 | func GetPartitionId(timestamp time.Time) string { 25 | if partitionDuration == time.Hour { 26 | rounded := time.Date(timestamp.Year(), timestamp.Month(), timestamp.Day(), timestamp.Hour(), 0, 0, 0, timestamp.Location()) 27 | return fmt.Sprintf("%012d", uint64(rounded.Unix())) 28 | } else if partitionDuration == 24*time.Hour { 29 | rounded := time.Date(timestamp.Year(), timestamp.Month(), timestamp.Day(), 0, 0, 0, 0, timestamp.Location()) 30 | return fmt.Sprintf("%012d", uint64(rounded.Unix())) 31 | } else { 32 | panic("Invalid partition duration") 33 | } 34 | } 35 | 36 | func GetTimeForPartition(partitionId string) (time.Time, error) { 37 | partInt, err := strconv.ParseInt(partitionId, 10, 64) 38 | if err != nil { 39 | return time.Time{}, err 40 | } 41 | 42 | partitionTime := time.Unix(partInt, 0).UTC() 43 | return partitionTime, nil 44 | } 45 | 46 | func GetTimeRangeForPartition(partitionId string) (time.Time, time.Time, error) { 47 | oldestTime, err := GetTimeForPartition(partitionId) 48 | if err != nil { 49 | return time.Time{}, time.Time{}, err 50 | } 51 | 52 | var newestTime time.Time 53 | if partitionDuration == time.Hour { 54 | newestTime = oldestTime.Add(time.Hour) 55 | } else if partitionDuration == 24*time.Hour { 56 | newestTime = oldestTime.Add(24 * time.Hour) 57 | } else { 58 | panic("Invalid partition duration") 59 | } 60 | return oldestTime, newestTime, nil 61 | } 62 | 63 | func GetAgeOfPartitionInHours(partitionId string) (float64, error) { 64 | timeForPartition, err := GetTimeForPartition(partitionId) 65 | if err != nil { 66 | return -1, err 67 | } 68 | 69 | nanosecondsInAnHour := time.Duration(60 * 60 * 1000000000) 70 | return float64(time.Now().Sub(timeForPartition) / nanosecondsInAnHour), nil 71 | } 72 | 73 | func TestHookSetPartitionDuration(partDuration time.Duration) { 74 | partitionDuration = partDuration 75 | } 76 | 77 | func GetPartitionDuration() time.Duration { 78 | return partitionDuration 79 | } 80 | -------------------------------------------------------------------------------- /pkg/sloop/store/untyped/partition_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package untyped 9 | 10 | import ( 11 | "testing" 12 | "time" 13 | 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | var someTs = time.Date(2019, 1, 2, 3, 4, 5, 6, time.UTC) 18 | var someTsRoundedHour = time.Date(2019, 1, 2, 3, 0, 0, 0, time.UTC) 19 | var someTsRoundedDay = time.Date(2019, 1, 2, 0, 0, 0, 0, time.UTC) 20 | 21 | func Test_PartitionsRoundTrip_Hour(t *testing.T) { 22 | TestHookSetPartitionDuration(time.Hour) 23 | partStr := GetPartitionId(someTs) 24 | minTs, maxTs, err := GetTimeRangeForPartition(partStr) 25 | assert.Nil(t, err) 26 | assert.Equal(t, someTsRoundedHour, minTs) 27 | assert.Equal(t, someTsRoundedHour.Add(time.Hour), maxTs) 28 | } 29 | 30 | func Test_PartitionsRoundTrip_Day(t *testing.T) { 31 | TestHookSetPartitionDuration(24 * time.Hour) 32 | partStr := GetPartitionId(someTs) 33 | minTs, maxTs, err := GetTimeRangeForPartition(partStr) 34 | assert.Nil(t, err) 35 | assert.Equal(t, someTsRoundedDay, minTs) 36 | assert.Equal(t, someTsRoundedDay.Add(24*time.Hour), maxTs) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/sloop/store/untyped/store.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package untyped 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | "time" 14 | 15 | "github.com/dgraph-io/badger/v2" 16 | "github.com/dgraph-io/badger/v2/options" 17 | "github.com/golang/glog" 18 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 19 | ) 20 | 21 | type Config struct { 22 | RootPath string 23 | ConfigPartitionDuration time.Duration 24 | BadgerMaxTableSize int64 25 | BadgerKeepL0InMemory bool 26 | BadgerVLogFileSize int64 27 | BadgerVLogMaxEntries uint 28 | BadgerUseLSMOnlyOptions bool 29 | BadgerEnableEventLogging bool 30 | BadgerNumOfCompactors int 31 | BadgerNumL0Tables int 32 | BadgerNumL0TablesStall int 33 | BadgerSyncWrites bool 34 | BadgerLevelOneSize int64 35 | BadgerLevSizeMultiplier int 36 | BadgerVLogFileIOMapping bool 37 | BadgerDetailLogEnabled bool 38 | BadgerVLogTruncate bool 39 | } 40 | 41 | func OpenStore(factory badgerwrap.Factory, config *Config) (badgerwrap.DB, error) { 42 | if config.ConfigPartitionDuration != time.Hour && config.ConfigPartitionDuration != 24*time.Hour { 43 | return nil, fmt.Errorf("Only hour and day partitionDurations are supported") 44 | } 45 | 46 | err := os.MkdirAll(config.RootPath, 0755) 47 | if err != nil { 48 | glog.Infof("mkdir failed with %v", err) 49 | } 50 | 51 | var opts badger.Options 52 | if config.BadgerUseLSMOnlyOptions { 53 | // LSMOnlyOptions uses less disk space for vlog files. See the comments on the LSMOnlyOptions() func for details 54 | opts = badger.LSMOnlyOptions(config.RootPath) 55 | } else { 56 | opts = badger.DefaultOptions(config.RootPath) 57 | } 58 | 59 | if config.BadgerEnableEventLogging { 60 | opts = opts.WithEventLogging(true) 61 | } 62 | 63 | if config.BadgerMaxTableSize != 0 { 64 | opts = opts.WithMaxTableSize(config.BadgerMaxTableSize) 65 | } 66 | opts.KeepL0InMemory = config.BadgerKeepL0InMemory 67 | if config.BadgerVLogFileSize != 0 { 68 | opts = opts.WithValueLogFileSize(config.BadgerVLogFileSize) 69 | } 70 | if config.BadgerVLogMaxEntries != 0 { 71 | opts = opts.WithValueLogMaxEntries(uint32(config.BadgerVLogMaxEntries)) 72 | } 73 | 74 | if config.BadgerNumOfCompactors != 0 { 75 | opts = opts.WithNumCompactors(config.BadgerNumOfCompactors) 76 | } 77 | 78 | if config.BadgerNumL0Tables != 0 { 79 | opts = opts.WithNumLevelZeroTables(config.BadgerNumL0Tables) 80 | } 81 | 82 | if config.BadgerNumL0TablesStall != 0 { 83 | opts = opts.WithNumLevelZeroTablesStall(config.BadgerNumL0TablesStall) 84 | } 85 | 86 | opts.WithSyncWrites(config.BadgerSyncWrites) 87 | 88 | if config.BadgerLevelOneSize != 0 { 89 | opts = opts.WithLevelOneSize(config.BadgerLevelOneSize) 90 | } 91 | 92 | if config.BadgerLevSizeMultiplier != 0 { 93 | opts = opts.WithLevelSizeMultiplier(config.BadgerLevSizeMultiplier) 94 | } 95 | 96 | if config.BadgerVLogFileIOMapping { 97 | opts = opts.WithValueLogLoadingMode(options.FileIO) 98 | } 99 | 100 | if !config.BadgerDetailLogEnabled { 101 | opts = opts.WithLogger(nil) 102 | } 103 | 104 | opts = opts.WithTruncate(config.BadgerVLogTruncate) 105 | 106 | opts = opts.WithSyncWrites(config.BadgerSyncWrites) 107 | 108 | db, err := factory.Open(opts) 109 | if err != nil { 110 | return nil, fmt.Errorf("badger.OpenStore failed with: %v", err) 111 | } 112 | 113 | db.Flatten(5) 114 | glog.Infof("BadgerDB Options: %+v", opts) 115 | 116 | partitionDuration = config.ConfigPartitionDuration 117 | return db, nil 118 | } 119 | 120 | func CloseStore(db badgerwrap.DB) error { 121 | glog.Infof("Closing store") 122 | err := db.Close() 123 | glog.Infof("Finished closing store") 124 | return err 125 | } 126 | -------------------------------------------------------------------------------- /pkg/sloop/storemanager/sleepwithcancel.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package storemanager 9 | 10 | import ( 11 | "time" 12 | ) 13 | 14 | // This provides a way to Sleep with the ability to get woken up for a cancel 15 | // Once cancel is called all future sleeps will return immediately 16 | 17 | type SleepWithCancel struct { 18 | cancel chan bool 19 | } 20 | 21 | func NewSleepWithCancel() *SleepWithCancel { 22 | return &SleepWithCancel{cancel: make(chan bool, 10)} 23 | } 24 | 25 | func (s *SleepWithCancel) Sleep(after time.Duration) { 26 | select { 27 | case <-s.cancel: 28 | break 29 | case <-time.After(after): 30 | break 31 | } 32 | } 33 | 34 | func (s *SleepWithCancel) Cancel() { 35 | s.cancel <- true 36 | close(s.cancel) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/sloop/storemanager/sleepwithcancel_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package storemanager 9 | 10 | import ( 11 | "testing" 12 | "time" 13 | 14 | "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | func Test_SleepWithCancel_TestThatSleepsAfterCancelDontCrash(t *testing.T) { 18 | before := time.Now() 19 | s := NewSleepWithCancel() 20 | s.Cancel() 21 | s.Sleep(time.Minute) 22 | s.Sleep(time.Minute) 23 | s.Sleep(time.Hour) 24 | assert.True(t, time.Since(before).Seconds() < 100) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/sloop/storemanager/stats_test.go: -------------------------------------------------------------------------------- 1 | package storemanager 2 | 3 | import ( 4 | "path" 5 | "testing" 6 | 7 | "github.com/spf13/afero" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func Test_GetDirSizeRecursive(t *testing.T) { 12 | fs := afero.Afero{Fs: afero.NewMemMapFs()} 13 | fs.MkdirAll(someDir, 0700) 14 | // 3 vlog files 15 | fs.WriteFile(path.Join(someDir, "000010.vlog"), []byte("a"), 0700) 16 | fs.WriteFile(path.Join(someDir, "000011.vlog"), []byte("aa"), 0700) 17 | fs.WriteFile(path.Join(someDir, "000012.vlog"), []byte("aaaaa"), 0700) 18 | // 4 sst files 19 | fs.WriteFile(path.Join(someDir, "000070.sst"), []byte("zzzzzz"), 0700) 20 | fs.WriteFile(path.Join(someDir, "000071.sst"), []byte("zzzzzzz"), 0700) 21 | fs.WriteFile(path.Join(someDir, "000072.sst"), []byte("zzzzzzzz"), 0700) 22 | fs.WriteFile(path.Join(someDir, "000073.sst"), []byte("zzzzzzzzz"), 0700) 23 | // Other 24 | fs.WriteFile(path.Join(someDir, "KEYREGISTRY"), []byte("u"), 0700) 25 | fs.WriteFile(path.Join(someDir, "MANIFEST"), []byte("u"), 0700) 26 | 27 | subDir := path.Join(someDir, "subDir") 28 | fs.Mkdir(subDir, 0700) 29 | fs.WriteFile(path.Join(subDir, "randomFile"), []byte("abc"), 0700) 30 | 31 | fileSize, extFileCount, extByteCount, err := getDirSizeRecursive(someDir, &fs) 32 | assert.Nil(t, err) 33 | assert.Equal(t, uint64(43), fileSize) 34 | assert.Equal(t, map[string]int(map[string]int{"": 3, ".sst": 4, ".vlog": 3}), extFileCount) 35 | assert.Equal(t, map[string]uint64(map[string]uint64{"": 5, ".sst": 30, ".vlog": 8}), extByteCount) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/sloop/test/assertex/jsonequal.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package assertex 9 | 10 | import ( 11 | "fmt" 12 | "testing" 13 | 14 | "github.com/nsf/jsondiff" 15 | ) 16 | 17 | // This is similar functionality to assert.JSONEq() but is more useful for a couple reasons 18 | // 1) It prints the full actual string without transformed line-breaks, so its easy to copy the output back into source 19 | // code if desired 20 | // 2) JSONEq prints a diff of the json strings, where this shows a combined diff 21 | // 22 | // Expected Payload: 23 | // { 24 | // "foo": { "bar": 5 }, 25 | // "abc": [2,3] 26 | // } 27 | // 28 | // Actual Payload 29 | // { 30 | // "foo": { "bar": 1 }, 31 | // "abc": [2,3] 32 | // } 33 | // 34 | // assert.JSONEq() will give you: 35 | 36 | /* 37 | Error Trace: 38 | Error: Not equal: map[string]interface {}{"foo":map[string]interface {}{"bar":5}, "abc":[]interface {}{2, 3}} (expected) 39 | != map[string]interface {}{"foo":map[string]interface {}{"bar":1}, "abc":[]interface {}{2, 3}} (actual) 40 | 41 | Diff: 42 | --- Expected 43 | +++ Actual 44 | @@ -6,3 +6,3 @@ 45 | (string) (len=3) "foo": (map[string]interface {}) (len=1) { 46 | - (string) (len=3) "bar": (float64) 5 47 | + (string) (len=3) "bar": (float64) 1 48 | } 49 | */ 50 | 51 | // This helper will give you: 52 | 53 | /* 54 | Diff:NoMatch 55 | ## EXPECTED: 56 | { 57 | "foo": { "bar": 5 }, 58 | "abc": [2,3] 59 | } 60 | ## ACTUAL: 61 | { 62 | "foo": { "bar": 1 }, 63 | "abc": [2,3] 64 | } 65 | ## DIFF: 66 | { 67 | "abc": [ 68 | 2, 69 | 3 70 | ], 71 | "foo": { 72 | "bar": 5 => 1 73 | } 74 | */ 75 | 76 | func JsonEqualBytes(t *testing.T, expectedByte []byte, actualByte []byte) { 77 | diff, diffString := jsondiff.Compare(expectedByte, actualByte, &jsondiff.Options{}) 78 | if diff != jsondiff.FullMatch { 79 | fmt.Printf("Diff:%v\n", diff.String()) 80 | fmt.Printf("## EXPECTED:\n%v\n", string(expectedByte)) 81 | fmt.Printf("## ACTUAL:\n%v\n", string(actualByte)) 82 | fmt.Printf("## DIFF:\n%v", diffString) 83 | t.Fail() 84 | } 85 | } 86 | 87 | func JsonEqual(t *testing.T, expectedStr string, actualStr string) { 88 | JsonEqualBytes(t, []byte(expectedStr), []byte(actualStr)) 89 | } 90 | -------------------------------------------------------------------------------- /pkg/sloop/test/assertex/protoequal.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package assertex 9 | 10 | import ( 11 | "fmt" 12 | "testing" 13 | 14 | "github.com/golang/protobuf/proto" 15 | ) 16 | 17 | func areProtoEqual(expected interface{}, actual interface{}) bool { 18 | expectedProto, ok := expected.(proto.Message) 19 | if ok { 20 | actualProto, ok := actual.(proto.Message) 21 | if ok { 22 | return proto.Equal(expectedProto, actualProto) 23 | } 24 | } 25 | return false 26 | } 27 | 28 | func ProtoEqual(t *testing.T, expected interface{}, actual interface{}) { 29 | if !areProtoEqual(expected, actual) { 30 | fmt.Printf("## EXPECTED:\n%v\n", expected) 31 | fmt.Printf("## ACTUAL:\n%v\n", actual) 32 | t.Fail() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pkg/sloop/webserver/indexhandler.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package webserver 9 | 10 | import ( 11 | "net/http" 12 | ) 13 | 14 | type indexData struct { 15 | DefaultLookback string 16 | DefaultNamespace string 17 | DefaultKind string 18 | LeftBarLinks []ComputedLink 19 | CurrentContext string 20 | } 21 | 22 | func indexHandler(config WebConfig) http.HandlerFunc { 23 | return func(writer http.ResponseWriter, request *http.Request) { 24 | indexTemplate, err := getTemplate(indexTemplateFile, _webfilesIndexHtml) 25 | if err != nil { 26 | logWebError(err, "Template.New failed", request, writer) 27 | return 28 | } 29 | data := indexData{} 30 | data.DefaultLookback = config.DefaultLookback 31 | data.DefaultNamespace = config.DefaultNamespace 32 | data.DefaultKind = config.DefaultResources 33 | data.CurrentContext = config.CurrentContext 34 | data.LeftBarLinks, err = makeLeftBarLinks(config.LeftBarLinks) 35 | if err != nil { 36 | logWebError(err, "Could not make left bar links", request, writer) 37 | return 38 | } 39 | 40 | err = indexTemplate.Execute(writer, data) 41 | if err != nil { 42 | logWebError(err, "Template.ExecuteTemplate failed", request, writer) 43 | return 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pkg/sloop/webserver/links.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package webserver 9 | 10 | type LinkTemplate struct { 11 | Text string `json:"text"` 12 | UrlTemplate string `json:"urlTemplate"` 13 | } 14 | 15 | type ResourceLinkTemplate struct { 16 | Text string `json:"text""` 17 | UrlTemplate string `json:"urlTemplate"` 18 | Kinds []string `json:"kinds"` 19 | } 20 | 21 | type ComputedLink struct { 22 | Text string 23 | Url string 24 | } 25 | 26 | func makeResourceLinks(namespace string, name string, kind string, resLinks []ResourceLinkTemplate) ([]ComputedLink, error) { 27 | var ret []ComputedLink 28 | 29 | for _, thisLink := range resLinks { 30 | if thisLink.Text == "" { 31 | continue 32 | } 33 | data := struct { 34 | Name string 35 | Namespace string 36 | Kind string 37 | }{ 38 | Name: name, 39 | Namespace: namespace, 40 | Kind: kind, 41 | } 42 | linkUrl, err := runTextTemplate(thisLink.UrlTemplate, data) 43 | if err != nil { 44 | return []ComputedLink{}, err 45 | } 46 | kindMatch := false 47 | if len(thisLink.Kinds) == 0 { 48 | kindMatch = true 49 | } else { 50 | for _, linkKind := range thisLink.Kinds { 51 | if linkKind == kind { 52 | kindMatch = true 53 | } 54 | } 55 | } 56 | 57 | if kindMatch { 58 | ret = append(ret, ComputedLink{Text: thisLink.Text, Url: linkUrl}) 59 | } 60 | } 61 | return ret, nil 62 | } 63 | 64 | func makeLeftBarLinks(lbLinks []LinkTemplate) ([]ComputedLink, error) { 65 | var ret []ComputedLink 66 | 67 | for _, thisLink := range lbLinks { 68 | if thisLink.Text == "" { 69 | continue 70 | } 71 | // TODO: Add cluster 72 | data := struct{}{} 73 | linkUrl, err := runTextTemplate(thisLink.UrlTemplate, data) 74 | if err != nil { 75 | return []ComputedLink{}, err 76 | } 77 | 78 | ret = append(ret, ComputedLink{Text: thisLink.Text, Url: linkUrl}) 79 | } 80 | return ret, nil 81 | } 82 | -------------------------------------------------------------------------------- /pkg/sloop/webserver/links_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package webserver 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func Test_makeExternalLinks_SimpleCaseOneUrl(t *testing.T) { 17 | linkTemplates := []ResourceLinkTemplate{ 18 | { 19 | Text: "ThisName1", 20 | UrlTemplate: "http://somehost/{{.Namespace}}/{{.Name}}/{{.Kind}}", 21 | Kinds: []string{"someKind"}, 22 | }, 23 | { 24 | Text: "ThisName2", 25 | UrlTemplate: "doesNotMatter", 26 | Kinds: []string{"someOther", "abc"}, 27 | }, 28 | { 29 | Text: "ThisName3", 30 | UrlTemplate: "http://urlforallkinds/", 31 | Kinds: []string{}, 32 | }, 33 | } 34 | links, err := makeResourceLinks("someNamespace", "someName", "someKind", linkTemplates) 35 | assert.Nil(t, err) 36 | assert.Equal(t, 2, len(links)) 37 | assert.Equal(t, "ThisName1", links[0].Text) 38 | assert.Equal(t, "http://somehost/someNamespace/someName/someKind", links[0].Url) 39 | assert.Equal(t, "ThisName3", links[1].Text) 40 | assert.Equal(t, "http://urlforallkinds/", links[1].Url) 41 | } 42 | 43 | func Test_makeExternalLinks_UpperAndLowerWork(t *testing.T) { 44 | linkTemplates := []ResourceLinkTemplate{ 45 | {Text: "ThisName", 46 | UrlTemplate: "http://somehost/{{.Namespace | ToUpper}}/{{.Name | ToLower}}"}, 47 | } 48 | links, err := makeResourceLinks("someNamespace", "someName", "someKind", linkTemplates) 49 | assert.Nil(t, err) 50 | assert.Equal(t, 1, len(links)) 51 | assert.Equal(t, "ThisName", links[0].Text) 52 | assert.Equal(t, "http://somehost/SOMENAMESPACE/somename", links[0].Url) 53 | } 54 | 55 | func Test_makeExternalLinks_EmptyReturnsNothing(t *testing.T) { 56 | linkTemplates := []ResourceLinkTemplate{ 57 | {Text: "", 58 | UrlTemplate: ""}, 59 | } 60 | links, err := makeResourceLinks("someNamespace", "someName", "someKind", linkTemplates) 61 | assert.Nil(t, err) 62 | assert.Equal(t, 0, len(links)) 63 | } 64 | 65 | func Test_makeLeftBarLinks_JustPassthrough(t *testing.T) { 66 | lbLinks := []LinkTemplate{ 67 | {Text: "foo", UrlTemplate: "http://some-url.com"}, 68 | } 69 | clinks, err := makeLeftBarLinks(lbLinks) 70 | assert.Nil(t, err) 71 | assert.Equal(t, 1, len(clinks)) 72 | assert.Equal(t, "foo", clinks[0].Text) 73 | assert.Equal(t, "http://some-url.com", clinks[0].Url) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/sloop/webserver/middleware.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package webserver 9 | 10 | import ( 11 | "context" 12 | "fmt" 13 | "net/http" 14 | "time" 15 | 16 | "github.com/golang/glog" 17 | "github.com/prometheus/client_golang/prometheus" 18 | "github.com/prometheus/client_golang/prometheus/promauto" 19 | "github.com/prometheus/client_golang/prometheus/promhttp" 20 | ) 21 | 22 | const requestIDKey string = "reqId" 23 | 24 | var ( 25 | metricWebServerRequestCount = promauto.NewCounterVec( 26 | prometheus.CounterOpts{ 27 | Name: "sloop_http_requests_total", 28 | Help: "A counter for requests to the wrapped handler.", 29 | }, 30 | []string{"code", "handler"}, 31 | ) 32 | metricWebServerRequestDuration = promauto.NewHistogramVec( 33 | prometheus.HistogramOpts{ 34 | Name: "sloop_http_request_duration_seconds", 35 | Help: "A histogram of latencies for requests to the wrapped handler.", 36 | Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 30, 60}, 37 | }, 38 | []string{"handler"}, 39 | ) 40 | ) 41 | 42 | func getRequestId(webContext context.Context) string { 43 | requestID, ok := webContext.Value(requestIDKey).(string) 44 | if !ok { 45 | requestID = "unknown" 46 | } 47 | return requestID 48 | } 49 | 50 | // Sets a request id in the context which can be used for logging 51 | func traceMiddleware(next http.Handler) http.Handler { 52 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 53 | requestID := r.Header.Get("X-Request-Id") 54 | if requestID == "" { 55 | requestID = fmt.Sprintf("%d", time.Now().UnixNano()/1000) 56 | } 57 | ctx := context.WithValue(r.Context(), requestIDKey, requestID) 58 | w.Header().Set("X-Request-Id", requestID) 59 | next.ServeHTTP(w, r.WithContext(ctx)) 60 | }) 61 | } 62 | 63 | // Logs all HTTP requests to glog 64 | func glogMiddleware(next http.Handler) http.Handler { 65 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 66 | before := time.Now() 67 | next.ServeHTTP(w, r) 68 | requestID := getRequestId(r.Context()) 69 | timeTaken := time.Since(before) 70 | glog.Infof("reqId: %v http url: %v took: %v remote: %v useragent: %v", requestID, r.URL, timeTaken, r.RemoteAddr, r.UserAgent()) 71 | }) 72 | } 73 | 74 | func middlewareChain(handlerName string, next http.Handler) http.HandlerFunc { 75 | return promhttp.InstrumentHandlerCounter( 76 | metricWebServerRequestCount.MustCurryWith(prometheus.Labels{"handler": handlerName}), 77 | promhttp.InstrumentHandlerDuration( 78 | metricWebServerRequestDuration.MustCurryWith(prometheus.Labels{"handler": handlerName}), 79 | traceMiddleware( 80 | glogMiddleware( 81 | next, 82 | ), 83 | ), 84 | ), 85 | ) 86 | } 87 | 88 | func metricCountMiddleware(handlerName string, next http.Handler) http.HandlerFunc { 89 | return promhttp.InstrumentHandlerCounter( 90 | metricWebServerRequestCount.MustCurryWith(prometheus.Labels{"handler": handlerName}), next) 91 | } 92 | -------------------------------------------------------------------------------- /pkg/sloop/webserver/params.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package webserver 9 | 10 | import ( 11 | "fmt" 12 | "net/http" 13 | "regexp" 14 | "strconv" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | func timeFromUnixTimeParam(request *http.Request, paramName string, defaultTs time.Time, unit time.Duration) (time.Time, error) { 20 | unixStr := request.URL.Query().Get(paramName) 21 | if unixStr == "" { 22 | return defaultTs, nil 23 | } 24 | startDateInt64, err := strconv.ParseInt(unixStr, 10, 64) 25 | if err != nil { 26 | return defaultTs, err 27 | } 28 | if unit == time.Second { 29 | return time.Unix(startDateInt64, 0).UTC(), nil 30 | } else if unit == time.Millisecond { 31 | return time.Unix(startDateInt64/1000, 1000*1000*(startDateInt64%1000)).UTC(), nil 32 | } else { 33 | return time.Time{}, fmt.Errorf("Invalid unit. Only second and millisecond are supported") 34 | } 35 | } 36 | 37 | func durationFromParam(request *http.Request, paramName string, defaultDuration time.Duration) (time.Duration, error) { 38 | durStr := request.URL.Query().Get(paramName) 39 | if durStr == "" { 40 | return defaultDuration, nil 41 | } 42 | dur, err := time.ParseDuration(durStr) 43 | if err != nil { 44 | return defaultDuration, err 45 | } 46 | return dur, nil 47 | } 48 | 49 | // Need to allow: 50 | // 1) Kubernetes names: 253 chars, lower a-z, '-' and '.' 51 | // 2) All const "_all" 52 | // 3) Resource names that start with upper "Pod" 53 | func cleanStringFromParam(request *http.Request, paramName string, defaultStr string) string { 54 | strVal := request.URL.Query().Get(paramName) 55 | if strVal == "" { 56 | return defaultStr 57 | } 58 | reg := regexp.MustCompile("[^a-zA-Z0-9\\-_.]+") 59 | clean := reg.ReplaceAllString(strVal, "") 60 | clean = strings.ReplaceAll(clean, "..", "") 61 | return clean 62 | } 63 | 64 | func numberFromParam(request *http.Request, paramName string, defaultNum int) int { 65 | numStr := request.URL.Query().Get(paramName) 66 | if numStr == "" { 67 | return defaultNum 68 | } 69 | numVal, err := strconv.ParseInt(numStr, 10, 64) 70 | if err != nil { 71 | return defaultNum 72 | } 73 | return int(numVal) 74 | } 75 | -------------------------------------------------------------------------------- /pkg/sloop/webserver/params_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package webserver 9 | 10 | import ( 11 | "fmt" 12 | "net/http" 13 | "net/url" 14 | "testing" 15 | "time" 16 | 17 | "github.com/stretchr/testify/assert" 18 | ) 19 | 20 | const ( 21 | someParam = "someparam" 22 | ) 23 | 24 | var ( 25 | someDefaultTime = time.Date(2019, 11, 24, 6, 5, 4, 3*1000*1000, time.UTC) 26 | someOtherTime = someDefaultTime.Add(time.Minute) 27 | simeOtherTimeRoundSec = someOtherTime.Round(time.Second) 28 | simeOtherTimeRoundMs = someOtherTime.Round(time.Millisecond) 29 | someDefaultDuration = time.Minute 30 | someOtherDuration = time.Hour 31 | someDefaultString = "abc" 32 | someOtherString = "def" 33 | someDirtyString = "abcABC019-_. !@#$%^&*()+" 34 | dirtyStringCleaned = "abcABC019-_." 35 | ) 36 | 37 | func Test_timeFromParam_EmptyStrReturnDefault(t *testing.T) { 38 | request := &http.Request{} 39 | request.URL = &url.URL{} 40 | 41 | ret, err := timeFromUnixTimeParam(request, someParam, someDefaultTime, time.Millisecond) 42 | assert.Nil(t, err) 43 | assert.Equal(t, ret, someDefaultTime) 44 | } 45 | 46 | func Test_timeFromParam_InvalidStringReturnsError(t *testing.T) { 47 | request := &http.Request{} 48 | request.URL, _ = url.Parse(fmt.Sprintf("http://localhost/a?%v=%v", someParam, "thisIsNotANumber")) 49 | _, err := timeFromUnixTimeParam(request, someParam, someDefaultTime, time.Millisecond) 50 | assert.NotNil(t, err) 51 | } 52 | 53 | func Test_timeFromParam_UnixTimeWorksMillisecond(t *testing.T) { 54 | request := &http.Request{} 55 | request.URL, _ = url.Parse(fmt.Sprintf("http://localhost/a?%v=%v", someParam, someOtherTime.Unix()*1000+3)) 56 | ret, err := timeFromUnixTimeParam(request, someParam, time.Time{}, time.Millisecond) 57 | assert.Nil(t, err) 58 | assert.Equal(t, simeOtherTimeRoundMs, ret) 59 | } 60 | 61 | func Test_timeFromParam_UnixTimeWorksSecond(t *testing.T) { 62 | request := &http.Request{} 63 | request.URL, _ = url.Parse(fmt.Sprintf("http://localhost/a?%v=%v", someParam, someOtherTime.Unix())) 64 | ret, err := timeFromUnixTimeParam(request, someParam, time.Time{}, time.Second) 65 | assert.Nil(t, err) 66 | assert.Equal(t, simeOtherTimeRoundSec, ret) 67 | } 68 | 69 | func Test_durationFromParam_EmptyReturnsDefault(t *testing.T) { 70 | request := &http.Request{} 71 | request.URL = &url.URL{} 72 | 73 | ret, err := durationFromParam(request, someParam, someDefaultDuration) 74 | assert.Nil(t, err) 75 | assert.Equal(t, ret, someDefaultDuration) 76 | } 77 | 78 | func Test_durationFromParam_InvalidDuration_ReturnsError(t *testing.T) { 79 | request := &http.Request{} 80 | request.URL, _ = url.Parse(fmt.Sprintf("http://localhost/a?%v=%v", someParam, "NotADuration")) 81 | _, err := durationFromParam(request, someParam, someDefaultDuration) 82 | assert.NotNil(t, err) 83 | } 84 | 85 | func Test_durationFromParam_GAoodDuration_ReturnsError(t *testing.T) { 86 | request := &http.Request{} 87 | request.URL, _ = url.Parse(fmt.Sprintf("http://localhost/a?%v=%v", someParam, someOtherDuration)) 88 | ret, err := durationFromParam(request, someParam, someDefaultDuration) 89 | assert.Nil(t, err) 90 | assert.Equal(t, someOtherDuration, ret) 91 | } 92 | 93 | func Test_cleanStringFromParam_Empty_ReturnsDefault(t *testing.T) { 94 | request := &http.Request{} 95 | request.URL = &url.URL{} 96 | 97 | ret := cleanStringFromParam(request, someParam, someDefaultString) 98 | assert.Equal(t, someDefaultString, ret) 99 | } 100 | 101 | func Test_cleanStringFromParam_GoodIn_GoodOut(t *testing.T) { 102 | request := &http.Request{} 103 | 104 | request.URL, _ = url.Parse(fmt.Sprintf("http://localhost/a?%v=%v", someParam, someOtherString)) 105 | ret := cleanStringFromParam(request, someParam, someDefaultString) 106 | assert.Equal(t, someOtherString, ret) 107 | } 108 | 109 | func Test_cleanStringFromParam_DirtyIn_CleanOut(t *testing.T) { 110 | request := &http.Request{} 111 | 112 | request.URL, _ = url.Parse(fmt.Sprintf("http://localhost/a?%v=%v", someParam, url.QueryEscape(someDirtyString))) 113 | ret := cleanStringFromParam(request, someParam, someDefaultString) 114 | assert.Equal(t, dirtyStringCleaned, ret) 115 | } 116 | 117 | func Test_numberFromParam_GoodCase(t *testing.T) { 118 | request := &http.Request{} 119 | request.URL, _ = url.Parse(fmt.Sprintf("http://localhost/a?%v=%v", someParam, "123")) 120 | ret := numberFromParam(request, someParam, 456) 121 | assert.Equal(t, 123, ret) 122 | } 123 | 124 | func Test_numberFromParam_ErrorReturnsDefault(t *testing.T) { 125 | request := &http.Request{} 126 | request.URL, _ = url.Parse(fmt.Sprintf("http://localhost/a?%v=%v", someParam, "abc")) 127 | ret := numberFromParam(request, someParam, 456) 128 | assert.Equal(t, 456, ret) 129 | } 130 | 131 | func Test_numberFromParam_EmptyReturnsDefault(t *testing.T) { 132 | request := &http.Request{} 133 | request.URL, _ = url.Parse(fmt.Sprintf("http://localhost/")) 134 | ret := numberFromParam(request, someParam, 456) 135 | assert.Equal(t, 456, ret) 136 | } 137 | -------------------------------------------------------------------------------- /pkg/sloop/webserver/resourcehandler.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | package webserver 9 | 10 | import ( 11 | "bytes" 12 | "fmt" 13 | "html/template" 14 | "net/http" 15 | "path" 16 | "strings" 17 | "time" 18 | 19 | "github.com/salesforce/sloop/pkg/sloop/queries" 20 | ) 21 | 22 | type externalLink struct { 23 | Text string 24 | Url string 25 | } 26 | 27 | type resourceData struct { 28 | Namespace string 29 | Name string 30 | Kind string 31 | Uuid string 32 | ClickTime time.Time 33 | SelfUrl string 34 | Links []ComputedLink 35 | EventsUrl string 36 | PayloadUrl string 37 | PlusMinusTime time.Duration 38 | } 39 | 40 | func runTextTemplate(templateStr string, data interface{}) (string, error) { 41 | funcMap := template.FuncMap{ 42 | "ToUpper": strings.ToUpper, 43 | "ToLower": strings.ToLower, 44 | } 45 | tmpl, err := template.New("").Funcs(funcMap).Parse(templateStr) 46 | if err != nil { 47 | return "", err 48 | } 49 | var tpl bytes.Buffer 50 | err = tmpl.Execute(&tpl, data) 51 | if err != nil { 52 | return "", err 53 | } 54 | return tpl.String(), nil 55 | } 56 | 57 | func resourceHandler(resLinks []ResourceLinkTemplate, currentContext string) http.HandlerFunc { 58 | return func(writer http.ResponseWriter, request *http.Request) { 59 | resourceTemplate, err := getTemplate(resourceTemplateFile, _webfilesResourceHtml) 60 | if err != nil { 61 | logWebError(err, "Template.New failed", request, writer) 62 | return 63 | } 64 | d := resourceData{} 65 | d.Namespace = cleanStringFromParam(request, queries.NamespaceParam, "") 66 | d.Name = cleanStringFromParam(request, queries.NameParam, "") 67 | d.Kind = cleanStringFromParam(request, queries.KindParam, "") 68 | d.Uuid = cleanStringFromParam(request, queries.UuidParam, "") 69 | d.ClickTime, err = timeFromUnixTimeParam(request, queries.ClickTimeParam, time.Time{}, time.Millisecond) 70 | if err != nil || d.ClickTime == (time.Time{}) { 71 | logWebError(err, "Invalid click time", request, writer) 72 | return 73 | } 74 | // TODO: Make this selectable in the UX 75 | d.PlusMinusTime = time.Minute * 15 76 | 77 | d.SelfUrl = request.URL.String() 78 | d.Links, err = makeResourceLinks(d.Namespace, d.Name, d.Kind, resLinks) 79 | if err != nil { 80 | logWebError(err, "Error creating external links", request, writer) 81 | return 82 | } 83 | 84 | queryStart := d.ClickTime.Add(-1 * d.PlusMinusTime).Unix() 85 | queryEnd := d.ClickTime.Add(d.PlusMinusTime).Unix() 86 | 87 | dataParams := fmt.Sprintf("?query=%v&namespace=%v&start_time=%v&end_time=%v&kind=%v&name=%v", "GetEventData", d.Namespace, queryStart, queryEnd, d.Kind, d.Name) 88 | d.EventsUrl = path.Join("/", currentContext, "data"+dataParams) 89 | 90 | dataParams = fmt.Sprintf("?query=%v&namespace=%v&start_time=%v&end_time=%v&kind=%v&name=%v", "GetResPayload", d.Namespace, queryStart, queryEnd, d.Kind, d.Name) 91 | d.PayloadUrl = path.Join("/", currentContext, "data"+dataParams) 92 | 93 | err = resourceTemplate.Execute(writer, d) 94 | if err != nil { 95 | logWebError(err, "Template.ExecuteTemplate failed", request, writer) 96 | return 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /pkg/sloop/webserver/webfiles.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | 7 | _ "github.com/jteeuwen/go-bindata" 8 | "github.com/salesforce/sloop/pkg/sloop/common" 9 | "github.com/spf13/afero" 10 | ) 11 | 12 | const ( 13 | prefix = "webfiles/" 14 | errorString = "Webfile %v does not exist in local directory or in binary form." 15 | ) 16 | 17 | // go-bindata -o bindata.go webfiles 18 | // ReadWebfile is a function which finds the webfiles that have been predefined and converted to binary format. 19 | // sample input : fileName="index.html" 20 | func readWebfile(fileName string, fs *afero.Afero) ([]byte, error) { 21 | //file exists as a physical file 22 | data, err := fs.ReadFile(common.GetFilePath(webFilesPath, fileName)) 23 | if err != nil { 24 | //file exists in binary 25 | binFileList := AssetNames() 26 | binFileName := common.GetFilePath(prefix, fileName) 27 | if common.Contains(binFileList, binFileName) { 28 | return Asset(binFileName) 29 | } 30 | return nil, fmt.Errorf(errorString, fileName) 31 | } 32 | return data, err 33 | } 34 | 35 | // Example input: 36 | // templateName = index.html 37 | // Get Template function creates a new template of the webfile passed as a string after first reading the file by 38 | // calling ReadWebfile (). 39 | func getTemplate(templateName string, _ []byte) (*template.Template, error) { 40 | fs := afero.Afero{Fs: afero.NewOsFs()} 41 | data, err := readWebfile(templateName, &fs) 42 | if err != nil { 43 | return nil, err 44 | } 45 | newTemplate := template.New(templateName) 46 | newTemplate, err = newTemplate.Parse(string(data)) 47 | if err != nil { 48 | return nil, err 49 | } 50 | return newTemplate, nil 51 | } 52 | -------------------------------------------------------------------------------- /pkg/sloop/webserver/webfiles/debug.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 12 | 13 | Sloop Debug Menu 14 | 15 | 16 | 17 | [ Home ] 18 | 19 |

Sloop Debug Menu

20 | 21 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /pkg/sloop/webserver/webfiles/debug.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | // Sets the Home link reference for all the debug pages 9 | function loadHomeRef() { 10 | var homeRef = "/" + window.location.pathname.split('/')[1]; 11 | document.getElementById("homeLink").setAttribute("href", homeRef); 12 | return; 13 | } 14 | -------------------------------------------------------------------------------- /pkg/sloop/webserver/webfiles/debugconfig.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 12 | 13 | Sloop Debug Config 14 | 15 | 16 | 17 | [ Home ][ Debug Menu ]
18 | 19 |

Current Config

20 | 21 |
22 |
{{.}}
23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /pkg/sloop/webserver/webfiles/debughistogram.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 12 | 13 | Sloop Keys Histogram 14 | 15 | 16 | 17 | [ Home ][ Debug Menu ]
18 | 19 |

Sloop Keys Histogram

20 | 21 |
22 |
23 | 24 |

25 |

26 |

27 |


28 | 29 |
30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
Total keys{{.TotalKeys}}
Total Sloop Keys
{{.TotalSloopKeys}}
Total Estimated Size
{{.TotalEstimatedSize}}
Deleted Keys
{{.DeletedKeys}}
Total Internal Keys
{{.TotalInternalKeys}}
Total Internal Keys Size
{{.TotalInternalKeysSize}}
Total Internal Head Keys
{{.TotalHeadKeys}}
Total Internal Move Keys
{{.TotalMoveKeys}}
Total Internal Discard Keys
{{.TotalDiscardKeys}}
44 | 45 |
46 | 47 | Partitions List:
48 |
49 | 50 | 51 | {{range $key, $value := .HistogramMap}} 52 | 53 | {{end}} 54 |
TablePartition IDNumber of KeysEstimated SizeMinimum SizeMaximum SizeAverage Size
{{$key.TableName}}{{$key.PartitionID}}{{$value.TotalKeys}}{{$value.TotalSize}}{{$value.MinimumSize}}{{$value.MaximumSize}}{{$value.AverageSize}}
55 | 56 | 57 | 58 | 60 | 61 | -------------------------------------------------------------------------------- /pkg/sloop/webserver/webfiles/debuglistkeys.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 12 | 13 | 14 | Sloop Debug Query Badger 15 | 16 | 17 | 18 | [ Home ][ Debug Menu ]
19 | 20 |

Query Sloop's Badger DB Directly

21 | 22 |
23 |
24 |
25 |

33 | 34 | 35 | 36 | 37 |

38 | 39 |
40 |
41 |

42 |
43 | 44 | 59 | 60 |
61 |

62 | 63 | 64 |
65 |
66 |

67 | 68 | 69 | 70 | 71 | 72 |
Total Keys Matched{{.KeysMatched}}
Total Size of Matched Keys
{{.TotalSize}}
Total Keys Searched
{{.TotalKeys}}
73 |


74 | Key List:
75 |
    76 | {{range $key, $value := .Keys}} 77 |
  1. {{.}} 78 | {{end}} 79 |
80 | 81 | 82 | 83 | 91 | 92 | -------------------------------------------------------------------------------- /pkg/sloop/webserver/webfiles/debugtables.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 12 | 13 | Sloop Debug Badger Tables 14 | 15 | 16 | 17 | [ Home ][ Debug Menu ]
18 | 19 |

Badger Tables

20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{range .}} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {{end}} 40 |
LevelKeyCountLeftRightIDSize
{{.Level}}{{.KeyCount}}{{.LeftKey}}{{.RightKey}}{{.ID}}{{.Size}}
41 | 42 | 43 | -------------------------------------------------------------------------------- /pkg/sloop/webserver/webfiles/debugviewkey.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 12 | 13 | Sloop Debug View Record 14 | 15 | 16 | 17 | [ Home ][ Debug Menu ]
18 | 19 | Record View
20 | 21 | 22 | 23 | {{if not (eq "" .ExtraName) }} 24 | 25 | {{end}} 26 |
Key{{.Key}}
Payload
{{.Payload}}
{{.ExtraName}}
{{.ExtraValue}}
27 | 28 | 29 | -------------------------------------------------------------------------------- /pkg/sloop/webserver/webfiles/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/salesforce/sloop/588295c21b968ce93e461103d0d55f149f6bc94c/pkg/sloop/webserver/webfiles/favicon.ico -------------------------------------------------------------------------------- /pkg/sloop/webserver/webfiles/resource.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | html, body { 9 | overflow: auto; 10 | } 11 | 12 | #resource_event_table { 13 | font-family: Helvetica, sans-serif; 14 | border-collapse: collapse; 15 | width: 100%; 16 | } 17 | 18 | .resource-bar-label { 19 | fill: #E5E9F0; 20 | } 21 | 22 | #resource_container { 23 | width: 100%; 24 | } 25 | #close { 26 | float:right; 27 | padding:2px 5px; 28 | background:midnightblue; 29 | color: white; 30 | } 31 | #resource_event_table td, #resource_event_table th { 32 | border: 1px solid lightgrey; 33 | padding: 4px; 34 | } 35 | 36 | #resource_event_table tr:nth-child(even){background-color: whitesmoke;} 37 | 38 | #resource_event_table tr:hover {background-color: lightgrey;} 39 | 40 | #resource_event_table th { 41 | text-align: left; 42 | background-color: midnightblue; 43 | color: white; 44 | cursor:pointer; 45 | } 46 | -------------------------------------------------------------------------------- /pkg/sloop/webserver/webfiles/sloop.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2019, salesforce.com, inc. 3 | * All rights reserved. 4 | * SPDX-License-Identifier: BSD-3-Clause 5 | * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause 6 | */ 7 | 8 | body { 9 | font-family: sans-serif; 10 | } 11 | 12 | .svg-container { 13 | height: 100%; 14 | width: 100%; 15 | overflow: scroll; 16 | background-color: whitesmoke; 17 | } 18 | 19 | svg { 20 | z-index: -1; 21 | } 22 | 23 | .tick text { 24 | font-size: 14px; 25 | } 26 | 27 | html, body { 28 | height: 100%; 29 | padding: 0; 30 | margin: 0; 31 | overflow: hidden; 32 | font-family: sans-serif; 33 | } 34 | 35 | .tick { 36 | color: #E5E9F0; 37 | } 38 | 39 | .ignore-pointer-events { 40 | pointer-events: none; 41 | } 42 | 43 | .resource-bar-label { 44 | dominant-baseline: hanging; 45 | font-size: 14px; 46 | } 47 | 48 | .heatmap { 49 | cursor: pointer; 50 | } 51 | 52 | .resource { 53 | cursor: pointer; 54 | } 55 | 56 | .tooltip { 57 | position: fixed; 58 | opacity: 0; 59 | background: whitesmoke; 60 | color: #3B4252; 61 | border-radius: 5px; 62 | box-shadow: 0 0 10px rgba(0,0,0,.5); 63 | line-height: 1; 64 | z-index: 1; 65 | font: 12px sans-serif; 66 | max-height: 50%; 67 | max-width: 50%; 68 | overflow-y: scroll; 69 | } 70 | 71 | #tiny-tooltip { 72 | padding: 10px; 73 | } 74 | 75 | div#resource_container { 76 | padding: 0; 77 | } 78 | 79 | #sloopleftnav { 80 | height: 100%; 81 | width: 320px; 82 | padding-right: 20px; 83 | overflow-y: auto; 84 | } 85 | 86 | #currentContext { 87 | width:100%; 88 | } 89 | 90 | select { 91 | width: 100%; 92 | } 93 | 94 | .svg-container { 95 | padding-top: 10px; 96 | border-left: 2px solid black; 97 | height: 100%; 98 | overflow: scroll; 99 | background-color: #2E3440; 100 | color: #D8DEE9; 101 | } 102 | 103 | svg { 104 | z-index: -1; 105 | } 106 | 107 | .sloopleftnav { 108 | background-color: #3B4252; 109 | padding-left: 10px; 110 | padding-top: 10px; 111 | font-size: 13px; 112 | color: #E5E9F0; 113 | } 114 | 115 | #sloopleftnav a:link { color: lightgray } 116 | #sloopleftnav a:visited { color: lightgray } 117 | #sloopleftnav a:hover { color: white } 118 | #sloopleftnav a:active { color: lightgray } 119 | 120 | .custom_progress { 121 | display: inline-block; 122 | vertical-align: top; 123 | text-align: center; 124 | height: 100%; 125 | } 126 | 127 | a[target="_blank"]:after { 128 | content: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAQElEQVR42qXKwQkAIAxDUUdxtO6/RBQkQZvSi8I/pL4BoGw/XPkh4XigPmsUgh0626AjRsgxHTkUThsG2T/sIlzdTsp52kSS1wAAAABJRU5ErkJggg==); 129 | margin: 0 3px 0 5px; 130 | } 131 | 132 | input[type="datetime-local"] { 133 | width: 200px; 134 | height: 1.3rem; 135 | font-size: 0.85em; 136 | } 137 | 138 | input[type="button"] { 139 | width: 35px; 140 | height: 1.3rem; 141 | font-size: 0.80em; 142 | } 143 | 144 | input[type="text"] { 145 | width: 245px; 146 | height: 1.3rem; 147 | } 148 | 149 | /* Toggle switch styling */ 150 | .switch-container { 151 | display: inline-block; 152 | margin-top: 20px; 153 | } 154 | 155 | .switch-container p { 156 | display:inline-block; 157 | vertical-align: top; 158 | } 159 | 160 | .switch { 161 | position: relative; 162 | display: inline-block; 163 | width: 32px; 164 | height: 20px; 165 | margin-left: auto; 166 | margin-right: 0; 167 | } 168 | 169 | .switch input { 170 | display: none; 171 | } 172 | 173 | .slider { 174 | position: absolute; 175 | cursor: pointer; 176 | top: 0; 177 | left: 0; 178 | right: 0; 179 | bottom: 0; 180 | background-color: #ccc; 181 | -webkit-transition: .4s; 182 | transition: .4s; 183 | } 184 | 185 | .slider:before { 186 | position: absolute; 187 | content: ""; 188 | height: 14px; 189 | width: 14px; 190 | left: 2px; 191 | bottom: 3px; 192 | background-color: white; 193 | -webkit-transition: .4s; 194 | transition: .4s; 195 | } 196 | 197 | input:checked+.slider { 198 | background-color: #6bab35; 199 | } 200 | 201 | input:focus+.slider { 202 | box-shadow: 0 0 1px #6bab35; 203 | } 204 | 205 | input:checked+.slider:before { 206 | -webkit-transform: translateX(14px); 207 | -ms-transform: translateX(14px); 208 | transform: translateX(14px); 209 | } 210 | /* Rounded sliders */ 211 | .slider.round { 212 | border-radius: 34px; 213 | } 214 | 215 | .slider.round:before { 216 | border-radius: 50%; 217 | } 218 | -------------------------------------------------------------------------------- /pkg/sloop/webserver/webfiles_test.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "os" 5 | "path" 6 | "testing" 7 | 8 | "github.com/salesforce/sloop/pkg/sloop/common" 9 | "github.com/spf13/afero" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | const ( 14 | defaultFileMode = os.FileMode(0755) 15 | someContents1 = "contents abcd" 16 | filePath = "webfiles/index.html" 17 | fileName = "index.html" 18 | ) 19 | 20 | func Test_BindataReadWebfile_True(t *testing.T) { 21 | expectedOutput, err := Asset(filePath) 22 | assert.Nil(t, err) 23 | 24 | actualOutput, _ := readWebfile(fileName, &afero.Afero{afero.NewMemMapFs()}) 25 | assert.Equal(t, expectedOutput, actualOutput) 26 | } 27 | 28 | func Test_LocalReadWebfile_True(t *testing.T) { 29 | notExpectedOutput, _ := Asset(filePath) 30 | 31 | fs := &afero.Afero{afero.NewMemMapFs()} 32 | fullPath := common.GetFilePath(webFilesPath, fileName) 33 | writeFile(t, fs, fullPath, someContents1) 34 | 35 | actualOutput, _ := readWebfile(fileName, fs) 36 | 37 | assert.NotEqual(t, notExpectedOutput, actualOutput) 38 | assert.Equal(t, []uint8(someContents1), actualOutput) 39 | } 40 | 41 | func Test_FileNotinLocalOrBin(t *testing.T) { 42 | fileName := "blah.html" 43 | _, err := readWebfile(fileName, &afero.Afero{afero.NewMemMapFs()}) 44 | assert.Errorf(t, err, errorString, fileName) 45 | } 46 | 47 | func writeFile(t *testing.T, fs *afero.Afero, filePath string, content string) { 48 | err := fs.MkdirAll(path.Dir(filePath), defaultFileMode) 49 | assert.Nil(t, err) 50 | err = fs.WriteFile(filePath, []byte(content), defaultFileMode) 51 | assert.Nil(t, err) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/sloop/webserver/webserver_test.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | badger "github.com/dgraph-io/badger/v2" 9 | "github.com/salesforce/sloop/pkg/sloop/store/untyped/badgerwrap" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestRedirectHandlerHandler(t *testing.T) { 14 | testCases := map[string]struct { 15 | url string 16 | code int 17 | location string 18 | }{ 19 | "successfully redirect on /": { 20 | "/", 21 | http.StatusTemporaryRedirect, 22 | "/clusterContext", 23 | }, 24 | "return 404 for invalid url": { 25 | "/an-invalid-url", 26 | http.StatusNotFound, 27 | "", 28 | }, 29 | } 30 | 31 | for name, tc := range testCases { 32 | t.Run(name, func(t *testing.T) { 33 | req, err := http.NewRequest("GET", tc.url, nil) 34 | assert.Nil(t, err) 35 | // Create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 36 | rr := httptest.NewRecorder() 37 | handler := http.HandlerFunc(redirectHandler("clusterContext")) 38 | handler.ServeHTTP(rr, req) 39 | 40 | assert.Equal(t, tc.code, rr.Code) 41 | if len(tc.location) > 0 { 42 | assert.Equal(t, tc.location, rr.Result().Header["Location"][0]) 43 | } 44 | }) 45 | } 46 | } 47 | 48 | func TestWebFileHandler(t *testing.T) { 49 | req, err := http.NewRequest("GET", "/clusterContext/webfiles/index.html", nil) 50 | assert.Nil(t, err) 51 | 52 | // Create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 53 | rr := httptest.NewRecorder() 54 | handler := http.HandlerFunc(webFileHandler("clusterContext")) 55 | handler.ServeHTTP(rr, req) 56 | 57 | assert.Equal(t, http.StatusOK, rr.Code) 58 | assert.NotNil(t, rr.Body.String()) 59 | } 60 | 61 | func TestBackupHandler(t *testing.T) { 62 | req, err := http.NewRequest("GET", "/clusterContext/data/backup", nil) 63 | assert.Nil(t, err) 64 | 65 | db, err := (&badgerwrap.MockFactory{}).Open(badger.DefaultOptions("")) 66 | assert.Nil(t, err) 67 | 68 | // Create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. 69 | rr := httptest.NewRecorder() 70 | handler := http.HandlerFunc(backupHandler(db, "clusterContext")) 71 | handler.ServeHTTP(rr, req) 72 | 73 | assert.Equal(t, http.StatusOK, rr.Code) 74 | assert.NotNil(t, rr.Body.String()) 75 | } 76 | -------------------------------------------------------------------------------- /prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s 3 | evaluation_interval: 15s 4 | 5 | rule_files: 6 | # - "first.rules" 7 | # - "second.rules" 8 | 9 | scrape_configs: 10 | - job_name: sloop 11 | static_configs: 12 | - targets: ['localhost:8080'] 13 | -------------------------------------------------------------------------------- /providers/aws/sloop-to-eks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | USAGE="Usage: ./sloop_to_eks.sh [] [] 4 | 5 | : Provide EKS cluster to connect to. 6 | : defaults to us-west-2. 7 | : defaults to \`default\` 8 | " 9 | 10 | if [ $# -lt 1 ] || [ "$1" == "help" ]; then 11 | echo "$USAGE" 12 | exit 0 13 | fi 14 | REGION="us-west-2" 15 | if [ "$2" != "" ]; then 16 | REGION=$2 17 | fi 18 | PROFILE="default" 19 | if [ "$3" != "" ]; then 20 | PROFILE=$3 21 | fi 22 | aws eks --region $REGION --profile $PROFILE update-kubeconfig --name $1 23 | docker run --rm -it -p 8080:8080 -v ~/.kube/:/kube/ -e KUBECONFIG=/kube/config -e AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN=$AWS_SESSION_TOKEN sloop 24 | --------------------------------------------------------------------------------