├── .gitignore ├── .golangci.yaml ├── .goreleaser.yml ├── .helmignore ├── .lighthouse └── jenkins-x │ ├── pullrequest.yaml │ ├── release.yaml │ └── triggers.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── OWNERS ├── README.md ├── charts └── jx-pipelines-visualizer │ ├── Chart.yaml │ ├── Makefile │ ├── templates │ ├── _helpers.tpl │ ├── deployment.yaml │ ├── ingress.yaml │ ├── istio.yaml │ ├── rbac.yaml │ └── service.yaml │ └── values.yaml ├── cmd └── server │ └── main.go ├── docs └── screenshots │ ├── home.png │ ├── pipeline-success-with-timeline.png │ └── pipeline-success.png ├── go.mod ├── go.sum ├── hack └── changelog-header.md ├── informer.go ├── internal ├── kube │ ├── client.go │ └── config.go └── version │ └── version.go ├── pipeline.go ├── pipeline_running.go ├── promote.sh ├── store.go └── web ├── handlers ├── branch.go ├── functions │ ├── app.go │ ├── date.go │ ├── links.go │ ├── pipeline.go │ ├── pipeline_counts.go │ └── reflect.go ├── home.go ├── logs.go ├── logs_live.go ├── owner.go ├── pipeline.go ├── pipelinerun.go ├── repository.go ├── router.go ├── running.go ├── running_events.go └── shields_io.go ├── static ├── app.css ├── avatar.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── home │ └── index.js ├── jenkins-x.svg ├── lib │ ├── ansi_up.js │ ├── ansi_up.js.map │ ├── clr-icons.min.css │ ├── clr-icons.min.js │ ├── clr-ui.min.css │ └── custom-elements.min.js ├── pipeline │ ├── index.js │ └── main.css └── running │ └── index.js └── templates ├── archived_logs.tmpl ├── home.tmpl ├── layout.tmpl ├── pipeline.tmpl └── running.tmpl /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | 4 | build/ 5 | dist/ 6 | 7 | .cr-release-packages/ -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 1h30m 3 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | env: 3 | - GO111MODULE=on 4 | - CGO_ENABLED=0 5 | before: 6 | hooks: 7 | - go mod download 8 | 9 | builds: 10 | - id: jx-pipelines-visualizer 11 | # Path to main.go file or main package. 12 | # Default is `.`. 13 | main: ./cmd/server/main.go 14 | 15 | # Binary name. 16 | # Can be a path (e.g. `bin/app`) to wrap the binary in a directory. 17 | # Default is the name of the project directory. 18 | binary: jx-pipelines-visualizer 19 | 20 | # Custom ldflags templates. 21 | # Default is `-s -w -X main.version={{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{.Date}} -X main.builtBy=goreleaser`. 22 | ldflags: 23 | - -X "github.com/jenkins-x/jx-pipelines-visualizer/internal/version.Version={{.Env.VERSION}}" -X "github.com/jenkins-x/jx-pipelines-visualizer/internal/version.Revision={{.Env.REV}}" -X "github.com/jenkins-x/jx-pipelines-visualizer/internal/version.Date={{.Env.BUILDDATE}}" 24 | 25 | # GOOS list to build for. 26 | # For more info refer to: https://golang.org/doc/install/source#environment 27 | # Defaults are darwin and linux. 28 | goos: 29 | - windows 30 | - darwin 31 | - linux 32 | 33 | # GOARCH to build for. 34 | # For more info refer to: https://golang.org/doc/install/source#environment 35 | # Defaults are 386 and amd64. 36 | goarch: 37 | - amd64 38 | - arm64 39 | ignore: 40 | - goos: windows 41 | goarch: arm64 42 | 43 | archives: 44 | - name_template: "jx-pipelines-visualizer-{{ .Os }}-{{ .Arch }}" 45 | format_overrides: 46 | - goos: windows 47 | format: zip 48 | 49 | checksum: 50 | # You can change the name of the checksums file. 51 | # Default is `jx-pipelines-visualizer_{{ .Version }}_checksums.txt`. 52 | name_template: "jx-pipelines-visualizer-checksums.txt" 53 | 54 | # Algorithm to be used. 55 | # Accepted options are sha256, sha512, sha1, crc32, md5, sha224 and sha384. 56 | # Default is sha256. 57 | algorithm: sha256 58 | 59 | changelog: 60 | # set it to true if you wish to skip the changelog generation 61 | disable: true 62 | 63 | release: 64 | # If set to true, will not auto-publish the release. 65 | # Default is false. 66 | draft: false 67 | 68 | # If set to auto, will mark the release as not ready for production 69 | # in case there is an indicator for this in the tag e.g. v1.0.0-rc1 70 | # If set to true, will mark the release as not ready for production. 71 | # Default is false. 72 | prerelease: false 73 | 74 | # You can change the name of the GitHub release. 75 | # Default is `{{.Tag}}` 76 | name_template: "{{.Env.VERSION}}" 77 | 78 | -------------------------------------------------------------------------------- /.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 | *.png 23 | 24 | # known compile time folders 25 | target/ 26 | node_modules/ 27 | vendor/ -------------------------------------------------------------------------------- /.lighthouse/jenkins-x/pullrequest.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tekton.dev/v1beta1 2 | kind: PipelineRun 3 | metadata: 4 | creationTimestamp: null 5 | name: pullrequest 6 | spec: 7 | pipelineSpec: 8 | tasks: 9 | - name: from-build-pack 10 | resources: {} 11 | taskSpec: 12 | metadata: {} 13 | stepTemplate: 14 | image: uses:jenkins-x/jx3-pipeline-catalog/tasks/go-plugin/pullrequest.yaml@versionStream 15 | name: "" 16 | resources: {} 17 | workingDir: /workspace/source 18 | steps: 19 | - image: uses:jenkins-x/jx3-pipeline-catalog/tasks/git-clone/git-clone-pr.yaml@versionStream 20 | name: "" 21 | resources: {} 22 | - name: jx-variables 23 | - name: build-make-linux 24 | resources: {} 25 | - name: build-make-test 26 | resources: {} 27 | - name: build-container-build 28 | resources: {} 29 | podTemplate: {} 30 | serviceAccountName: tekton-bot 31 | timeout: 1h0m0s 32 | status: {} 33 | -------------------------------------------------------------------------------- /.lighthouse/jenkins-x/release.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: tekton.dev/v1beta1 2 | kind: PipelineRun 3 | metadata: 4 | creationTimestamp: null 5 | name: release 6 | spec: 7 | pipelineSpec: 8 | tasks: 9 | - name: chart 10 | resources: {} 11 | taskSpec: 12 | metadata: {} 13 | stepTemplate: 14 | image: uses:jenkins-x/jx3-pipeline-catalog/tasks/go-plugin/release.yaml@versionStream 15 | name: "" 16 | resources: {} 17 | workingDir: /workspace/source 18 | steps: 19 | - image: uses:jenkins-x/jx3-pipeline-catalog/tasks/git-clone/git-clone.yaml@versionStream 20 | name: "" 21 | resources: {} 22 | - name: next-version 23 | resources: {} 24 | - name: jx-variables 25 | resources: {} 26 | - name: release-binary 27 | resources: {} 28 | - name: build-and-push-image 29 | resources: {} 30 | - name: chart-docs 31 | resources: {} 32 | - name: changelog 33 | resources: {} 34 | - name: release-chart 35 | resources: {} 36 | - name: upload-binaries 37 | resources: {} 38 | - name: promote-release 39 | resources: {} 40 | serviceAccountName: tekton-bot 41 | timeout: 1h30m0s 42 | status: {} 43 | -------------------------------------------------------------------------------- /.lighthouse/jenkins-x/triggers.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: config.lighthouse.jenkins-x.io/v1alpha1 2 | kind: TriggerConfig 3 | spec: 4 | presubmits: 5 | - name: pr 6 | context: "pr" 7 | always_run: true 8 | optional: false 9 | source: "pullrequest.yaml" 10 | postsubmits: 11 | - name: release 12 | context: "release" 13 | source: "release.yaml" 14 | branches: 15 | - ^main$ 16 | - ^master$ 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.12 2 | 3 | RUN apk add --no-cache ca-certificates \ 4 | && adduser -D -u 1000 jx 5 | 6 | COPY ./web/static /app/web/static 7 | COPY ./web/templates /app/web/templates 8 | COPY ./build/linux/jx-pipelines-visualizer /app/ 9 | 10 | WORKDIR /app 11 | USER 1000 12 | 13 | ENTRYPOINT ["/app/jx-pipelines-visualizer"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dailymotion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Make does not offer a recursive wildcard function, so here's one: 2 | rwildcard=$(wildcard $1$2) $(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2)) 3 | 4 | SHELL := /bin/bash 5 | NAME := jx-project 6 | BINARY_NAME := jx-pipelines-visualizer 7 | BUILD_TARGET = build 8 | MAIN_SRC_FILE=cmd/server/main.go 9 | GO := GO111MODULE=on go 10 | GO_NOMOD :=GO111MODULE=off go 11 | REV := $(shell git rev-parse --short HEAD 2> /dev/null || echo 'unknown') 12 | ORG := jenkins-x 13 | ORG_REPO := $(ORG)/$(NAME) 14 | RELEASE_ORG_REPO := $(ORG_REPO) 15 | ROOT_PACKAGE := github.com/$(ORG_REPO) 16 | GO_VERSION := $(shell $(GO) version | sed -e 's/^[^0-9.]*\([0-9.]*\).*/\1/') 17 | #GO_VERSION := 1.23.3 18 | 19 | GO_DEPENDENCIES := $(call rwildcard,pkg/,*.go) $(call rwildcard,cmd/j,*.go) 20 | 21 | GOPRIVATE := github.com/jenkins-x/jx-apps,github.com/jenkins-x/jx-helpers 22 | 23 | BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2> /dev/null || echo 'unknown') 24 | BUILD_DATE := $(shell date +%Y%m%d-%H:%M:%S) 25 | CGO_ENABLED = 0 26 | 27 | REPORTS_DIR=$(BUILD_TARGET)/reports 28 | 29 | GOTEST := $(GO) test 30 | 31 | # set dev version unless VERSION is explicitly set via environment 32 | VERSION ?= $(shell echo "$$(git for-each-ref refs/tags/ --count=1 --sort=-version:refname --format='%(refname:short)' 2>/dev/null)-dev+$(REV)" | sed 's/^v//') 33 | 34 | # Build flags for setting build-specific configuration at build time - defaults to empty 35 | #BUILD_TIME_CONFIG_FLAGS ?= "" 36 | 37 | # Full build flags used when building binaries. Not used for test compilation/execution. 38 | BUILDFLAGS := -ldflags \ 39 | " -X $(ROOT_PACKAGE)/pkg/version.Version=$(VERSION)\ 40 | -X $(ROOT_PACKAGE)/pkg/version.Revision='$(REV)'\ 41 | -X $(ROOT_PACKAGE)/pkg/version.Branch='$(BRANCH)'\ 42 | -X $(ROOT_PACKAGE)/pkg/version.BuildDate='$(BUILD_DATE)'\ 43 | -X $(ROOT_PACKAGE)/pkg/version.GoVersion='$(GO_VERSION)'\ 44 | -X github.com/jenkins-x/jx-pipelines-visualizer/internal/version.Version=$(VERSION)\ 45 | -X github.com/jenkins-x/jx-pipelines-visualizer/internal/version.Revision='$(REV)'\ 46 | -X github.com/jenkins-x/jx-pipelines-visualizer/internal/version.Date='$(BUILD_DATE)'\ 47 | $(BUILD_TIME_CONFIG_FLAGS)" 48 | 49 | # Some tests expect default values for version.*, so just use the config package values there. 50 | TEST_BUILDFLAGS := -ldflags "$(BUILD_TIME_CONFIG_FLAGS)" 51 | 52 | ifdef DEBUG 53 | BUILDFLAGS := -gcflags "all=-N -l" $(BUILDFLAGS) 54 | endif 55 | 56 | ifdef PARALLEL_BUILDS 57 | BUILDFLAGS += -p $(PARALLEL_BUILDS) 58 | GOTEST += -p $(PARALLEL_BUILDS) 59 | else 60 | # -p 4 seems to work well for people 61 | GOTEST += -p 4 62 | endif 63 | 64 | ifdef DISABLE_TEST_CACHING 65 | GOTEST += -count=1 66 | endif 67 | 68 | TEST_PACKAGE ?= ./... 69 | COVER_OUT:=$(REPORTS_DIR)/cover.out 70 | COVERFLAGS=-coverprofile=$(COVER_OUT) --covermode=count --coverpkg=./... 71 | 72 | .PHONY: list 73 | list: ## List all make targets 74 | @$(MAKE) -pRrn : -f $(MAKEFILE_LIST) 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | sort 75 | 76 | .PHONY: help 77 | .DEFAULT_GOAL := help 78 | help: 79 | @grep -h -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 80 | 81 | full: check ## Build and run the tests 82 | check: build test ## Build and run the tests 83 | get-test-deps: ## Install test dependencies 84 | $(GO_NOMOD) get github.com/axw/gocov/gocov 85 | $(GO_NOMOD) get -u gopkg.in/matm/v1/gocov-html 86 | 87 | print-version: ## Print version 88 | @echo $(VERSION) 89 | 90 | build: $(GO_DEPENDENCIES) clean ## Build jx-labs binary for current OS 91 | go env -w GOPRIVATE=github.com/jenkins-x/jx-apps,github.com/jenkins-x/jx-helpers 92 | CGO_ENABLED=$(CGO_ENABLED) $(GO) $(BUILD_TARGET) $(BUILDFLAGS) -o build/$(BINARY_NAME) $(MAIN_SRC_FILE) 93 | 94 | build-all: $(GO_DEPENDENCIES) build make-reports-dir ## Build all files - runtime, all tests etc. 95 | CGO_ENABLED=$(CGO_ENABLED) $(GOTEST) -run=nope -tags=integration -failfast -short ./... $(BUILDFLAGS) 96 | 97 | tidy-deps: ## Cleans up dependencies 98 | $(GO) mod tidy 99 | # mod tidy only takes compile dependencies into account, let's make sure we capture tooling dependencies as well 100 | @$(MAKE) install-generate-deps 101 | 102 | .PHONY: make-reports-dir 103 | make-reports-dir: 104 | mkdir -p $(REPORTS_DIR) 105 | 106 | test: ## Run tests with the "unit" build tag 107 | KUBECONFIG=/cluster/connections/not/allowed CGO_ENABLED=$(CGO_ENABLED) $(GOTEST) --tags="integration unit" -failfast -short ./... $(TEST_BUILDFLAGS) 108 | 109 | test-coverage : make-reports-dir ## Run tests and coverage for all tests with the "unit" build tag 110 | CGO_ENABLED=$(CGO_ENABLED) $(GOTEST) --tags=unit $(COVERFLAGS) -failfast -short ./... $(TEST_BUILDFLAGS) 111 | 112 | test-report: make-reports-dir get-test-deps test-coverage ## Create the test report 113 | @gocov convert $(COVER_OUT) | gocov report 114 | 115 | test-report-html: make-reports-dir get-test-deps test-coverage ## Create the test report in HTML format 116 | @gocov convert $(COVER_OUT) | gocov-html > $(REPORTS_DIR)/cover.html && open $(REPORTS_DIR)/cover.html 117 | 118 | install: $(GO_DEPENDENCIES) ## Install the binary 119 | GOBIN=${GOPATH}/bin $(GO) install $(BUILDFLAGS) $(MAIN_SRC_FILE) 120 | 121 | linux: ## Build for Linux 122 | CGO_ENABLED=$(CGO_ENABLED) GOOS=linux GOARCH=amd64 $(GO) $(BUILD_TARGET) $(BUILDFLAGS) -o build/linux/$(BINARY_NAME) $(MAIN_SRC_FILE) 123 | chmod +x build/linux/$(BINARY_NAME) 124 | 125 | arm: ## Build for ARM 126 | CGO_ENABLED=$(CGO_ENABLED) GOOS=linux GOARCH=arm $(GO) $(BUILD_TARGET) $(BUILDFLAGS) -o build/arm/$(BINARY_NAME) $(MAIN_SRC_FILE) 127 | chmod +x build/arm/$(BINARY_NAME) 128 | 129 | win: ## Build for Windows 130 | CGO_ENABLED=$(CGO_ENABLED) GOOS=windows GOARCH=amd64 $(GO) $(BUILD_TARGET) $(BUILDFLAGS) -o build/win/$(BINARY_NAME)-windows-amd64.exe $(MAIN_SRC_FILE) 131 | 132 | darwin: ## Build for OSX 133 | CGO_ENABLED=$(CGO_ENABLED) GOOS=darwin GOARCH=amd64 $(GO) $(BUILD_TARGET) $(BUILDFLAGS) -o build/darwin/$(BINARY_NAME) $(MAIN_SRC_FILE) 134 | chmod +x build/darwin/$(BINARY_NAME) 135 | 136 | .PHONY: release 137 | release: clean linux test 138 | 139 | release-all: release linux win darwin 140 | 141 | .PHONY: goreleaser 142 | goreleaser: 143 | step-go-releaser --organisation=$(ORG) --revision=$(REV) --branch=$(BRANCH) --build-date=$(BUILD_DATE) --go-version=$(GO_VERSION) --root-package=$(ROOT_PACKAGE) --version=$(VERSION) 144 | 145 | .PHONY: clean 146 | clean: ## Clean the generated artifacts 147 | rm -rf build release dist 148 | 149 | get-fmt-deps: ## Install test dependencies 150 | $(GO_NOMOD) get golang.org/x/tools/cmd/goimports 151 | 152 | .PHONY: fmt 153 | fmt: importfmt ## Format the code 154 | $(eval FORMATTED = $(shell $(GO) fmt ./...)) 155 | @if [ "$(FORMATTED)" == "" ]; \ 156 | then \ 157 | echo "All Go files properly formatted"; \ 158 | else \ 159 | echo "Fixed formatting for: $(FORMATTED)"; \ 160 | fi 161 | 162 | .PHONY: importfmt 163 | importfmt: get-fmt-deps 164 | @echo "Formatting the imports..." 165 | goimports -w $(GO_DEPENDENCIES) 166 | 167 | .PHONY: lint 168 | lint: ## Lint the code 169 | ./hack/gofmt.sh 170 | ./hack/linter.sh 171 | ./hack/generate.sh 172 | 173 | .PHONY: all 174 | all: fmt build lint test 175 | 176 | bin/docs: 177 | go build $(LDFLAGS) -v -o bin/docs cmd/docs/*.go 178 | 179 | .PHONY: docs 180 | docs: bin/docs 181 | @echo "Generating docs" 182 | @./bin/docs --target=./docs/cmd 183 | @./bin/docs --target=./docs/man/man1 --kind=man 184 | @rm -f ./bin/docs -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | approvers: 2 | - rawlingsj 3 | - jstrachan 4 | - vbehar 5 | - florianorpeliere 6 | - ankitm123 7 | - babadofar 8 | - tomhobson 9 | - msvticket 10 | reviewers: 11 | - rawlingsj 12 | - jstrachan 13 | - vbehar 14 | - florianorpeliere 15 | - ankitm123 16 | - babadofar 17 | - tomhobson 18 | - msvticket 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jenkins X Pipelines Visualizer 2 | 3 | This is a Web UI for [Jenkins X](https://jenkins-x.io/), with a clear goal: **visualize the pipelines - and their logs**. 4 | 5 | ![Pipeline View](docs/screenshots/pipeline-success.png) 6 | 7 | ## Features 8 | 9 | - View the pipelines information: metadata, status, stages and steps with timing 10 | - View the pipelines logs in real-time 11 | - Retrieve the archived pipelines logs from the long-term storage (GCS, S3, ...) 12 | - View all pipelines with their status, and filter/sort them 13 | - Read-only: only requires READ permissions on the Jenkins X and Tekton Pipelines CRDs 14 | - Expose a [Shields.io](https://shields.io/) compatible [endpoint](https://shields.io/endpoint) 15 | - Backward-compatible URLs with the old "JX UI" - so that you can easily swap the JXUI URL for the jx-pipelines-visualizer one in the Lighthouse config, and have Lighthouse set links to jx-pipelines-visualizer in GitHub Pull Requests. 16 | - Work in context of a single namespace or in a cluster context 17 | 18 | ### Screenshots 19 | 20 | ![Pipeline with timeline](docs/screenshots/pipeline-success-with-timeline.png) 21 | 22 | ![Home](docs/screenshots/home.png) 23 | 24 | You can also see the [announcement blog post](https://jenkins-x.io/blog/2020/09/23/jx-pipelines-visualizer/) for more details and a demo. 25 | 26 | ### Out of scope 27 | 28 | There are a number of features we don't want to include in this project - at least for the moment: 29 | 30 | - Everything Auth-related 31 | - use a reverse-proxy in front or anything else to handle it 32 | - for example [Vouch and Okta](https://medium.com/@vbehar/how-to-protect-a-kubernetes-ingress-behind-okta-with-nginx-91e279e06009) 33 | - or [dex](https://github.com/dexidp/dex), [oauth2-proxy](https://github.com/oauth2-proxy/oauth2-proxy), ... 34 | - or [nginx basic-auth](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#authentication) - if you are using the nginx ingress controller 35 | - Create/Update/Delete operations 36 | - it is meant to be a read-only web UI 37 | - you can use the Octant-based UI for these use-cases. 38 | - Anything in Jenkins X which is not related to the pipelines 39 | - such as managing repositories, environments, and so on. 40 | - you can use the Octant-based UI for these use-cases. 41 | 42 | ## Installation 43 | 44 | ### With Jenkins X v3 45 | 46 | It's already installed by default with Jenkins X v3. 47 | 48 | By default an ingress is created to access the UI using basic authentication. See the [documentation for how to access it](https://jenkins-x.io/v3/develop/ui/dashboard/#accessing-the-pipelines-visualizer) 49 | 50 | You can see the default values here: 51 | 52 | ### With Jenkins X v2 53 | 54 | In the Git repository for your dev environment: 55 | 56 | - Update the `env/requirements.yaml` file with the following: 57 | ``` 58 | - name: jx-pipelines-visualizer 59 | repository: https://jenkins-x-charts.github.io/repo 60 | version: 1.7.2 61 | ``` 62 | - Create a new file `env/jx-pipelines-visualizer/values.tmpl.yaml` with the following content: 63 | ``` 64 | {{- if .Requirements.storage.logs.enabled }} 65 | config: 66 | archivedLogsURLTemplate: >- 67 | {{ .Requirements.storage.logs.url }}{{`/jenkins-x/logs/{{.Owner}}/{{.Repository}}/{{if hasPrefix .Branch "pr"}}{{.Branch | upper}}{{else}}{{.Branch}}{{end}}/{{.Build}}.log`}} 68 | {{- end }} 69 | 70 | gitSecretName: "" 71 | 72 | ingress: 73 | enabled: true 74 | hosts: 75 | - pipelines{{.Requirements.ingress.namespaceSubDomain}}{{.Requirements.ingress.domain}} 76 | {{- if .Requirements.ingress.tls.enabled }} 77 | tls: 78 | enabled: true 79 | secrets: 80 | # re-use the existing tls secret managed by jx 81 | {{- if .Requirements.ingress.tls.production }} 82 | tls-{{ .Requirements.ingress.domain | replace "." "-" }}-p: {} 83 | {{- else }} 84 | tls-{{ .Requirements.ingress.domain | replace "." "-" }}-s: {} 85 | {{- end }} 86 | {{- end }} 87 | annotations: 88 | kubernetes.io/ingress.class: nginx 89 | ``` 90 | 91 | This will expose the UI at `pipelines.your.domain.tld` - without any auth. You can add [basic auth by appending a few additional annotations](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/#authentication) - re-using the Jenkins X Auth Secret: 92 | 93 | ``` 94 | nginx.ingress.kubernetes.io/auth-type: basic 95 | nginx.ingress.kubernetes.io/auth-secret: jx-basic-auth 96 | ``` 97 | 98 | - If you want [Lighthouse](https://github.com/jenkins-x/lighthouse) to add links to your jx-pipelines-visualizer instance from your Pull/Merge Request checks, update the `env/lighthouse-jx/values.tmpl.yaml` file and add the following: 99 | ``` 100 | env: 101 | LIGHTHOUSE_REPORT_URL_BASE: "https://pipelines{{.Requirements.ingress.namespaceSubDomain}}{{.Requirements.ingress.domain}}" 102 | ``` 103 | 104 | ### With Helm v3 105 | 106 | ``` 107 | $ helm repo add jx https://jenkins-x-charts.github.io/repo 108 | $ helm install jx-pipelines-visualizer jx/jx-pipelines-Visualizer 109 | ``` 110 | 111 | ### With Helm v2 112 | 113 | ``` 114 | $ helm repo add jx https://jenkins-x-charts.github.io/repo 115 | $ helm repo update 116 | $ helm install --name jx-pipelines-visualizer jx/jx-pipelines-visualizer 117 | ``` 118 | 119 | ## Usage 120 | 121 | Just go to the homepage, and use the links to view the pipelines logs. 122 | 123 | To generate a status badge compatible with [shields.io](https://shields.io/): 124 | - read the [shields.io documentation](https://shields.io/endpoint) 125 | - the custom endpoint is: `https://YOUR_HOST/{owner}/{repo}/{branch}/shields.io` - for example `https://jx.example.com/my-org/my-repo/master/shields.io`. It returns a JSON response with the status of the latest build for the given branch. 126 | 127 | ### Configuration 128 | 129 | See the [values.yaml](charts/jx-pipelines-visualizer/values.yaml) file for the configuration. 130 | 131 | If you are not using the Helm Chart, the binary is using CLI flags only - no config files. You can run `jx-pipelines-visualizer -h` to see all the flags. 132 | 133 | ## Running locally 134 | 135 | ``` 136 | go run cmd/server/main.go 137 | ``` 138 | 139 | ## How It Works 140 | 141 | It uses the "informer" Kubernetes pattern to keep a local cache of the Jenkins X PipelineActivities, and index them in an in-memory [Bleve](http://blevesearch.com/) index. 142 | 143 | It uses part of jx code to retrieve the build logs - mainly the part to stream the build logs from the running pods. It is the same code used by the `jx get build logs` command. 144 | 145 | ## Credits 146 | 147 | Thanks to [Dailymotion](https://www.dailymotion.com/) for creating the [original repository](https://github.com/dailymotion/jx-pipelines-visualizer) and then donate it to the Jenkins X project. 148 | -------------------------------------------------------------------------------- /charts/jx-pipelines-visualizer/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | name: jx-pipelines-visualizer 3 | description: Web UI for Jenkins X, with a clear goal - visualize the pipelines - and their logs. 4 | icon: https://raw.githubusercontent.com/jenkins-x/jenkins-x-website/master/images/logo/jenkinsx-icon-color.svg 5 | home: https://github.com/jenkins-x/jx-pipelines-visualizer 6 | version: 0.0.1 7 | appVersion: latest 8 | sources: 9 | - https://github.com/jenkins-x/jx-pipelines-visualizer 10 | -------------------------------------------------------------------------------- /charts/jx-pipelines-visualizer/Makefile: -------------------------------------------------------------------------------- 1 | CHART_REPO := gs://jenkinsxio/charts 2 | NAME := jx-pipelines-visualizer 3 | 4 | build: clean 5 | rm -rf Chart.lock 6 | helm dependency build 7 | helm lint 8 | 9 | install: clean build 10 | helm install . --name ${NAME} 11 | 12 | upgrade: clean build 13 | helm upgrade ${NAME} . 14 | 15 | delete: 16 | helm delete --purge ${NAME} 17 | 18 | clean: 19 | rm -rf charts 20 | rm -rf ${NAME}*.tgz 21 | 22 | release: clean 23 | sed -i -e "s/version:.*/version: $(VERSION)/" Chart.yaml 24 | 25 | helm dependency build 26 | helm lint 27 | helm package . 28 | helm repo add jx-labs $(CHART_REPO) 29 | helm gcs push ${NAME}*.tgz jx-labs --public 30 | rm -rf ${NAME}*.tgz% -------------------------------------------------------------------------------- /charts/jx-pipelines-visualizer/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "jxpipelines.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 6 | {{- end -}} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "jxpipelines.fullname" -}} 14 | {{- if .Values.fullnameOverride -}} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 16 | {{- else -}} 17 | {{- $name := default .Chart.Name .Values.nameOverride -}} 18 | {{- if contains $name .Release.Name -}} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 20 | {{- else -}} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 22 | {{- end -}} 23 | {{- end -}} 24 | {{- end -}} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "jxpipelines.chartref" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 31 | {{- end -}} 32 | 33 | {{/* 34 | Prints the labels selector. 35 | */}} 36 | {{- define "jxpipelines.labels.selector" -}} 37 | app.kubernetes.io/name: {{ include "jxpipelines.name" . }} 38 | app.kubernetes.io/instance: {{ .Release.Name | quote }} 39 | {{- end -}} 40 | 41 | {{- /* 42 | Prints the standard Helm labels - used in metadata. 43 | */ -}} 44 | {{- define "jxpipelines.labels" -}} 45 | {{ include "jxpipelines.labels.selector" . }} 46 | helm.sh/chart: {{ include "jxpipelines.chartref" . }} 47 | {{- with .Chart.AppVersion }} 48 | app.kubernetes.io/version: {{ . | quote }} 49 | {{- end }} 50 | app.kubernetes.io/managed-by: {{ .Release.Service | quote }} 51 | {{- end -}} 52 | -------------------------------------------------------------------------------- /charts/jx-pipelines-visualizer/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "jxpipelines.fullname" . }} 5 | labels: 6 | {{- include "jxpipelines.labels" . | nindent 4 }} 7 | {{- with .Values.deployment.labels }} 8 | {{ tpl (toYaml .) $ | trim | indent 4 }} 9 | {{- end }} 10 | {{- with .Values.deployment.annotations }} 11 | annotations: {{- tpl (toYaml .) $ | trim | nindent 4 }} 12 | {{- end }} 13 | spec: 14 | replicas: {{ .Values.deployment.replicas }} 15 | revisionHistoryLimit: {{ .Values.deployment.revisionHistoryLimit }} 16 | selector: 17 | matchLabels: {{- include "jxpipelines.labels.selector" . | nindent 6 }} 18 | template: 19 | metadata: 20 | labels: 21 | {{- include "jxpipelines.labels" . | nindent 8 }} 22 | {{- with .Values.pod.labels }} 23 | {{ tpl (toYaml .) $ | trim | indent 8 }} 24 | {{- end }} 25 | {{- with .Values.pod.annotations }} 26 | annotations: {{- tpl (toYaml .) $ | trim | nindent 8 }} 27 | {{- end }} 28 | spec: 29 | containers: 30 | - name: {{ .Chart.Name }} 31 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 32 | {{- with .Values.image.pullPolicy }} 33 | imagePullPolicy: {{ . }} 34 | {{- end }} 35 | args: 36 | {{- with .Values.config.namespace }} 37 | - -namespace 38 | - {{ . }} 39 | {{- end }} 40 | {{- with .Values.config.resyncInterval }} 41 | - -resync-interval 42 | - {{ . }} 43 | {{- end }} 44 | {{- with .Values.config.archivedLogsURLTemplate }} 45 | - -archived-logs-url-template 46 | - {{ . }} 47 | {{- end }} 48 | {{- with .Values.config.archivedPipelinesURLTemplate }} 49 | - -archived-pipelines-url-template 50 | - {{ . }} 51 | {{- end }} 52 | {{- with .Values.config.archivedPipelineRunsURLTemplate }} 53 | - -archived-pipelineruns-url-template 54 | - {{ . }} 55 | {{- end }} 56 | {{- with .Values.config.pipelineTraceURLTemplate }} 57 | - -pipeline-trace-url-template 58 | - {{ . }} 59 | {{- end }} 60 | {{- with .Values.config.logLevel }} 61 | - -log-level 62 | - {{ . }} 63 | {{- end }} 64 | {{- if .Values.pod.env }} 65 | env: 66 | - name: XDG_CONFIG_HOME 67 | value: /home/jenkins 68 | - name: GIT_SECRET_MOUNT_PATH 69 | value: /secrets/git 70 | {{- range $pkey, $pval := .Values.pod.env }} 71 | - name: {{ $pkey }} 72 | value: {{ quote $pval }} 73 | {{- end }} 74 | {{- end }} 75 | {{- with .Values.pod.envFrom }} 76 | envFrom: 77 | {{- toYaml . | trim | nindent 10 }} 78 | {{- end }} 79 | ports: 80 | - name: http 81 | containerPort: 8080 82 | livenessProbe: 83 | tcpSocket: 84 | port: http 85 | readinessProbe: 86 | httpGet: 87 | path: /healthz 88 | port: http 89 | {{- if or .Values.gitSecretName .Values.extraVolumes }} 90 | volumeMounts: 91 | {{- if .Values.gitSecretName }} 92 | - mountPath: /secrets/git 93 | name: secrets-git 94 | {{- end }} 95 | {{- if .Values.extraVolumeMounts }} 96 | {{ toYaml .Values.extraVolumeMounts | nindent 8 }} 97 | {{- end }} 98 | {{- end }} 99 | {{- with .Values.pod.resources }} 100 | resources: {{- toYaml . | trim | nindent 10 }} 101 | {{- end }} 102 | {{- with .Values.pod.securityContext }} 103 | securityContext: {{- toYaml . | trim | nindent 8 }} 104 | {{- end }} 105 | serviceAccountName: {{ include "jxpipelines.fullname" . }} 106 | enableServiceLinks: {{ .Values.pod.enableServiceLinks }} 107 | {{- with .Values.pod.activeDeadlineSeconds }} 108 | activeDeadlineSeconds: {{ . }} 109 | {{- end }} 110 | {{- with .Values.pod.terminationGracePeriodSeconds }} 111 | terminationGracePeriodSeconds: {{ . }} 112 | {{- end }} 113 | {{- with .Values.pod.affinity }} 114 | affinity: {{- tpl (toYaml .) $ | trim | nindent 8 }} 115 | {{- end }} 116 | {{- with .Values.pod.nodeSelector }} 117 | nodeSelector: {{- tpl (toYaml .) $ | trim | nindent 8 }} 118 | {{- end }} 119 | {{- with .Values.pod.tolerations }} 120 | tolerations: {{- tpl (toYaml .) $ | trim | nindent 8 }} 121 | {{- end }} 122 | {{- with .Values.pod.hostAliases }} 123 | hostAliases: {{- tpl (toYaml .) $ | trim | nindent 8 }} 124 | {{- end }} 125 | {{- with .Values.pod.schedulerName }} 126 | schedulerName: {{ tpl . $ | trim }} 127 | {{- end }} 128 | {{- if or .Values.gitSecretName .Values.extraVolumes }} 129 | volumes: 130 | {{- if .Values.gitSecretName }} 131 | - name: secrets-git 132 | secret: 133 | defaultMode: 420 134 | secretName: {{ .Values.gitSecretName }} 135 | {{- end }} 136 | {{- if .Values.extraVolumes }} 137 | {{- toYaml .Values.extraVolumes | nindent 6 }} 138 | {{- end }} 139 | {{- end }} 140 | -------------------------------------------------------------------------------- /charts/jx-pipelines-visualizer/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | 3 | --- 4 | apiVersion: {{ .Values.ingress.apiVersion }} 5 | kind: Ingress 6 | metadata: 7 | name: {{ include "jxpipelines.fullname" . }} 8 | labels: 9 | {{- include "jxpipelines.labels" . | nindent 4 }} 10 | {{- with .Values.ingress.labels }} 11 | {{- tpl (toYaml .) $ | trim | nindent 4 }} 12 | {{- end }} 13 | {{- if or .Values.ingress.annotations .Values.ingress.class }} 14 | annotations: 15 | {{- with .Values.ingress.class }} 16 | kubernetes.io/ingress.class: {{ . }} 17 | {{- end }} 18 | {{- with .Values.ingress.annotations }} 19 | {{- tpl (toYaml .) $ | trim | nindent 4 }} 20 | {{- end }} 21 | {{- end }} 22 | spec: 23 | rules: 24 | {{- if eq .Values.ingress.apiVersion "networking.k8s.io/v1beta1" }} 25 | {{- range .Values.ingress.hosts }} 26 | - host: {{ tpl . $ }} 27 | http: 28 | paths: 29 | - backend: 30 | serviceName: {{ include "jxpipelines.fullname" $ }} 31 | servicePort: http 32 | {{- end }} 33 | {{- else }} 34 | {{- $pathType := .Values.ingress.pathType | default "ImplementationSpecific" -}} 35 | {{- $path := .Values.ingress.path -}} 36 | {{- range .Values.ingress.hosts }} 37 | - host: {{ tpl . $ }} 38 | http: 39 | paths: 40 | - pathType: {{ $pathType }} 41 | backend: 42 | service: 43 | name: {{ include "jxpipelines.fullname" $ }} 44 | port: 45 | name: http 46 | {{- if eq $pathType "Prefix" }} 47 | path: {{ $path | default "/" }} 48 | {{- end }} 49 | {{- end }} 50 | {{- end }} 51 | 52 | {{- if .Values.ingress.tls.enabled }} 53 | tls: 54 | {{- range $name, $secret := .Values.ingress.tls.secrets }} 55 | {{- if and $secret.b64encodedCertificate $secret.b64encodedCertificateKey }} 56 | - secretName: {{ include "jxpipelines.fullname" $ }}-tls-{{ $name }} 57 | {{- else }} 58 | - secretName: {{ $name }} 59 | {{- end }} 60 | hosts: 61 | {{- if $secret.hosts }} 62 | {{- range $secret.hosts }} 63 | - {{ . }} 64 | {{- end }} 65 | {{- else }} 66 | {{- range $.Values.ingress.hosts }} 67 | - {{ . }} 68 | {{- end }} 69 | {{- end }} 70 | {{- end }} 71 | {{- end }} 72 | 73 | {{- if .Values.ingress.basicAuth.enabled }} 74 | --- 75 | apiVersion: v1 76 | data: 77 | auth: {{ .Values.ingress.basicAuth.authData | quote }} 78 | kind: Secret 79 | metadata: 80 | name: jx-pipelines-visualizer-basic-auth 81 | type: Opaque 82 | {{- end -}} 83 | 84 | {{- if .Values.ingress.tls.enabled -}} 85 | {{- range $name, $secret := .Values.ingress.tls.secrets -}} 86 | {{- if and $secret.b64encodedCertificate $secret.b64encodedCertificateKey }} 87 | --- 88 | apiVersion: v1 89 | kind: Secret 90 | metadata: 91 | name: {{ include "jxpipelines.fullname" $ }}-tls-{{ $name }} 92 | labels: {{- include "jxpipelines.labels" $ | nindent 4 }} 93 | type: kubernetes.io/tls 94 | data: 95 | tls.crt: {{ $secret.b64encodedCertificate | quote }} 96 | tls.key: {{ $secret.b64encodedCertificateKey | quote }} 97 | {{- end -}} 98 | {{- end -}} 99 | {{- end -}} 100 | 101 | {{- end -}} 102 | -------------------------------------------------------------------------------- /charts/jx-pipelines-visualizer/templates/istio.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.istio.enabled }} 2 | apiVersion: {{ .Values.istio.apiVersion }} 3 | kind: VirtualService 4 | metadata: 5 | name: {{ include "jxpipelines.fullname" $ }} 6 | spec: 7 | gateways: 8 | - {{ .Values.istio.gateway }} 9 | hosts: 10 | - dashboard{{ .Values.jxRequirements.ingress.namespaceSubDomain }}{{ .Values.jxRequirements.ingress.domain }} 11 | http: 12 | - route: 13 | - destination: 14 | host: {{ include "jxpipelines.fullname" $ }} 15 | weight: 100 16 | {{- end }} 17 | -------------------------------------------------------------------------------- /charts/jx-pipelines-visualizer/templates/rbac.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "jxpipelines.fullname" . }} 6 | labels: {{- include "jxpipelines.labels" . | nindent 4 }} 7 | annotations: {{ toYaml .Values.serviceAccount.annotations | nindent 4 }} 8 | --- 9 | apiVersion: rbac.authorization.k8s.io/v1 10 | kind: ClusterRole 11 | metadata: 12 | name: {{ include "jxpipelines.fullname" . }} 13 | labels: {{- include "jxpipelines.labels" . | nindent 4 }} 14 | rules: {{- toYaml .Values.role.rules | nindent 2 }} 15 | --- 16 | apiVersion: rbac.authorization.k8s.io/v1 17 | kind: ClusterRoleBinding 18 | metadata: 19 | name: {{ include "jxpipelines.fullname" . }} 20 | labels: {{- include "jxpipelines.labels" . | nindent 4 }} 21 | roleRef: 22 | apiGroup: rbac.authorization.k8s.io 23 | kind: ClusterRole 24 | name: {{ include "jxpipelines.fullname" . }} 25 | subjects: 26 | - kind: ServiceAccount 27 | name: {{ include "jxpipelines.fullname" . }} 28 | namespace: {{ .Release.Namespace }} -------------------------------------------------------------------------------- /charts/jx-pipelines-visualizer/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "jxpipelines.fullname" . }} 5 | labels: 6 | {{- include "jxpipelines.labels" . | nindent 4 }} 7 | {{- with .Values.service.labels }} 8 | {{ tpl (toYaml .) $ | trim | indent 4 }} 9 | {{- end }} 10 | {{- with .Values.service.annotations }} 11 | annotations: {{- tpl (toYaml .) $ | trim | nindent 4 }} 12 | {{- end }} 13 | spec: 14 | {{- with .Values.service.type }} 15 | type: {{ . }} 16 | {{- end }} 17 | {{- with .Values.service.loadBalancerIP }} 18 | loadBalancerIP: {{ . }} 19 | {{- end }} 20 | ports: 21 | - name: http 22 | port: {{ .Values.service.port }} 23 | targetPort: http 24 | selector: {{- include "jxpipelines.labels.selector" . | nindent 4 }} -------------------------------------------------------------------------------- /charts/jx-pipelines-visualizer/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for the Helm Chart 2 | 3 | fullnameOverride: 4 | nameOverride: 5 | gitSecretName: tekton-git 6 | 7 | config: 8 | # gs://BUCKET_NAME/jenkins-x/logs/{{.Owner}}/{{.Repository}}/{{if hasPrefix .Branch \"pr\"}}{{.Branch | upper}}{{else}}{{.Branch}}{{end}}/{{.Build}}.log 9 | archivedLogsURLTemplate: 10 | # gs://BUCKET_NAME/jenkins-x/logs/{{.Owner}}/{{.Repository}}/{{if hasPrefix .Branch \"pr\"}}{{.Branch | upper}}{{else}}{{.Branch}}{{end}}/{{.Build}}.yaml 11 | archivedPipelinesURLTemplate: 12 | # gs://BUCKET_NAME/jenkins-x/pipelineruns/{{.Namespace}}/{{.Name}}.yaml 13 | archivedPipelineRunsURLTemplate: 14 | # https://GRAFANA_URL/explore?left=%5B%22now%22,%22now%22,%22Tempo%22,%7B%22query%22:%22{{.TraceID}}%22%7D%5D 15 | pipelineTraceURLTemplate: 16 | 17 | # Set a fixed namespace if the visualizer should show pipelines only from selected namespace 18 | #namespace: jx 19 | resyncInterval: 60s 20 | logLevel: INFO 21 | 22 | image: 23 | repository: gcr.io/jenkinsxio/jx-pipelines-visualizer 24 | # If no tag, fallback to the Chart's AppVersion 25 | tag: 26 | pullPolicy: 27 | 28 | deployment: 29 | replicas: 1 30 | revisionHistoryLimit: 2 31 | labels: {} 32 | annotations: {} 33 | 34 | pod: 35 | resources: 36 | requests: 37 | cpu: "0.2" 38 | memory: 128M 39 | limits: 40 | cpu: "1" 41 | memory: 512M 42 | labels: {} 43 | annotations: {} 44 | activeDeadlineSeconds: 45 | enableServiceLinks: false 46 | terminationGracePeriodSeconds: 47 | affinity: {} 48 | nodeSelector: {} 49 | tolerations: [] 50 | hostAliases: [] 51 | schedulerName: 52 | securityContext: 53 | fsGroup: 1000 54 | env: {} 55 | envFrom: [] 56 | service: 57 | port: 80 58 | type: 59 | loadBalancerIP: 60 | labels: {} 61 | annotations: {} 62 | 63 | ingress: 64 | enabled: false 65 | class: nginx 66 | labels: {} 67 | annotations: {} 68 | 69 | apiVersion: "networking.k8s.io/v1beta1" 70 | pathType: "ImplementationSpecific" 71 | path: "" 72 | 73 | # hosts: 74 | # - pipelines.example.com 75 | # - pipelines.foo.bar 76 | hosts: [] 77 | 78 | # enables basic auth secret to be created 79 | basicAuth: 80 | enabled: false 81 | authData: "" 82 | 83 | tls: 84 | enabled: false 85 | 86 | # secrets: 87 | # embedded: 88 | # b64encodedCertificate: e30k 89 | # b64encodedCertificateKey: e30k 90 | # hosts: 91 | # - pipelines.example.com 92 | # existing-secret-name: {} 93 | # existing-secret-name-with-custom-hosts: 94 | # hosts: 95 | # - pipelines.foo.bar 96 | secrets: {} 97 | 98 | istio: 99 | enabled: false 100 | apiVersion: networking.istio.io/v1beta1 101 | gateway: jx-gateway 102 | 103 | jx: 104 | # whether to create a Release CRD when installing charts with Release CRDs included 105 | releaseCRD: true 106 | 107 | serviceAccount: 108 | # allow additional annotations to be added to the ServiceAccount 109 | # such as for workload identity on clouds 110 | annotations: {} 111 | 112 | role: 113 | rules: 114 | - apiGroups: 115 | - jenkins.io 116 | resources: 117 | - pipelineactivities 118 | - pipelinestructures 119 | verbs: 120 | - list 121 | - watch 122 | - get 123 | - apiGroups: 124 | - tekton.dev 125 | resources: 126 | - pipelineruns 127 | - pipelines 128 | verbs: 129 | - list 130 | - watch 131 | - get 132 | - apiGroups: 133 | - "" 134 | resources: 135 | - pods 136 | verbs: 137 | - list 138 | - watch 139 | - get 140 | - apiGroups: 141 | - "" 142 | resources: 143 | - pods/log 144 | verbs: 145 | - get 146 | 147 | extraVolumes: [] 148 | # - name: config 149 | # configMap: 150 | # name: minio-certificate 151 | 152 | extraVolumeMounts: [] 153 | # - name: config 154 | # mountPath: /config 155 | # readOnly: true 156 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | v1 "github.com/jenkins-x/jx-api/v4/pkg/client/clientset/versioned/typed/jenkins.io/v1" 9 | "net/http" 10 | "time" 11 | 12 | visualizer "github.com/jenkins-x/jx-pipelines-visualizer" 13 | "github.com/jenkins-x/jx-pipelines-visualizer/internal/kube" 14 | "github.com/jenkins-x/jx-pipelines-visualizer/internal/version" 15 | "github.com/jenkins-x/jx-pipelines-visualizer/web/handlers" 16 | 17 | jxclientset "github.com/jenkins-x/jx-api/v4/pkg/client/clientset/versioned" 18 | "github.com/sirupsen/logrus" 19 | ) 20 | 21 | var ( 22 | options struct { 23 | namespace string 24 | defaultJXNamespace string 25 | resyncInterval time.Duration 26 | archivedLogsURLTemplate string 27 | archivedPipelinesURLTemplate string 28 | archivedPipelineRunsURLTemplate string 29 | pipelineTraceURLTemplate string 30 | kubeConfigPath string 31 | listenAddr string 32 | logLevel string 33 | printVersion bool 34 | } 35 | ) 36 | 37 | func init() { 38 | flag.StringVar(&options.namespace, "namespace", "", "Name of the jx namespace") 39 | flag.StringVar(&options.defaultJXNamespace, "default-jx-namespace", "jx", "Default Jenkins X installation namespace") 40 | flag.DurationVar(&options.resyncInterval, "resync-interval", 1*time.Minute, "Resync interval between full re-list operations") 41 | flag.StringVar(&options.archivedLogsURLTemplate, "archived-logs-url-template", "", "Go template string used to build the archived logs URL") 42 | flag.StringVar(&options.archivedPipelinesURLTemplate, "archived-pipelines-url-template", "", "Go template string used to build the archived pipelines URL") 43 | flag.StringVar(&options.archivedPipelineRunsURLTemplate, "archived-pipelineruns-url-template", "", "Go template string used to build the archived pipelineruns URL") 44 | flag.StringVar(&options.pipelineTraceURLTemplate, "pipeline-trace-url-template", "", "Go template string used to build the pipeline trace URL") 45 | flag.StringVar(&options.logLevel, "log-level", "INFO", "Log level - one of: trace, debug, info, warn(ing), error, fatal or panic") 46 | flag.StringVar(&options.kubeConfigPath, "kubeconfig", kube.DefaultKubeConfigPath(), "Kubernetes Config Path. Default: KUBECONFIG env var value") 47 | flag.StringVar(&options.listenAddr, "listen-addr", ":8080", "Address on which the server will listen for incoming connections") 48 | flag.BoolVar(&options.printVersion, "version", false, "Print the version") 49 | } 50 | 51 | func main() { 52 | flag.Parse() 53 | 54 | if options.printVersion { 55 | fmt.Printf("Version %s - Revision %s - Date %s", version.Version, version.Revision, version.Date) 56 | return 57 | } 58 | 59 | ctx, cancelFunc := context.WithCancel(context.Background()) 60 | defer cancelFunc() 61 | 62 | logger := logrus.New() 63 | logLevel, err := logrus.ParseLevel(options.logLevel) 64 | if err != nil { 65 | logger.WithField("logLevel", options.logLevel).Error("Failed to set log level") 66 | } else { 67 | logger.SetLevel(logLevel) 68 | } 69 | logger.WithField("logLevel", logLevel).Info("Starting") 70 | 71 | kClient, err := kube.NewClient(options.kubeConfigPath) 72 | if err != nil { 73 | logger.WithError(err).Fatal("failed to create a Kubernetes client") 74 | } 75 | jxClient, err := jxclientset.NewForConfig(kClient.Config) 76 | if err != nil { 77 | logger.WithError(err).Fatal("failed to create a Jenkins X client") 78 | } 79 | 80 | store, err := visualizer.NewStore() 81 | if err != nil { 82 | logger.WithError(err).Fatal("failed to create a new store") 83 | } 84 | 85 | runningPipelines := new(visualizer.RunningPipelines) 86 | 87 | logger.WithField("namespace", options.namespace).WithField("resyncInterval", options.resyncInterval).Info("Starting Informer") 88 | (&visualizer.Informer{ 89 | JXClient: jxClient, 90 | Namespace: options.namespace, 91 | ResyncInterval: options.resyncInterval, 92 | Store: store, 93 | RunningPipelines: runningPipelines, 94 | Logger: logger, 95 | }).Start(ctx) 96 | 97 | handler, err := handlers.Router{ 98 | Store: store, 99 | RunningPipelines: runningPipelines, 100 | KConfig: kClient.Config, 101 | PAInterfaceFactory: func(namespace string) v1.PipelineActivityInterface { 102 | return jxClient.JenkinsV1().PipelineActivities(namespace) 103 | }, 104 | Namespace: options.namespace, 105 | DefaultJXNamespace: options.defaultJXNamespace, 106 | ArchivedLogsURLTemplate: options.archivedLogsURLTemplate, 107 | ArchivedPipelinesURLTemplate: options.archivedPipelinesURLTemplate, 108 | ArchivedPipelineRunsURLTemplate: options.archivedPipelineRunsURLTemplate, 109 | PipelineTraceURLTemplate: options.pipelineTraceURLTemplate, 110 | Logger: logger, 111 | }.Handler() 112 | if err != nil { 113 | logger.WithError(err).Fatal("failed to initialize the HTTP handler") 114 | } 115 | http.Handle("/", handler) 116 | 117 | logger.WithField("listenAddr", options.listenAddr).Info("Starting HTTP Server") 118 | err = http.ListenAndServe(options.listenAddr, nil) 119 | if !errors.Is(err, http.ErrServerClosed) { 120 | logger.WithError(err).Fatal("failed to start HTTP server") 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /docs/screenshots/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkins-x/jx-pipelines-visualizer/9ffad52c64afa3bc49ff2d6d9ecc34a54e1261db/docs/screenshots/home.png -------------------------------------------------------------------------------- /docs/screenshots/pipeline-success-with-timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkins-x/jx-pipelines-visualizer/9ffad52c64afa3bc49ff2d6d9ecc34a54e1261db/docs/screenshots/pipeline-success-with-timeline.png -------------------------------------------------------------------------------- /docs/screenshots/pipeline-success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkins-x/jx-pipelines-visualizer/9ffad52c64afa3bc49ff2d6d9ecc34a54e1261db/docs/screenshots/pipeline-success.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jenkins-x/jx-pipelines-visualizer 2 | 3 | require ( 4 | github.com/Masterminds/sprig/v3 v3.3.0 5 | github.com/blevesearch/bleve/v2 v2.4.4 6 | github.com/gorilla/mux v1.8.1 7 | github.com/jenkins-x-plugins/jx-pipeline v0.7.16 8 | github.com/jenkins-x/jx-api/v4 v4.7.9 9 | github.com/jenkins-x/jx-helpers/v3 v3.9.1 10 | github.com/mitchellh/go-homedir v1.1.0 11 | github.com/rickb777/date v1.21.1 12 | github.com/rs/xid v1.6.0 13 | github.com/sirupsen/logrus v1.9.3 14 | github.com/subchord/go-sse v1.0.7 15 | github.com/tektoncd/pipeline v0.66.0 16 | github.com/unrolled/render v1.7.0 17 | github.com/urfave/negroni/v2 v2.0.2 18 | gopkg.in/yaml.v2 v2.4.0 19 | k8s.io/apimachinery v0.32.1 20 | k8s.io/cli-runtime v0.32.1 21 | k8s.io/client-go v0.32.1 22 | ) 23 | 24 | require ( 25 | cloud.google.com/go v0.115.0 // indirect 26 | cloud.google.com/go/auth v0.6.1 // indirect 27 | cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect 28 | cloud.google.com/go/compute/metadata v0.5.0 // indirect 29 | cloud.google.com/go/iam v1.1.8 // indirect 30 | cloud.google.com/go/storage v1.42.0 // indirect 31 | code.gitea.io/sdk/gitea v0.18.0 // indirect 32 | contrib.go.opencensus.io/exporter/ocagent v0.7.1-0.20200907061046-05415f1de66d // indirect 33 | contrib.go.opencensus.io/exporter/prometheus v0.4.2 // indirect 34 | dario.cat/mergo v1.0.1 // indirect 35 | fortio.org/safecast v1.0.0 // indirect 36 | github.com/AlecAivazis/survey/v2 v2.3.7 // indirect 37 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 // indirect 38 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0 // indirect 39 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 // indirect 40 | github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2 // indirect 41 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 42 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 43 | github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect 44 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect 45 | github.com/Masterminds/goutils v1.1.1 // indirect 46 | github.com/Masterminds/semver/v3 v3.3.1 // indirect 47 | github.com/RoaringBitmap/roaring v1.9.4 // indirect 48 | github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 49 | github.com/aws/aws-sdk-go v1.54.13 // indirect 50 | github.com/aws/aws-sdk-go-v2 v1.30.1 // indirect 51 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect 52 | github.com/aws/aws-sdk-go-v2/config v1.27.23 // indirect 53 | github.com/aws/aws-sdk-go-v2/credentials v1.17.23 // indirect 54 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.9 // indirect 55 | github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.4 // indirect 56 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.13 // indirect 57 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.13 // indirect 58 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect 59 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.13 // indirect 60 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect 61 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.15 // indirect 62 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.15 // indirect 63 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.13 // indirect 64 | github.com/aws/aws-sdk-go-v2/service/s3 v1.58.0 // indirect 65 | github.com/aws/aws-sdk-go-v2/service/sso v1.22.1 // indirect 66 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.1 // indirect 67 | github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 // indirect 68 | github.com/aws/smithy-go v1.20.3 // indirect 69 | github.com/beorn7/perks v1.0.1 // indirect 70 | github.com/bits-and-blooms/bitset v1.20.0 // indirect 71 | github.com/blendle/zapdriver v1.3.1 // indirect 72 | github.com/blevesearch/bleve_index_api v1.1.12 // indirect 73 | github.com/blevesearch/geo v0.1.20 // indirect 74 | github.com/blevesearch/go-faiss v1.0.24 // indirect 75 | github.com/blevesearch/go-porterstemmer v1.0.3 // indirect 76 | github.com/blevesearch/gtreap v0.1.1 // indirect 77 | github.com/blevesearch/mmap-go v1.0.4 // indirect 78 | github.com/blevesearch/scorch_segment_api/v2 v2.2.16 // indirect 79 | github.com/blevesearch/segment v0.9.1 // indirect 80 | github.com/blevesearch/snowballstem v0.9.0 // indirect 81 | github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect 82 | github.com/blevesearch/vellum v1.0.10 // indirect 83 | github.com/blevesearch/zapx/v11 v11.3.10 // indirect 84 | github.com/blevesearch/zapx/v12 v12.3.10 // indirect 85 | github.com/blevesearch/zapx/v13 v13.3.10 // indirect 86 | github.com/blevesearch/zapx/v14 v14.3.10 // indirect 87 | github.com/blevesearch/zapx/v15 v15.3.16 // indirect 88 | github.com/blevesearch/zapx/v16 v16.1.9-0.20241217210638-a0519e7caf3b // indirect 89 | github.com/bluekeyes/go-gitdiff v0.8.0 // indirect 90 | github.com/cenkalti/backoff v2.2.1+incompatible // indirect 91 | github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect 92 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 93 | github.com/cyphar/filepath-securejoin v0.4.0 // indirect 94 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 95 | github.com/davidmz/go-pageant v1.0.2 // indirect 96 | github.com/emicklei/go-restful/v3 v3.12.1 // indirect 97 | github.com/evanphx/json-patch/v5 v5.9.0 // indirect 98 | github.com/fatih/color v1.18.0 // indirect 99 | github.com/felixge/httpsnoop v1.0.4 // indirect 100 | github.com/fsnotify/fsnotify v1.8.0 // indirect 101 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 102 | github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 // indirect 103 | github.com/go-errors/errors v1.5.1 // indirect 104 | github.com/go-fed/httpsig v1.1.0 // indirect 105 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 106 | github.com/go-git/go-billy/v5 v5.6.2 // indirect 107 | github.com/go-git/go-git/v5 v5.13.1 // indirect 108 | github.com/go-kit/log v0.2.1 // indirect 109 | github.com/go-logfmt/logfmt v0.5.1 // indirect 110 | github.com/go-logr/logr v1.4.2 // indirect 111 | github.com/go-logr/stdr v1.2.2 // indirect 112 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 113 | github.com/go-openapi/jsonreference v0.21.0 // indirect 114 | github.com/go-openapi/swag v0.23.0 // indirect 115 | github.com/go-stack/stack v1.8.1 // indirect 116 | github.com/gogo/protobuf v1.3.2 // indirect 117 | github.com/golang-jwt/jwt/v5 v5.2.1 // indirect 118 | github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect 119 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 120 | github.com/golang/protobuf v1.5.4 // indirect 121 | github.com/golang/snappy v0.0.4 // indirect 122 | github.com/google/cel-go v0.20.1 // indirect 123 | github.com/google/gnostic-models v0.6.9 // indirect 124 | github.com/google/go-cmp v0.6.0 // indirect 125 | github.com/google/gofuzz v1.2.0 // indirect 126 | github.com/google/s2a-go v0.1.7 // indirect 127 | github.com/google/uuid v1.6.0 // indirect 128 | github.com/google/wire v0.6.0 // indirect 129 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect 130 | github.com/googleapis/gax-go/v2 v2.12.5 // indirect 131 | github.com/gorilla/websocket v1.5.1 // indirect 132 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect 133 | github.com/hashicorp/errwrap v1.1.0 // indirect 134 | github.com/hashicorp/go-multierror v1.1.1 // indirect 135 | github.com/hashicorp/go-version v1.7.0 // indirect 136 | github.com/huandu/xstrings v1.5.0 // indirect 137 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 138 | github.com/jenkins-x/go-scm v1.14.53 // indirect 139 | github.com/jenkins-x/jx-kube-client/v3 v3.0.8 // indirect 140 | github.com/jenkins-x/jx-logging/v3 v3.0.17 // indirect 141 | github.com/jenkins-x/logrus-stackdriver-formatter v0.2.7 // indirect 142 | github.com/jmespath/go-jmespath v0.4.0 // indirect 143 | github.com/josharian/intern v1.0.0 // indirect 144 | github.com/json-iterator/go v1.1.12 // indirect 145 | github.com/kylelemons/godebug v1.1.0 // indirect 146 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 147 | github.com/mailru/easyjson v0.9.0 // indirect 148 | github.com/mattn/go-colorable v0.1.14 // indirect 149 | github.com/mattn/go-isatty v0.0.20 // indirect 150 | github.com/mitchellh/copystructure v1.2.0 // indirect 151 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 152 | github.com/mmcloughlin/avo v0.6.0 // indirect 153 | github.com/moby/spdystream v0.5.0 // indirect 154 | github.com/moby/term v0.5.0 // indirect 155 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 156 | github.com/modern-go/reflect2 v1.0.2 // indirect 157 | github.com/mschoch/smat v0.2.0 // indirect 158 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 159 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 160 | github.com/pjbgf/sha1cd v0.3.1 // indirect 161 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 162 | github.com/pkg/errors v0.9.1 // indirect 163 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 164 | github.com/prometheus/client_golang v1.19.1 // indirect 165 | github.com/prometheus/client_model v0.6.1 // indirect 166 | github.com/prometheus/common v0.54.0 // indirect 167 | github.com/prometheus/procfs v0.15.1 // indirect 168 | github.com/prometheus/statsd_exporter v0.22.7 // indirect 169 | github.com/rawlingsj/jsonschema v0.0.0-20210511142122-a9c2cfdb7dcf // indirect 170 | github.com/rickb777/plural v1.4.2 // indirect 171 | github.com/shopspring/decimal v1.4.0 // indirect 172 | github.com/shurcooL/githubv4 v0.0.0-20191102174205-af46314aec7b // indirect 173 | github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect 174 | github.com/spf13/cast v1.7.1 // indirect 175 | github.com/spf13/cobra v1.8.1 // indirect 176 | github.com/spf13/pflag v1.0.5 // indirect 177 | github.com/stoewer/go-strcase v1.2.0 // indirect 178 | github.com/stretchr/testify v1.10.0 // indirect 179 | github.com/x448/float16 v0.8.4 // indirect 180 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 181 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 182 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 183 | go.etcd.io/bbolt v1.3.11 // indirect 184 | go.opencensus.io v0.24.0 // indirect 185 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.52.0 // indirect 186 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect 187 | go.opentelemetry.io/otel v1.28.0 // indirect 188 | go.opentelemetry.io/otel/metric v1.28.0 // indirect 189 | go.opentelemetry.io/otel/trace v1.28.0 // indirect 190 | go.uber.org/multierr v1.11.0 // indirect 191 | go.uber.org/zap v1.27.0 // indirect 192 | gocloud.dev v0.37.0 // indirect 193 | golang.org/x/crypto v0.32.0 // indirect 194 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 195 | golang.org/x/mod v0.22.0 // indirect 196 | golang.org/x/net v0.34.0 // indirect 197 | golang.org/x/oauth2 v0.25.0 // indirect 198 | golang.org/x/sync v0.10.0 // indirect 199 | golang.org/x/sys v0.29.0 // indirect 200 | golang.org/x/term v0.28.0 // indirect 201 | golang.org/x/text v0.21.0 // indirect 202 | golang.org/x/time v0.9.0 // indirect 203 | golang.org/x/tools v0.29.0 // indirect 204 | golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 205 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 206 | google.golang.org/api v0.187.0 // indirect 207 | google.golang.org/genproto v0.0.0-20240624140628-dc46fd24d27d // indirect 208 | google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect 209 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect 210 | google.golang.org/grpc v1.67.0 // indirect 211 | google.golang.org/protobuf v1.36.3 // indirect 212 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 213 | gopkg.in/inf.v0 v0.9.1 // indirect 214 | gopkg.in/warnings.v0 v0.1.2 // indirect 215 | gopkg.in/yaml.v3 v3.0.1 // indirect 216 | k8s.io/api v0.32.1 // indirect 217 | k8s.io/klog/v2 v2.130.1 // indirect 218 | k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect 219 | k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect 220 | knative.dev/pkg v0.0.0-20240416145024-0f34a8815650 // indirect 221 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 222 | sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect 223 | sigs.k8s.io/yaml v1.4.0 // indirect 224 | ) 225 | 226 | go 1.23.0 227 | -------------------------------------------------------------------------------- /hack/changelog-header.md: -------------------------------------------------------------------------------- 1 | ### Linux 2 | 3 | ```shell 4 | curl -L https://github.com/jenkins-x/jx-pipelines-visualizer/releases/download/v{{.Version}}/jx-pipelines-visualizer-linux-amd64.tar.gz | tar xzv 5 | sudo mv jx-pipelines-visualizer /usr/local/bin 6 | ``` 7 | 8 | ### macOS 9 | 10 | ```shell 11 | curl -L https://github.com/jenkins-x/jx-pipelines-visualizer/releases/download/v{{.Version}}/jx-pipelines-visualizer-darwin-amd64.tar.gz | tar xzv 12 | sudo mv jx-pipelines-visualizer /usr/local/bin 13 | ``` 14 | -------------------------------------------------------------------------------- /informer.go: -------------------------------------------------------------------------------- 1 | package visualizer 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | 8 | jenkinsv1 "github.com/jenkins-x/jx-api/v4/pkg/apis/jenkins.io/v1" 9 | jxclientset "github.com/jenkins-x/jx-api/v4/pkg/client/clientset/versioned" 10 | informers "github.com/jenkins-x/jx-api/v4/pkg/client/informers/externalversions" 11 | "github.com/sirupsen/logrus" 12 | "k8s.io/client-go/tools/cache" 13 | ) 14 | 15 | type Informer struct { 16 | JXClient *jxclientset.Clientset 17 | Namespace string 18 | ResyncInterval time.Duration 19 | Store *Store 20 | RunningPipelines *RunningPipelines 21 | Logger *logrus.Logger 22 | } 23 | 24 | func (i *Informer) Start(ctx context.Context) { 25 | informerFactory := informers.NewSharedInformerFactoryWithOptions( 26 | i.JXClient, 27 | i.ResyncInterval, 28 | informers.WithNamespace(i.Namespace), 29 | ) 30 | 31 | informerFactory.Jenkins().V1().PipelineActivities().Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ 32 | AddFunc: func(obj interface{}) { 33 | pa, ok := obj.(*jenkinsv1.PipelineActivity) 34 | if !ok { 35 | return 36 | } 37 | 38 | i.indexPipelineActivity(pa, "index") 39 | i.RunningPipelines.Add(pa) 40 | }, 41 | UpdateFunc: func(old, new interface{}) { 42 | pa, ok := new.(*jenkinsv1.PipelineActivity) 43 | if !ok { 44 | return 45 | } 46 | 47 | i.indexPipelineActivity(pa, "re-index") 48 | i.RunningPipelines.Add(pa) 49 | }, 50 | DeleteFunc: func(obj interface{}) { 51 | pa, ok := obj.(*jenkinsv1.PipelineActivity) 52 | if !ok { 53 | return 54 | } 55 | 56 | if i.Logger != nil && i.Logger.IsLevelEnabled(logrus.DebugLevel) { 57 | i.Logger.WithField("PipelineActivity", pa.Name).Debug("Deleting PipelineActivity") 58 | } 59 | err := i.Store.Delete(pa.Name) 60 | if err != nil && i.Logger != nil { 61 | i.Logger.WithError(err).WithField("PipelineActivity", pa.Name).Error("failed to delete PipelineActivity") 62 | } 63 | i.RunningPipelines.Add(pa) 64 | }, 65 | }) 66 | 67 | informerFactory.Start(ctx.Done()) 68 | } 69 | 70 | func (i *Informer) indexPipelineActivity(pa *jenkinsv1.PipelineActivity, operation string) { 71 | if isJenkinsPipelineActivity(pa) { 72 | if i.Logger != nil && i.Logger.IsLevelEnabled(logrus.DebugLevel) { 73 | i.Logger.WithField("PipelineActivity", pa.Name).Debug("Ignoring PipelineActivity created by Jenkins") 74 | } 75 | return 76 | } 77 | 78 | if i.Logger != nil && i.Logger.IsLevelEnabled(logrus.DebugLevel) { 79 | i.Logger.WithField("PipelineActivity", pa.Name).Debugf("%sing new PipelineActivity", strings.Title(operation)) 80 | } 81 | p := PipelineFromPipelineActivity(pa) 82 | err := i.Store.Add(p) 83 | if err != nil && i.Logger != nil { 84 | i.Logger.WithError(err).WithField("PipelineActivity", pa.Name).Errorf("failed to %s new PipelineActivity", operation) 85 | } 86 | } 87 | 88 | // isJenkinsPipelineActivity returns true if the given PipelineActivity has been created by Jenkins 89 | // see https://github.com/jenkinsci/jx-resources-plugin/blob/master/src/main/java/org/jenkinsci/plugins/jx/resources/BuildSyncRunListener.java#L106 90 | func isJenkinsPipelineActivity(pa *jenkinsv1.PipelineActivity) bool { 91 | if strings.Contains(pa.Spec.BuildURL, "/blue/organizations/jenkins/") { 92 | return true 93 | } 94 | if strings.Contains(pa.Spec.BuildLogsURL, "/job/") && strings.HasSuffix(pa.Spec.BuildLogsURL, "/console") { 95 | return true 96 | } 97 | return false 98 | } 99 | -------------------------------------------------------------------------------- /internal/kube/client.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "fmt" 5 | 6 | "k8s.io/client-go/kubernetes" 7 | "k8s.io/client-go/rest" 8 | ) 9 | 10 | type Client struct { 11 | Config *rest.Config 12 | Clientset *kubernetes.Clientset 13 | } 14 | 15 | func NewClient(kubeConfigPath string) (*Client, error) { 16 | config, err := NewConfig(kubeConfigPath) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | clientSet, err := kubernetes.NewForConfig(config) 22 | if err != nil { 23 | return nil, fmt.Errorf("failed to build a kube clientset: %w", err) 24 | } 25 | 26 | return &Client{ 27 | Config: config, 28 | Clientset: clientSet, 29 | }, nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/kube/config.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/mitchellh/go-homedir" 9 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 10 | "k8s.io/client-go/rest" 11 | "k8s.io/client-go/tools/clientcmd" 12 | ) 13 | 14 | func NewConfig(kubeConfigPath string) (*rest.Config, error) { 15 | // first, let's try to see if we are running in a pod in a cluster 16 | config, err := rest.InClusterConfig() 17 | if err == nil { 18 | _ = rest.SetKubernetesDefaults(config) 19 | return config, nil 20 | } 21 | 22 | // otherwise, fallback to using our kubeconfig path 23 | config, err = clientcmd.BuildConfigFromFlags("", kubeConfigPath) 24 | if err != nil { 25 | return nil, fmt.Errorf("failed to build kube config from %s: %w", kubeConfigPath, err) 26 | } 27 | 28 | _ = rest.SetKubernetesDefaults(config) 29 | return config, nil 30 | } 31 | 32 | func DefaultKubeConfigPath() string { 33 | if kubeconfig := os.Getenv("KUBECONFIG"); len(kubeconfig) > 0 { 34 | return kubeconfig 35 | } 36 | 37 | home, _ := homedir.Dir() 38 | if len(home) > 0 { 39 | return filepath.Join(home, ".kube", "config") 40 | } 41 | 42 | wd, _ := os.Getwd() 43 | if len(wd) > 0 { 44 | return filepath.Join(wd, ".kube", "config") 45 | } 46 | 47 | return "" 48 | } 49 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | // these are set at compile time by GoReleaser through LD Flags 4 | var ( 5 | Version = "dev" 6 | Revision = "unknown" 7 | Date = "now" 8 | ) 9 | -------------------------------------------------------------------------------- /pipeline.go: -------------------------------------------------------------------------------- 1 | package visualizer 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | 7 | jenkinsv1 "github.com/jenkins-x/jx-api/v4/pkg/apis/jenkins.io/v1" 8 | ) 9 | 10 | type Pipeline struct { 11 | Name string 12 | Namespace string 13 | Provider string 14 | Owner string 15 | Repository string 16 | Branch string 17 | Build string 18 | Context string 19 | Author string 20 | AuthorAvatarURL string 21 | Commit string 22 | Status string 23 | Description string 24 | Start time.Time 25 | End time.Time 26 | Duration time.Duration 27 | GitUrl string 28 | } 29 | 30 | func (p Pipeline) PullRequestNumber() string { 31 | if strings.HasPrefix(p.Branch, "PR-") { 32 | return strings.TrimPrefix(p.Branch, "PR-") 33 | } 34 | return "" 35 | } 36 | 37 | func PipelineFromPipelineActivity(pa *jenkinsv1.PipelineActivity) Pipeline { 38 | p := Pipeline{ 39 | Name: pa.Name, 40 | Provider: pa.Labels["provider"], 41 | Owner: pa.Spec.GitOwner, 42 | Repository: pa.Spec.GitRepository, 43 | Branch: pa.Spec.GitBranch, 44 | Build: pa.Spec.Build, 45 | Context: getContext(pa), 46 | Author: pa.Spec.Author, 47 | AuthorAvatarURL: pa.Spec.AuthorAvatarURL, 48 | Commit: pa.Spec.LastCommitSHA, 49 | Status: string(pa.Spec.Status), 50 | Description: pa.Annotations["description"], 51 | Namespace: pa.Namespace, 52 | GitUrl: strings.TrimSuffix(pa.Spec.GitURL, ".git"), 53 | } 54 | if pa.Spec.StartedTimestamp != nil { 55 | p.Start = pa.Spec.StartedTimestamp.Time 56 | } else { 57 | p.Start = pa.CreationTimestamp.Time 58 | } 59 | if pa.Spec.CompletedTimestamp != nil { 60 | p.End = pa.Spec.CompletedTimestamp.Time 61 | } 62 | if !p.Start.IsZero() && !p.End.IsZero() { 63 | p.Duration = p.End.Sub(p.Start) 64 | } 65 | return p 66 | } 67 | 68 | func getContext(pa *jenkinsv1.PipelineActivity) string { 69 | if pa.Spec.Context != "" { 70 | return pa.Spec.Context 71 | } 72 | for _, label := range []string{"context", "lighthouse.jenkins-x.io/context"} { 73 | if pipelineContext, found := pa.Labels[label]; found { 74 | return pipelineContext 75 | } 76 | } 77 | return "" 78 | } 79 | -------------------------------------------------------------------------------- /pipeline_running.go: -------------------------------------------------------------------------------- 1 | package visualizer 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | jenkinsv1 "github.com/jenkins-x/jx-api/v4/pkg/apis/jenkins.io/v1" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type RunningPipeline struct { 14 | Pipeline 15 | Stage string 16 | StageStartTime time.Time 17 | Step string 18 | StepStartTime time.Time 19 | } 20 | 21 | func (running RunningPipeline) String() string { 22 | return fmt.Sprintf("%s/%s/%s", running.Name, running.Stage, running.Step) 23 | } 24 | 25 | func (running RunningPipeline) JSON() string { 26 | data, err := json.Marshal(running) 27 | if err != nil { 28 | return "" 29 | } 30 | return string(data) 31 | } 32 | 33 | type Watcher struct { 34 | Name string 35 | Added chan RunningPipeline 36 | Deleted chan RunningPipeline 37 | } 38 | 39 | type RunningPipelines struct { 40 | Logger *logrus.Logger 41 | running sync.Map 42 | watchers sync.Map 43 | } 44 | 45 | func (pipelines *RunningPipelines) Add(pa *jenkinsv1.PipelineActivity) { 46 | if pa == nil { 47 | return 48 | } 49 | 50 | runnings := pipelines.getForActivity(pa) 51 | if len(runnings) == 0 { 52 | if pa.Spec.Status.IsTerminated() { 53 | return 54 | } else { 55 | runnings = RunningPipelinesFromPipelineActivity(pa) 56 | for _, running := range runnings { 57 | pipelines.running.Store(running.String(), running) 58 | pipelines.onRunningPipelineAdded(running) 59 | } 60 | return 61 | } 62 | } 63 | 64 | if pa.Spec.Status.IsTerminated() { 65 | for _, running := range runnings { 66 | pipelines.running.Delete(running.String()) 67 | pipelines.onRunningPipelineDeleted(running) 68 | } 69 | return 70 | } 71 | 72 | // delete runnings which are finished 73 | for _, running := range runnings { 74 | for _, stage := range pa.Spec.Steps { 75 | if stage.Stage != nil && stage.Stage.Name == running.Stage { 76 | for _, step := range stage.Stage.Steps { 77 | if step.Name == running.Step { 78 | if step.Status.IsTerminated() { 79 | pipelines.running.Delete(running.String()) 80 | pipelines.onRunningPipelineDeleted(running) 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | 88 | currentlyRunnings := RunningPipelinesFromPipelineActivity(pa) 89 | for _, currentlyRunning := range currentlyRunnings { 90 | var alreadyRunning bool 91 | for _, running := range runnings { 92 | if running.String() == currentlyRunning.String() { 93 | alreadyRunning = true 94 | break 95 | } 96 | } 97 | if !alreadyRunning { 98 | pipelines.running.Store(currentlyRunning.String(), currentlyRunning) 99 | pipelines.onRunningPipelineAdded(currentlyRunning) 100 | } 101 | } 102 | } 103 | 104 | func (pipelines *RunningPipelines) Get() []RunningPipeline { 105 | var runnings []RunningPipeline 106 | pipelines.running.Range(func(key, value interface{}) bool { 107 | if running, ok := value.(RunningPipeline); ok { 108 | runnings = append(runnings, running) 109 | } 110 | return true 111 | }) 112 | return runnings 113 | } 114 | 115 | func (pipelines *RunningPipelines) getForActivity(pa *jenkinsv1.PipelineActivity) []RunningPipeline { 116 | var runnings []RunningPipeline 117 | pipelines.running.Range(func(key, value interface{}) bool { 118 | if running, ok := value.(RunningPipeline); ok { 119 | if running.Name == pa.Name { 120 | runnings = append(runnings, running) 121 | } 122 | } 123 | return true 124 | }) 125 | return runnings 126 | } 127 | 128 | func (pipelines *RunningPipelines) onRunningPipelineAdded(running RunningPipeline) { 129 | pipelines.watchers.Range(func(key, value interface{}) bool { 130 | if watcher, ok := value.(Watcher); ok { 131 | go func() { 132 | defer func() { 133 | if r := recover(); r != nil { 134 | pipelines.Logger.WithField("msg", r).Error("Panic when writing to channel") 135 | } 136 | }() 137 | watcher.Added <- running 138 | }() 139 | } 140 | return true 141 | }) 142 | } 143 | 144 | func (pipelines *RunningPipelines) onRunningPipelineDeleted(running RunningPipeline) { 145 | pipelines.watchers.Range(func(key, value interface{}) bool { 146 | if watcher, ok := value.(Watcher); ok { 147 | go func() { 148 | defer func() { 149 | if r := recover(); r != nil { 150 | pipelines.Logger.WithField("msg", r).Error("Panic when writing to channel") 151 | } 152 | }() 153 | watcher.Deleted <- running 154 | }() 155 | } 156 | return true 157 | }) 158 | } 159 | 160 | func (pipelines *RunningPipelines) Register(watcher Watcher) { 161 | pipelines.watchers.Store(watcher.Name, watcher) 162 | } 163 | 164 | func (pipelines *RunningPipelines) UnRegister(watcher Watcher) { 165 | pipelines.watchers.Delete(watcher.Name) 166 | } 167 | 168 | func RunningPipelinesFromPipelineActivity(pa *jenkinsv1.PipelineActivity) []RunningPipeline { 169 | var runnings []RunningPipeline 170 | for _, stage := range pa.Spec.Steps { 171 | if stage.Stage != nil { 172 | for _, step := range stage.Stage.Steps { 173 | if step.Status == jenkinsv1.ActivityStatusTypeRunning { 174 | running := RunningPipeline{ 175 | Pipeline: PipelineFromPipelineActivity(pa), 176 | Stage: stage.Stage.Name, 177 | StageStartTime: stage.Stage.StartedTimestamp.Time, 178 | Step: step.Name, 179 | StepStartTime: step.StartedTimestamp.Time, 180 | } 181 | runnings = append(runnings, running) 182 | } 183 | } 184 | } 185 | } 186 | return runnings 187 | } 188 | -------------------------------------------------------------------------------- /promote.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "promoting the new version ${VERSION} to downstream repositories" 4 | 5 | 6 | -------------------------------------------------------------------------------- /store.go: -------------------------------------------------------------------------------- 1 | package visualizer 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/blevesearch/bleve/v2" 9 | "github.com/blevesearch/bleve/v2/analysis/analyzer/keyword" 10 | "github.com/blevesearch/bleve/v2/search" 11 | "github.com/blevesearch/bleve/v2/search/query" 12 | ) 13 | 14 | type Store struct { 15 | Index bleve.Index 16 | } 17 | 18 | func NewStore() (*Store, error) { 19 | keywordFieldMapping := bleve.NewTextFieldMapping() 20 | keywordFieldMapping.Analyzer = keyword.Name 21 | 22 | indexMapping := bleve.NewIndexMapping() 23 | indexMapping.DefaultAnalyzer = keyword.Name 24 | 25 | startFieldMapping := bleve.NewDateTimeFieldMapping() 26 | startFieldMapping.Name = "Start" 27 | indexMapping.DefaultMapping.AddFieldMapping(startFieldMapping) 28 | indexMapping.DefaultMapping.AddFieldMappingsAt("End", bleve.NewDateTimeFieldMapping()) 29 | 30 | index, err := bleve.NewMemOnly(indexMapping) 31 | if err != nil { 32 | return nil, fmt.Errorf("failed to created a Bleve in-memory Index: %w", err) 33 | } 34 | 35 | store := &Store{ 36 | Index: index, 37 | } 38 | return store, nil 39 | } 40 | 41 | func (s *Store) Add(p Pipeline) error { 42 | return s.Index.Index(p.Name, p) 43 | } 44 | 45 | func (s *Store) Delete(name string) error { 46 | return s.Index.Delete(name) 47 | } 48 | 49 | type Query struct { 50 | Owner string 51 | Repository string 52 | Branch string 53 | Query string 54 | } 55 | 56 | type Pipelines struct { 57 | Pipelines []Pipeline 58 | Counts struct { 59 | Statuses map[string]int 60 | Repositories map[string]int 61 | Authors map[string]int 62 | Durations map[string]int 63 | } 64 | } 65 | 66 | func (s *Store) All() (*Pipelines, error) { 67 | request := bleve.NewSearchRequest(bleve.NewMatchAllQuery()) 68 | request.SortBy([]string{"-Start"}) 69 | addFacetRequests(request) 70 | request.Size = 10000 71 | request.Fields = []string{"*"} 72 | result, err := s.Index.Search(request) 73 | if err != nil { 74 | return nil, fmt.Errorf("failed to search all: %w", err) 75 | } 76 | 77 | pipelines := bleveResultToPipelines(result) 78 | return &pipelines, nil 79 | } 80 | 81 | func (s *Store) Query(q Query) (*Pipelines, error) { 82 | request := bleve.NewSearchRequest(q.ToBleveQuery()) 83 | request.SortBy([]string{"-Start"}) 84 | addFacetRequests(request) 85 | request.Size = 10000 86 | request.Fields = []string{"*"} 87 | result, err := s.Index.Search(request) 88 | if err != nil { 89 | return nil, fmt.Errorf("failed to search for %v: %w", q, err) 90 | } 91 | 92 | pipelines := bleveResultToPipelines(result) 93 | return &pipelines, nil 94 | } 95 | 96 | func (q Query) ToBleveQuery() query.Query { 97 | var queryString strings.Builder 98 | if len(q.Query) > 0 { 99 | queryString.WriteString("+") 100 | queryString.WriteString(q.Query) 101 | } 102 | if len(q.Owner) > 0 { 103 | if queryString.Len() > 0 { 104 | queryString.WriteString(" ") 105 | } 106 | queryString.WriteString("+Owner:") 107 | queryString.WriteString(q.Owner) 108 | } 109 | if len(q.Repository) > 0 { 110 | if queryString.Len() > 0 { 111 | queryString.WriteString(" ") 112 | } 113 | queryString.WriteString("+Repository:") 114 | queryString.WriteString(q.Repository) 115 | } 116 | if len(q.Branch) > 0 { 117 | if queryString.Len() > 0 { 118 | queryString.WriteString(" ") 119 | } 120 | queryString.WriteString("+Branch:") 121 | queryString.WriteString(q.Branch) 122 | } 123 | return bleve.NewQueryStringQuery(queryString.String()) 124 | } 125 | 126 | func bleveResultToPipelines(result *bleve.SearchResult) Pipelines { 127 | var pipelines Pipelines 128 | 129 | for _, doc := range result.Hits { 130 | pipeline := bleveDocToPipeline(doc) 131 | pipelines.Pipelines = append(pipelines.Pipelines, pipeline) 132 | } 133 | 134 | for _, facet := range result.Facets { 135 | counts := map[string]int{} 136 | for _, term := range facet.Terms.Terms() { 137 | counts[term.Term] = term.Count 138 | } 139 | for _, numericRange := range facet.NumericRanges { 140 | counts[numericRange.Name] = numericRange.Count 141 | } 142 | counts["Other"] = facet.Other 143 | switch facet.Field { 144 | case "Status": 145 | pipelines.Counts.Statuses = counts 146 | case "Repository": 147 | pipelines.Counts.Repositories = counts 148 | case "Author": 149 | pipelines.Counts.Authors = counts 150 | case "Duration": 151 | pipelines.Counts.Durations = counts 152 | } 153 | } 154 | 155 | return pipelines 156 | } 157 | 158 | func bleveDocToPipeline(doc *search.DocumentMatch) Pipeline { 159 | var ( 160 | startDate, endDate time.Time 161 | ) 162 | if start, ok := doc.Fields["Start"].(string); ok { 163 | startDate, _ = time.Parse(time.RFC3339, start) 164 | } 165 | if end, ok := doc.Fields["End"].(string); ok { 166 | endDate, _ = time.Parse(time.RFC3339, end) 167 | } 168 | return Pipeline{ 169 | Name: doc.Fields["Name"].(string), 170 | Namespace: doc.Fields["Namespace"].(string), 171 | Provider: doc.Fields["Provider"].(string), 172 | Owner: doc.Fields["Owner"].(string), 173 | Repository: doc.Fields["Repository"].(string), 174 | Branch: doc.Fields["Branch"].(string), 175 | Build: doc.Fields["Build"].(string), 176 | Context: doc.Fields["Context"].(string), 177 | Author: doc.Fields["Author"].(string), 178 | AuthorAvatarURL: doc.Fields["AuthorAvatarURL"].(string), 179 | Commit: doc.Fields["Commit"].(string), 180 | Status: doc.Fields["Status"].(string), 181 | Description: doc.Fields["Description"].(string), 182 | GitUrl: doc.Fields["GitUrl"].(string), 183 | Start: startDate, 184 | End: endDate, 185 | Duration: time.Duration(doc.Fields["Duration"].(float64)), 186 | } 187 | } 188 | 189 | func addFacetRequests(request *bleve.SearchRequest) { 190 | request.AddFacet("Status", bleve.NewFacetRequest("Status", 4)) 191 | request.AddFacet("Repository", bleve.NewFacetRequest("Repository", 3)) 192 | request.AddFacet("Author", bleve.NewFacetRequest("Author", 3)) 193 | durationFacet := bleve.NewFacetRequest("Duration", 4) 194 | durationFacet.AddNumericRange("< 5 min", nil, durationAsFloat64Ptr(5*time.Minute)) 195 | durationFacet.AddNumericRange("5-15 min", durationAsFloat64Ptr(5*time.Minute), durationAsFloat64Ptr(15*time.Minute)) 196 | durationFacet.AddNumericRange("15-30 min", durationAsFloat64Ptr(15*time.Minute), durationAsFloat64Ptr(30*time.Minute)) 197 | durationFacet.AddNumericRange("> 30 min", durationAsFloat64Ptr(30*time.Minute), nil) 198 | request.AddFacet("Duration", durationFacet) 199 | } 200 | 201 | func durationAsFloat64Ptr(d time.Duration) *float64 { 202 | nanos := float64(d.Nanoseconds()) 203 | return &nanos 204 | } 205 | -------------------------------------------------------------------------------- /web/handlers/branch.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | 7 | visualizer "github.com/jenkins-x/jx-pipelines-visualizer" 8 | 9 | "github.com/gorilla/mux" 10 | "github.com/sirupsen/logrus" 11 | "github.com/unrolled/render" 12 | ) 13 | 14 | type BranchHandler struct { 15 | Store *visualizer.Store 16 | Render *render.Render 17 | Logger *logrus.Logger 18 | } 19 | 20 | func (h *BranchHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 21 | vars := mux.Vars(r) 22 | owner := vars["owner"] 23 | repository := vars["repo"] 24 | branch := vars["branch"] 25 | if strings.HasPrefix(branch, "pr-") { 26 | branch = strings.ToUpper(branch) 27 | } 28 | 29 | pipelines, err := h.Store.Query(visualizer.Query{ 30 | Owner: owner, 31 | Repository: repository, 32 | Branch: branch, 33 | Query: r.URL.Query().Get("q"), 34 | }) 35 | if err != nil { 36 | http.Error(w, err.Error(), http.StatusInternalServerError) 37 | return 38 | } 39 | 40 | err = h.Render.HTML(w, http.StatusOK, "home", struct { 41 | Owner string 42 | Repository string 43 | Branch string 44 | Query string 45 | Pipelines *visualizer.Pipelines 46 | }{ 47 | owner, 48 | repository, 49 | branch, 50 | r.URL.Query().Get("q"), 51 | pipelines, 52 | }) 53 | if err != nil { 54 | http.Error(w, err.Error(), http.StatusInternalServerError) 55 | return 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /web/handlers/functions/app.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "github.com/jenkins-x/jx-pipelines-visualizer/internal/version" 5 | ) 6 | 7 | func AppVersion() string { 8 | return version.Version 9 | } 10 | -------------------------------------------------------------------------------- /web/handlers/functions/date.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/rickb777/date" 7 | "github.com/rickb777/date/view" 8 | ) 9 | 10 | func VDate(dateOrTime interface{}) view.VDate { 11 | switch d := dateOrTime.(type) { 12 | case time.Time: 13 | return view.NewVDate(date.NewAt(d)) 14 | case date.Date: 15 | return view.NewVDate(d) 16 | default: 17 | return view.NewVDate(date.Today()) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /web/handlers/functions/links.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "text/template" 7 | 8 | visualizer "github.com/jenkins-x/jx-pipelines-visualizer" 9 | 10 | jenkinsv1 "github.com/jenkins-x/jx-api/v4/pkg/apis/jenkins.io/v1" 11 | ) 12 | 13 | func TraceURLFunc(pipelineTraceURLTemplate *template.Template) func(string) string { 14 | return func(traceID string) string { 15 | return traceIDToTraceURL(traceID, pipelineTraceURLTemplate) 16 | } 17 | } 18 | 19 | func RepositoryURL(pipeline interface{}) string { 20 | switch p := pipeline.(type) { 21 | case visualizer.Pipeline: 22 | return repositoryURLForPipeline(p) 23 | case visualizer.RunningPipeline: 24 | return repositoryURLForPipeline(p.Pipeline) 25 | case *jenkinsv1.PipelineActivity: 26 | return repositoryURLForPipelineActivity(p) 27 | default: 28 | return "" 29 | } 30 | } 31 | 32 | func PullRequestURL(pipeline interface{}) string { 33 | switch p := pipeline.(type) { 34 | case visualizer.Pipeline: 35 | return pullRequestURLForPipeline(p) 36 | case visualizer.RunningPipeline: 37 | return pullRequestURLForPipeline(p.Pipeline) 38 | case *jenkinsv1.PipelineActivity: 39 | return pullRequestURLForPipelineActivity(p) 40 | default: 41 | return "" 42 | } 43 | } 44 | 45 | func BranchURL(pipeline interface{}) string { 46 | switch p := pipeline.(type) { 47 | case visualizer.Pipeline: 48 | return branchURLForPipeline(p) 49 | case visualizer.RunningPipeline: 50 | return branchURLForPipeline(p.Pipeline) 51 | case *jenkinsv1.PipelineActivity: 52 | return branchURLForPipelineActivity(p) 53 | default: 54 | return "" 55 | } 56 | } 57 | 58 | func CommitURL(pipeline interface{}) string { 59 | switch p := pipeline.(type) { 60 | case *jenkinsv1.PipelineActivity: 61 | return commitURLForPipelineActivity(p) 62 | default: 63 | return "" 64 | } 65 | } 66 | 67 | func AuthorURL(pipeline interface{}) string { 68 | switch p := pipeline.(type) { 69 | case visualizer.Pipeline: 70 | return authorURLForPipeline(p) 71 | case visualizer.RunningPipeline: 72 | return authorURLForPipeline(p.Pipeline) 73 | case *jenkinsv1.PipelineActivity: 74 | return authorURLForPipelineActivity(p) 75 | default: 76 | return "" 77 | } 78 | } 79 | 80 | func authorURLForPipeline(pipeline visualizer.Pipeline) string { 81 | switch pipeline.Provider { 82 | case "github": 83 | return fmt.Sprintf("https://github.com/%s", pipeline.Author) 84 | case "gitlab": 85 | return fmt.Sprintf("https://gitlab.com/%s", pipeline.Author) 86 | case "bitbucket": 87 | return fmt.Sprintf("https://bitbucket.org/%s", pipeline.Author) 88 | default: 89 | return "" 90 | } 91 | } 92 | 93 | func authorURLForPipelineActivity(pa *jenkinsv1.PipelineActivity) string { 94 | switch pipelineActivityProvider(pa) { 95 | case "github": 96 | return fmt.Sprintf("https://github.com/%s", pa.Spec.Author) 97 | case "gitlab": 98 | return fmt.Sprintf("https://gitlab.com/%s", pa.Spec.Author) 99 | case "bitbucket": 100 | return fmt.Sprintf("https://bitbucket.org/%s", pa.Spec.Author) 101 | default: 102 | return "" 103 | } 104 | } 105 | 106 | func repositoryURLForPipeline(pipeline visualizer.Pipeline) string { 107 | if pipeline.GitUrl != "" { 108 | return pipeline.GitUrl 109 | } 110 | 111 | switch pipeline.Provider { 112 | case "github": 113 | return fmt.Sprintf("https://github.com/%s/%s", pipeline.Owner, pipeline.Repository) 114 | case "gitlab": 115 | return fmt.Sprintf("https://gitlab.com/%s/%s", pipeline.Owner, pipeline.Repository) 116 | case "bitbucket": 117 | return fmt.Sprintf("https://bitbucket.org/%s/%s", pipeline.Owner, pipeline.Repository) 118 | default: 119 | return "" 120 | } 121 | } 122 | 123 | func repositoryURLForPipelineActivity(pa *jenkinsv1.PipelineActivity) string { 124 | switch pipelineActivityProvider(pa) { 125 | case "github": 126 | return fmt.Sprintf("https://github.com/%s/%s", pa.Spec.GitOwner, pa.Spec.GitRepository) 127 | case "gitlab": 128 | return fmt.Sprintf("https://gitlab.com/%s/%s", pa.Spec.GitOwner, pa.Spec.GitRepository) 129 | case "bitbucket": 130 | return fmt.Sprintf("https://bitbucket.org/%s/%s", pa.Spec.GitOwner, pa.Spec.GitRepository) 131 | default: 132 | return "" 133 | } 134 | } 135 | 136 | func pullRequestURLForPipeline(pipeline visualizer.Pipeline) string { 137 | if pipeline.PullRequestNumber() == "" { 138 | return "" // not a PR 139 | } 140 | switch pipeline.Provider { 141 | case "github": 142 | return fmt.Sprintf("https://github.com/%s/%s/pull/%s", pipeline.Owner, pipeline.Repository, pipeline.PullRequestNumber()) 143 | case "gitlab": 144 | if pipeline.GitUrl != "" { 145 | return fmt.Sprintf("%s/%s/%s/-/merge_requests/%s", pipeline.GitUrl, pipeline.Owner, pipeline.Repository, pipeline.PullRequestNumber()) 146 | } 147 | 148 | return fmt.Sprintf("https://gitlab.com/%s/%s/-/merge_requests/%s", pipeline.Owner, pipeline.Repository, pipeline.PullRequestNumber()) 149 | case "bitbucket": 150 | if pipeline.GitUrl != "" { 151 | return fmt.Sprintf("%s/%s/%s/pull-requests/%s", pipeline.GitUrl, pipeline.Owner, pipeline.Repository, pipeline.PullRequestNumber()) 152 | } 153 | 154 | return fmt.Sprintf("https://bitbucket.org/%s/%s/pull-requests/%s", pipeline.Owner, pipeline.Repository, pipeline.PullRequestNumber()) 155 | default: 156 | return "" 157 | } 158 | } 159 | 160 | func pullRequestURLForPipelineActivity(pa *jenkinsv1.PipelineActivity) string { 161 | if !strings.HasPrefix(pa.Spec.GitBranch, "PR-") { 162 | return "" // not a PR 163 | } 164 | prNumber := strings.TrimPrefix(pa.Spec.GitBranch, "PR-") 165 | switch pipelineActivityProvider(pa) { 166 | case "github": 167 | return fmt.Sprintf("https://github.com/%s/%s/pull/%s", pa.Spec.GitOwner, pa.Spec.GitRepository, prNumber) 168 | case "gitlab": 169 | return fmt.Sprintf("https://gitlab.com/%s/%s/-/merge_requests/%s", pa.Spec.GitOwner, pa.Spec.GitRepository, prNumber) 170 | case "bitbucket": 171 | return fmt.Sprintf("https://bitbucket.org/%s/%s/pull-requests/%s", pa.Spec.GitOwner, pa.Spec.GitRepository, prNumber) 172 | default: 173 | return "" 174 | } 175 | } 176 | 177 | func branchURLForPipeline(pipeline visualizer.Pipeline) string { 178 | if pipeline.PullRequestNumber() != "" { 179 | return pullRequestURLForPipeline(pipeline) 180 | } 181 | switch pipeline.Provider { 182 | case "github": 183 | return fmt.Sprintf("https://github.com/%s/%s/tree/%s", pipeline.Owner, pipeline.Repository, pipeline.Branch) 184 | case "gitlab": 185 | return fmt.Sprintf("https://gitlab.com/%s/%s/-/tree/%s", pipeline.Owner, pipeline.Repository, pipeline.Branch) 186 | case "bitbucket": 187 | return fmt.Sprintf("https://bitbucket.org/%s/%s/branch/%s", pipeline.Owner, pipeline.Repository, pipeline.Branch) 188 | default: 189 | return "" 190 | } 191 | } 192 | 193 | func branchURLForPipelineActivity(pa *jenkinsv1.PipelineActivity) string { 194 | if strings.HasPrefix(pa.Spec.GitBranch, "PR-") { 195 | return pullRequestURLForPipelineActivity(pa) 196 | } 197 | switch pipelineActivityProvider(pa) { 198 | case "github": 199 | return fmt.Sprintf("https://github.com/%s/%s/tree/%s", pa.Spec.GitOwner, pa.Spec.GitRepository, pa.Spec.GitBranch) 200 | case "gitlab": 201 | return fmt.Sprintf("https://gitlab.com/%s/%s/-/tree/%s", pa.Spec.GitOwner, pa.Spec.GitRepository, pa.Spec.GitBranch) 202 | case "bitbucket": 203 | return fmt.Sprintf("https://bitbucket.org/%s/%s/branch/%s", pa.Spec.GitOwner, pa.Spec.GitRepository, pa.Spec.GitBranch) 204 | default: 205 | return "" 206 | } 207 | } 208 | 209 | func commitURLForPipelineActivity(pa *jenkinsv1.PipelineActivity) string { 210 | if len(pa.Spec.LastCommitURL) > 0 { 211 | return pa.Spec.LastCommitURL 212 | } 213 | switch pipelineActivityProvider(pa) { 214 | case "github": 215 | return fmt.Sprintf("https://github.com/%s/%s/commit/%s", pa.Spec.GitOwner, pa.Spec.GitRepository, pa.Spec.LastCommitSHA) 216 | case "gitlab": 217 | return fmt.Sprintf("https://gitlab.com/%s/%s/-/commit/%s", pa.Spec.GitOwner, pa.Spec.GitRepository, pa.Spec.LastCommitSHA) 218 | case "bitbucket": 219 | return fmt.Sprintf("https://bitbucket.org/%s/%s/commits/%s", pa.Spec.GitOwner, pa.Spec.GitRepository, pa.Spec.LastCommitSHA) 220 | default: 221 | return "" 222 | } 223 | } 224 | 225 | func pipelineActivityProvider(pa *jenkinsv1.PipelineActivity) string { 226 | if provider := pa.Labels["provider"]; provider != "" { 227 | return provider 228 | } 229 | 230 | if strings.Contains(pa.Spec.GitURL, "github") { 231 | return "github" 232 | } 233 | if strings.Contains(pa.Spec.GitURL, "gitlab") { 234 | return "gitlab" 235 | } 236 | if strings.Contains(pa.Spec.GitURL, "bitbucket") { 237 | return "bitbucket" 238 | } 239 | 240 | return "" 241 | } 242 | 243 | func traceIDToTraceURL(traceID string, pipelineTraceURLTemplate *template.Template) string { 244 | if pipelineTraceURLTemplate == nil { 245 | return "" 246 | } 247 | if traceID == "" { 248 | return "" 249 | } 250 | 251 | sb := new(strings.Builder) 252 | err := pipelineTraceURLTemplate.Execute(sb, map[string]string{ 253 | "TraceID": traceID, 254 | }) 255 | if err != nil { 256 | return err.Error() 257 | } 258 | return sb.String() 259 | } 260 | -------------------------------------------------------------------------------- /web/handlers/functions/pipeline.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | jenkinsv1 "github.com/jenkins-x/jx-api/v4/pkg/apis/jenkins.io/v1" 5 | ) 6 | 7 | func PipelinePreviewEnvironmentApplicationURL(pa *jenkinsv1.PipelineActivity) string { 8 | for _, stage := range pa.Spec.Steps { 9 | if stage.Preview != nil && stage.Preview.ApplicationURL != "" { 10 | return stage.Preview.ApplicationURL 11 | } 12 | } 13 | return "" 14 | } 15 | -------------------------------------------------------------------------------- /web/handlers/functions/pipeline_counts.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | func SortPipelineCounts(counts map[string]int) []map[string]interface{} { 8 | result := []map[string]interface{}{} 9 | for key, value := range counts { 10 | result = append(result, map[string]interface{}{ 11 | "key": key, 12 | "value": value, 13 | }) 14 | } 15 | sort.SliceStable(result, func(i, j int) bool { 16 | if result[i]["key"].(string) == "Other" { 17 | return false 18 | } 19 | if result[j]["key"].(string) == "Other" { 20 | return true 21 | } 22 | return result[i]["value"].(int) > result[j]["value"].(int) 23 | }) 24 | return result 25 | } 26 | -------------------------------------------------------------------------------- /web/handlers/functions/reflect.go: -------------------------------------------------------------------------------- 1 | package functions 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func IsAvailable(data interface{}, fieldName string) bool { 8 | v := reflect.ValueOf(data) 9 | if v.Kind() == reflect.Ptr { 10 | v = v.Elem() 11 | } 12 | if v.Kind() != reflect.Struct { 13 | return false 14 | } 15 | return v.FieldByName(fieldName).IsValid() 16 | } 17 | -------------------------------------------------------------------------------- /web/handlers/home.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | visualizer "github.com/jenkins-x/jx-pipelines-visualizer" 7 | 8 | "github.com/sirupsen/logrus" 9 | "github.com/unrolled/render" 10 | ) 11 | 12 | type HomeHandler struct { 13 | Store *visualizer.Store 14 | Render *render.Render 15 | Logger *logrus.Logger 16 | } 17 | 18 | func (h *HomeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 19 | var ( 20 | query = r.URL.Query().Get("q") 21 | pipelines *visualizer.Pipelines 22 | err error 23 | ) 24 | if query != "" { 25 | pipelines, err = h.Store.Query(visualizer.Query{ 26 | Query: query, 27 | }) 28 | } else { 29 | pipelines, err = h.Store.All() 30 | } 31 | if err != nil { 32 | http.Error(w, err.Error(), http.StatusInternalServerError) 33 | return 34 | } 35 | 36 | err = h.Render.HTML(w, http.StatusOK, "home", struct { 37 | Pipelines *visualizer.Pipelines 38 | Query string 39 | }{ 40 | pipelines, 41 | query, 42 | }) 43 | if err != nil { 44 | http.Error(w, err.Error(), http.StatusInternalServerError) 45 | return 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /web/handlers/logs.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "strings" 9 | "text/template" 10 | "time" 11 | 12 | "github.com/gorilla/mux" 13 | "github.com/jenkins-x-plugins/jx-pipeline/pkg/cloud/buckets" 14 | jxclientv1 "github.com/jenkins-x/jx-api/v4/pkg/client/clientset/versioned/typed/jenkins.io/v1" 15 | "github.com/jenkins-x/jx-helpers/v3/pkg/kube/naming" 16 | "github.com/sirupsen/logrus" 17 | "k8s.io/apimachinery/pkg/api/errors" 18 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 19 | ) 20 | 21 | type LogsHandler struct { 22 | PAInterfaceFactory func(namespace string) jxclientv1.PipelineActivityInterface 23 | DefaultJXNamespace string 24 | BuildLogsURLTemplate *template.Template 25 | Logger *logrus.Logger 26 | } 27 | 28 | func (h *LogsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 29 | vars := mux.Vars(r) 30 | owner := vars["owner"] 31 | repo := vars["repo"] 32 | branch := vars["branch"] 33 | if strings.HasPrefix(branch, "pr-") { 34 | branch = strings.ToUpper(branch) 35 | } 36 | build := vars["build"] 37 | namespace := vars["namespace"] 38 | if namespace == "" { 39 | namespace = h.DefaultJXNamespace 40 | } 41 | 42 | name := naming.ToValidName(fmt.Sprintf("%s-%s-%s-%s", owner, repo, branch, build)) 43 | 44 | ctx := context.Background() 45 | var buildLogsURL string 46 | pa, err := h.PAInterfaceFactory(namespace).Get(ctx, name, metav1.GetOptions{}) 47 | if err != nil && !errors.IsNotFound(err) { 48 | http.Error(w, err.Error(), http.StatusInternalServerError) 49 | return 50 | } 51 | if pa != nil { 52 | buildLogsURL = pa.Spec.BuildLogsURL 53 | } 54 | if len(buildLogsURL) == 0 { 55 | buildLogsURL, err = h.buildLogsURL(owner, repo, branch, build) 56 | if err != nil { 57 | http.Error(w, err.Error(), http.StatusInternalServerError) 58 | return 59 | } 60 | } 61 | if len(buildLogsURL) == 0 { 62 | http.NotFound(w, r) 63 | return 64 | } 65 | 66 | httpFn := func(urlString string) (string, func(*http.Request), error) { 67 | return urlString, func(*http.Request) {}, nil 68 | } 69 | reader, err := buckets.ReadURL(ctx, buildLogsURL, 30*time.Second, httpFn) 70 | if err != nil { 71 | if strings.Contains(err.Error(), "object doesn't exist") { 72 | http.NotFound(w, r) 73 | return 74 | } 75 | http.Error(w, err.Error(), http.StatusInternalServerError) 76 | return 77 | } 78 | defer reader.Close() 79 | 80 | _, err = io.Copy(w, reader) 81 | if err != nil { 82 | http.Error(w, err.Error(), http.StatusInternalServerError) 83 | return 84 | } 85 | } 86 | 87 | func (h *LogsHandler) buildLogsURL(owner, repo, branch, build string) (string, error) { 88 | if h.BuildLogsURLTemplate == nil { 89 | return "", nil 90 | } 91 | 92 | sb := new(strings.Builder) 93 | err := h.BuildLogsURLTemplate.Execute(sb, map[string]string{ 94 | "Owner": owner, 95 | "Repository": repo, 96 | "Branch": branch, 97 | "Build": build, 98 | }) 99 | if err != nil { 100 | return "", fmt.Errorf("failed to generate build logs URL: %w", err) 101 | } 102 | 103 | return sb.String(), nil 104 | } 105 | -------------------------------------------------------------------------------- /web/handlers/logs_live.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | jenkinsv1 "github.com/jenkins-x/jx-api/v4/pkg/apis/jenkins.io/v1" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/gorilla/mux" 11 | "github.com/jenkins-x-plugins/jx-pipeline/pkg/tektonlog" 12 | jxclient "github.com/jenkins-x/jx-api/v4/pkg/client/clientset/versioned" 13 | "github.com/jenkins-x/jx-helpers/v3/pkg/kube/naming" 14 | "github.com/rs/xid" 15 | "github.com/sirupsen/logrus" 16 | sse "github.com/subchord/go-sse" 17 | tknv1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" 18 | tknclient "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" 19 | "k8s.io/apimachinery/pkg/api/errors" 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | "k8s.io/apimachinery/pkg/labels" 22 | "k8s.io/client-go/kubernetes" 23 | ) 24 | 25 | type LiveLogsHandler struct { 26 | JXClient jxclient.Interface 27 | TektonClient tknclient.Interface 28 | DefaultJXNamespace string 29 | KubeClient kubernetes.Interface 30 | Namespace string 31 | Broker *sse.Broker 32 | Logger *logrus.Logger 33 | } 34 | 35 | func (h *LiveLogsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 36 | vars := mux.Vars(r) 37 | owner := vars["owner"] 38 | repo := vars["repo"] 39 | branch := vars["branch"] 40 | if strings.HasPrefix(branch, "pr-") { 41 | branch = strings.ToUpper(branch) 42 | } 43 | build := vars["build"] 44 | namespace := vars["namespace"] 45 | if namespace == "" { 46 | namespace = h.DefaultJXNamespace 47 | } 48 | 49 | name := naming.ToValidName(fmt.Sprintf("%s-%s-%s-%s", owner, repo, branch, build)) 50 | 51 | ctx := context.Background() 52 | pa, err := h.JXClient.JenkinsV1().PipelineActivities(namespace).Get(ctx, name, metav1.GetOptions{}) 53 | if err != nil { 54 | if errors.IsNotFound(err) { 55 | http.NotFound(w, r) 56 | return 57 | } 58 | http.Error(w, err.Error(), http.StatusInternalServerError) 59 | return 60 | } 61 | 62 | prBuild := getPipelineRunBuild(pa, build) 63 | pipelineruns, labelSelector, err := h.getPipelineRuns(ctx, owner, repo, branch, prBuild, namespace) 64 | if err != nil { 65 | http.Error(w, err.Error(), http.StatusInternalServerError) 66 | return 67 | } 68 | if len(pipelineruns) == 0 { 69 | http.Error(w, fmt.Sprintf("no PipelineRun found using labelSelector %s", labelSelector), http.StatusTooEarly) 70 | return 71 | } 72 | 73 | clientConnection, err := h.Broker.Connect(xid.New().String(), w, r) 74 | if err != nil { 75 | // streaming unsupported. http.Error() already used in broker.Connect() 76 | return 77 | } 78 | 79 | logger := &tektonlog.TektonLogger{ 80 | KubeClient: h.KubeClient, 81 | JXClient: h.JXClient, 82 | TektonClient: h.TektonClient, 83 | Namespace: namespace, 84 | } 85 | for logLine := range logger.GetRunningBuildLogs(ctx, pa, pipelineruns, name) { 86 | h.send(r.Context(), clientConnection, "log", logLine.Line) 87 | } 88 | 89 | if err := logger.Err(); err == nil && len(pipelineruns) == 1 && pipelineruns[0].Labels["jenkins.io/pipelineType"] == "meta" { 90 | // if we started with only the meta-pipeline, let's now retry with the "real" build pipeline 91 | pipelineruns, _, _ = h.getPipelineRuns(ctx, owner, repo, branch, build, namespace, "jenkins.io/pipelineType=build") 92 | if len(pipelineruns) > 0 { 93 | for logLine := range logger.GetRunningBuildLogs(ctx, pa, pipelineruns, name) { 94 | h.send(r.Context(), clientConnection, "log", logLine.Line) 95 | } 96 | } 97 | } 98 | 99 | if err := logger.Err(); err != nil { 100 | h.send(r.Context(), clientConnection, "error", err.Error()) 101 | } 102 | 103 | h.send(r.Context(), clientConnection, "EOF", "End Of Feed") 104 | 105 | select { 106 | case <-clientConnection.Done(): 107 | case <-r.Context().Done(): 108 | } 109 | } 110 | 111 | // getPipelineRunBuild the PipelineRun build can be different to the PipelineActivity build if using Jenkins X v3 112 | // as lighthouse tekton controller uses an automatically generated large build number on its generated PipelineRun resources 113 | func getPipelineRunBuild(pa *jenkinsv1.PipelineActivity, build string) string { 114 | if pa.Labels != nil { 115 | answer := pa.Labels["lighthouse.jenkins-x.io/buildNum"] 116 | if answer != "" { 117 | return answer 118 | } 119 | } 120 | return build 121 | } 122 | 123 | func (h *LiveLogsHandler) send(ctx context.Context, clientConnection *sse.ClientConnection, eventType, eventData string) { 124 | select { 125 | case <-clientConnection.Done(): 126 | return 127 | case <-ctx.Done(): 128 | return 129 | default: 130 | clientConnection.Send(sse.StringEvent{ 131 | Id: xid.New().String(), 132 | Event: eventType, 133 | Data: eventData, 134 | }) 135 | } 136 | } 137 | 138 | func (h *LiveLogsHandler) getPipelineRuns(ctx context.Context, owner, repo, branch, build string, namespace string, extraSelectors ...string) ([]*tknv1beta1.PipelineRun, string, error) { 139 | var extraLabelSet labels.Set 140 | for _, extraSelector := range extraSelectors { 141 | labelSet, err := labels.ConvertSelectorToLabelsMap(extraSelector) 142 | if err != nil { 143 | return nil, "", err 144 | } 145 | extraLabelSet = labels.Merge(extraLabelSet, labelSet) 146 | } 147 | 148 | labelSet := labels.Set(map[string]string{ 149 | "lighthouse.jenkins-x.io/refs.org": owner, 150 | "lighthouse.jenkins-x.io/refs.repo": repo, 151 | "lighthouse.jenkins-x.io/branch": branch, 152 | "lighthouse.jenkins-x.io/buildNum": build, 153 | }) 154 | labelSelector := labels.FormatLabels(labels.Merge(extraLabelSet, labelSet)) 155 | prList, err := h.TektonClient.TektonV1beta1().PipelineRuns(namespace).List(ctx, metav1.ListOptions{ 156 | LabelSelector: labelSelector, 157 | }) 158 | if err != nil { 159 | return nil, labelSelector, err 160 | } 161 | 162 | if len(prList.Items) == 0 { 163 | // let's also try with the "old" labels used in jx v2 164 | labelSet := labels.Set(map[string]string{ 165 | "owner": owner, 166 | "repository": repo, 167 | "branch": branch, 168 | "build": build, 169 | }) 170 | labelSelector := labels.FormatLabels(labels.Merge(extraLabelSet, labelSet)) 171 | prList, err = h.TektonClient.TektonV1beta1().PipelineRuns(namespace).List(ctx, metav1.ListOptions{ 172 | LabelSelector: labelSelector, 173 | }) 174 | if err != nil { 175 | return nil, labelSelector, err 176 | } 177 | } 178 | 179 | prs := make([]*tknv1beta1.PipelineRun, 0, len(prList.Items)) 180 | for i := range prList.Items { 181 | prs = append(prs, &prList.Items[i]) 182 | } 183 | 184 | return prs, labelSelector, nil 185 | } 186 | -------------------------------------------------------------------------------- /web/handlers/owner.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | visualizer "github.com/jenkins-x/jx-pipelines-visualizer" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/sirupsen/logrus" 10 | "github.com/unrolled/render" 11 | ) 12 | 13 | type OwnerHandler struct { 14 | Store *visualizer.Store 15 | Render *render.Render 16 | Logger *logrus.Logger 17 | } 18 | 19 | func (h *OwnerHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 20 | vars := mux.Vars(r) 21 | owner := vars["owner"] 22 | 23 | pipelines, err := h.Store.Query(visualizer.Query{ 24 | Owner: owner, 25 | Query: r.URL.Query().Get("q"), 26 | }) 27 | if err != nil { 28 | http.Error(w, err.Error(), http.StatusInternalServerError) 29 | return 30 | } 31 | 32 | err = h.Render.HTML(w, http.StatusOK, "home", struct { 33 | Owner string 34 | Query string 35 | Pipelines *visualizer.Pipelines 36 | }{ 37 | owner, 38 | r.URL.Query().Get("q"), 39 | pipelines, 40 | }) 41 | if err != nil { 42 | http.Error(w, err.Error(), http.StatusInternalServerError) 43 | return 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /web/handlers/pipeline.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | "text/template" 9 | "time" 10 | 11 | "github.com/gorilla/mux" 12 | "github.com/jenkins-x-plugins/jx-pipeline/pkg/cloud/buckets" 13 | jenkinsv1 "github.com/jenkins-x/jx-api/v4/pkg/apis/jenkins.io/v1" 14 | jxclientv1 "github.com/jenkins-x/jx-api/v4/pkg/client/clientset/versioned/typed/jenkins.io/v1" 15 | "github.com/jenkins-x/jx-helpers/v3/pkg/kube/naming" 16 | "github.com/sirupsen/logrus" 17 | "github.com/unrolled/render" 18 | "gopkg.in/yaml.v2" 19 | "k8s.io/apimachinery/pkg/api/errors" 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | "k8s.io/cli-runtime/pkg/printers" 22 | ) 23 | 24 | type PipelineHandler struct { 25 | PAInterfaceFactory func(namespace string) jxclientv1.PipelineActivityInterface 26 | DefaultJXNamespace string 27 | BuildLogsURLTemplate *template.Template 28 | StoredPipelinesURLTemplate *template.Template 29 | RenderYAML bool 30 | Render *render.Render 31 | Logger *logrus.Logger 32 | } 33 | 34 | func (h *PipelineHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 35 | vars := mux.Vars(r) 36 | owner := vars["owner"] 37 | repo := vars["repo"] 38 | branch := vars["branch"] 39 | namespace := vars["namespace"] 40 | 41 | if namespace == "" { 42 | namespace = h.DefaultJXNamespace 43 | } 44 | 45 | if strings.HasPrefix(branch, "pr-") { 46 | branch = strings.ToUpper(branch) 47 | } 48 | build := vars["build"] 49 | 50 | name := naming.ToValidName(fmt.Sprintf("%s-%s-%s-%s", owner, repo, branch, build)) 51 | 52 | ctx := context.Background() 53 | pa, err := h.PAInterfaceFactory(namespace).Get(ctx, name, metav1.GetOptions{}) 54 | 55 | if err != nil && !errors.IsNotFound(err) { 56 | http.Error(w, err.Error(), http.StatusInternalServerError) 57 | return 58 | } 59 | if errors.IsNotFound(err) { 60 | pa = nil 61 | } 62 | 63 | if pa == nil { 64 | pa, err = h.loadPipelineFromStorage(ctx, owner, repo, branch, build) 65 | if err != nil { 66 | http.Error(w, err.Error(), http.StatusInternalServerError) 67 | return 68 | } 69 | } 70 | 71 | if pa == nil { 72 | err := h.Render.HTML(w, http.StatusOK, "archived_logs", map[string]string{ 73 | "Owner": owner, 74 | "Repository": repo, 75 | "Branch": branch, 76 | "Build": build, 77 | }) 78 | if err != nil { 79 | http.Error(w, err.Error(), http.StatusInternalServerError) 80 | return 81 | } 82 | return 83 | } 84 | 85 | if h.RenderYAML { 86 | if pa.APIVersion == "" { 87 | pa.APIVersion = "jenkins.io/v1" 88 | } 89 | if pa.Kind == "" { 90 | pa.Kind = "PipelineActivity" 91 | } 92 | err = new(printers.YAMLPrinter).PrintObj(pa, w) 93 | if err != nil { 94 | http.Error(w, err.Error(), http.StatusInternalServerError) 95 | return 96 | } 97 | return 98 | } 99 | 100 | err = h.Render.HTML(w, http.StatusOK, "pipeline", struct { 101 | Pipeline *jenkinsv1.PipelineActivity 102 | }{ 103 | pa, 104 | }) 105 | if err != nil { 106 | http.Error(w, err.Error(), http.StatusInternalServerError) 107 | return 108 | } 109 | } 110 | 111 | func (h *PipelineHandler) loadPipelineFromStorage(ctx context.Context, owner, repo, branch, build string) (*jenkinsv1.PipelineActivity, error) { 112 | storedPipelineURL, err := h.storedPipelineURL(owner, repo, branch, build) 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | if storedPipelineURL == "" { 118 | return nil, nil 119 | } 120 | 121 | httpFn := func(urlString string) (string, func(*http.Request), error) { 122 | return urlString, func(*http.Request) {}, nil 123 | } 124 | reader, err := buckets.ReadURL(ctx, storedPipelineURL, 30*time.Second, httpFn) 125 | if err != nil { 126 | if strings.Contains(err.Error(), "object doesn't exist") { 127 | return nil, nil 128 | } 129 | return nil, err 130 | } 131 | defer reader.Close() 132 | 133 | var pa jenkinsv1.PipelineActivity 134 | err = yaml.NewDecoder(reader).Decode(&pa) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | if pa.Spec.BuildLogsURL == "" { 140 | pa.Spec.BuildLogsURL, err = h.buildLogsURL(owner, repo, branch, build) 141 | if err != nil { 142 | return nil, err 143 | } 144 | } 145 | 146 | return &pa, nil 147 | } 148 | 149 | func (h *PipelineHandler) storedPipelineURL(owner, repo, branch, build string) (string, error) { 150 | if h.StoredPipelinesURLTemplate == nil { 151 | return "", nil 152 | } 153 | 154 | sb := new(strings.Builder) 155 | err := h.StoredPipelinesURLTemplate.Execute(sb, map[string]string{ 156 | "Owner": owner, 157 | "Repository": repo, 158 | "Branch": branch, 159 | "Build": build, 160 | }) 161 | if err != nil { 162 | return "", fmt.Errorf("failed to generate stored pipeline URL: %w", err) 163 | } 164 | 165 | return sb.String(), nil 166 | } 167 | 168 | func (h *PipelineHandler) buildLogsURL(owner, repo, branch, build string) (string, error) { 169 | if h.BuildLogsURLTemplate == nil { 170 | return "", nil 171 | } 172 | 173 | sb := new(strings.Builder) 174 | err := h.BuildLogsURLTemplate.Execute(sb, map[string]string{ 175 | "Owner": owner, 176 | "Repository": repo, 177 | "Branch": branch, 178 | "Build": build, 179 | }) 180 | if err != nil { 181 | return "", fmt.Errorf("failed to generate build logs URL: %w", err) 182 | } 183 | 184 | return sb.String(), nil 185 | } 186 | -------------------------------------------------------------------------------- /web/handlers/pipelinerun.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | "text/template" 9 | "time" 10 | 11 | "github.com/gorilla/mux" 12 | "github.com/jenkins-x-plugins/jx-pipeline/pkg/cloud/buckets" 13 | "github.com/jenkins-x-plugins/jx-pipeline/pkg/tektonlog" 14 | jxclientv1 "github.com/jenkins-x/jx-api/v4/pkg/client/clientset/versioned/typed/jenkins.io/v1" 15 | "github.com/jenkins-x/jx-helpers/v3/pkg/kube/activities" 16 | visualizer "github.com/jenkins-x/jx-pipelines-visualizer" 17 | "github.com/sirupsen/logrus" 18 | "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" 19 | tknclient "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" 20 | "github.com/unrolled/render" 21 | "gopkg.in/yaml.v2" 22 | "k8s.io/apimachinery/pkg/api/errors" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | ) 25 | 26 | type PipelineRunHandler struct { 27 | PAInterfaceFactory func(namespace string) jxclientv1.PipelineActivityInterface 28 | TektonClient tknclient.Interface 29 | StoredPipelineRunsURLTemplate *template.Template 30 | Namespace string 31 | Store *visualizer.Store 32 | Render *render.Render 33 | Logger *logrus.Logger 34 | } 35 | 36 | func (h *PipelineRunHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 37 | vars := mux.Vars(r) 38 | pipelineRunName := vars["pipelineRun"] 39 | ns := vars["namespace"] 40 | if ns == "" { 41 | ns = h.Namespace 42 | } 43 | 44 | ctx := context.Background() 45 | pr, err := h.TektonClient.TektonV1beta1().PipelineRuns(ns).Get(ctx, pipelineRunName, metav1.GetOptions{}) 46 | if err != nil && !errors.IsNotFound(err) { 47 | http.Error(w, err.Error(), http.StatusInternalServerError) 48 | return 49 | } 50 | if errors.IsNotFound(err) { 51 | pr = nil 52 | } 53 | 54 | if pr == nil { 55 | pr, err = h.loadPipelineRunFromStorage(ctx, ns, pipelineRunName) 56 | if err != nil { 57 | http.Error(w, err.Error(), http.StatusInternalServerError) 58 | return 59 | } 60 | } 61 | 62 | if pr == nil { 63 | http.NotFound(w, r) 64 | return 65 | } 66 | 67 | var ( 68 | owner = activities.GetLabel(pr.Labels, activities.OwnerLabels) 69 | repo = activities.GetLabel(pr.Labels, activities.RepoLabels) 70 | branch = activities.GetLabel(pr.Labels, activities.BranchLabels) 71 | build = pr.Labels["build"] 72 | ) 73 | if owner == "" || repo == "" || branch == "" || build == "" { 74 | pa, err := tektonlog.GetPipelineActivityForPipelineRun(context.TODO(), h.PAInterfaceFactory(pr.Namespace), pr) 75 | if err != nil && !errors.IsNotFound(err) { 76 | http.Error(w, err.Error(), http.StatusInternalServerError) 77 | return 78 | } 79 | if pa == nil { 80 | http.NotFound(w, r) 81 | return 82 | } 83 | owner = pa.Spec.GitOwner 84 | repo = pa.Spec.GitRepository 85 | branch = pa.Spec.GitBranch 86 | build = pa.Spec.Build 87 | } 88 | 89 | redirectURL := fmt.Sprintf("/ns-%s/%s/%s/%s/%s", ns, owner, repo, branch, build) 90 | http.Redirect(w, r, redirectURL, http.StatusMovedPermanently) 91 | } 92 | 93 | func (h *PipelineRunHandler) loadPipelineRunFromStorage(ctx context.Context, namespace, name string) (*v1beta1.PipelineRun, error) { 94 | storedPipelineRunURL, err := h.storedPipelineRunURL(namespace, name) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | if storedPipelineRunURL == "" { 100 | return nil, nil 101 | } 102 | 103 | httpFn := func(urlString string) (string, func(*http.Request), error) { 104 | return urlString, func(*http.Request) {}, nil 105 | } 106 | reader, err := buckets.ReadURL(ctx, storedPipelineRunURL, 30*time.Second, httpFn) 107 | if err != nil { 108 | if strings.Contains(err.Error(), "object doesn't exist") { 109 | return nil, nil 110 | } 111 | return nil, err 112 | } 113 | defer reader.Close() 114 | 115 | var pr v1beta1.PipelineRun 116 | err = yaml.NewDecoder(reader).Decode(&pr) 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | return &pr, nil 122 | } 123 | 124 | func (h *PipelineRunHandler) storedPipelineRunURL(namespace, name string) (string, error) { 125 | if h.StoredPipelineRunsURLTemplate == nil { 126 | return "", nil 127 | } 128 | 129 | sb := new(strings.Builder) 130 | err := h.StoredPipelineRunsURLTemplate.Execute(sb, map[string]string{ 131 | "Namespace": namespace, 132 | "Name": name, 133 | }) 134 | if err != nil { 135 | return "", fmt.Errorf("failed to generate stored pipeline run URL: %w", err) 136 | } 137 | 138 | return sb.String(), nil 139 | } 140 | -------------------------------------------------------------------------------- /web/handlers/repository.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | visualizer "github.com/jenkins-x/jx-pipelines-visualizer" 7 | 8 | "github.com/gorilla/mux" 9 | "github.com/sirupsen/logrus" 10 | "github.com/unrolled/render" 11 | ) 12 | 13 | type RepositoryHandler struct { 14 | Store *visualizer.Store 15 | Render *render.Render 16 | Logger *logrus.Logger 17 | } 18 | 19 | func (h *RepositoryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 20 | vars := mux.Vars(r) 21 | owner := vars["owner"] 22 | repository := vars["repo"] 23 | 24 | pipelines, err := h.Store.Query(visualizer.Query{ 25 | Owner: owner, 26 | Repository: repository, 27 | Query: r.URL.Query().Get("q"), 28 | }) 29 | if err != nil { 30 | http.Error(w, err.Error(), http.StatusInternalServerError) 31 | return 32 | } 33 | 34 | err = h.Render.HTML(w, http.StatusOK, "home", struct { 35 | Owner string 36 | Repository string 37 | Query string 38 | Pipelines *visualizer.Pipelines 39 | }{ 40 | owner, 41 | repository, 42 | r.URL.Query().Get("q"), 43 | pipelines, 44 | }) 45 | if err != nil { 46 | http.Error(w, err.Error(), http.StatusInternalServerError) 47 | return 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /web/handlers/router.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | htmltemplate "html/template" 6 | "net/http" 7 | "text/template" 8 | 9 | visualizer "github.com/jenkins-x/jx-pipelines-visualizer" 10 | "github.com/jenkins-x/jx-pipelines-visualizer/internal/version" 11 | "github.com/jenkins-x/jx-pipelines-visualizer/web/handlers/functions" 12 | 13 | "github.com/Masterminds/sprig/v3" 14 | "github.com/gorilla/mux" 15 | jxclient "github.com/jenkins-x/jx-api/v4/pkg/client/clientset/versioned" 16 | jenkinsv1 "github.com/jenkins-x/jx-api/v4/pkg/client/clientset/versioned/typed/jenkins.io/v1" 17 | "github.com/sirupsen/logrus" 18 | sse "github.com/subchord/go-sse" 19 | tknclient "github.com/tektoncd/pipeline/pkg/client/clientset/versioned" 20 | "github.com/unrolled/render" 21 | "github.com/urfave/negroni/v2" 22 | "k8s.io/client-go/kubernetes" 23 | "k8s.io/client-go/rest" 24 | ) 25 | 26 | type Router struct { 27 | Store *visualizer.Store 28 | RunningPipelines *visualizer.RunningPipelines 29 | KConfig *rest.Config 30 | PAInterfaceFactory func(namespace string) jenkinsv1.PipelineActivityInterface 31 | Namespace string 32 | DefaultJXNamespace string 33 | ArchivedLogsURLTemplate string 34 | ArchivedPipelinesURLTemplate string 35 | ArchivedPipelineRunsURLTemplate string 36 | PipelineTraceURLTemplate string 37 | Logger *logrus.Logger 38 | render *render.Render 39 | } 40 | 41 | func (r Router) Handler() (http.Handler, error) { 42 | kClient, err := kubernetes.NewForConfig(r.KConfig) 43 | if err != nil { 44 | return nil, err 45 | } 46 | jxClient, err := jxclient.NewForConfig(r.KConfig) 47 | if err != nil { 48 | return nil, err 49 | } 50 | tknClient, err := tknclient.NewForConfig(r.KConfig) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | var archivedLogsURLTemplate *template.Template 56 | if len(r.ArchivedLogsURLTemplate) > 0 { 57 | archivedLogsURLTemplate, err = template.New("archivedLogsURL").Funcs(sprig.TxtFuncMap()).Parse(r.ArchivedLogsURLTemplate) 58 | if err != nil { 59 | return nil, err 60 | } 61 | } 62 | 63 | var archivedPipelinesURLTemplate *template.Template 64 | if len(r.ArchivedPipelinesURLTemplate) > 0 { 65 | archivedPipelinesURLTemplate, err = template.New("archivedPipelinesURL").Funcs(sprig.TxtFuncMap()).Parse(r.ArchivedPipelinesURLTemplate) 66 | if err != nil { 67 | return nil, err 68 | } 69 | } 70 | 71 | var archivedPipelineRunsURLTemplate *template.Template 72 | if len(r.ArchivedPipelineRunsURLTemplate) > 0 { 73 | archivedPipelineRunsURLTemplate, err = template.New("archivedPipelineRunsURL").Funcs(sprig.TxtFuncMap()).Parse(r.ArchivedPipelineRunsURLTemplate) 74 | if err != nil { 75 | return nil, err 76 | } 77 | } 78 | 79 | var pipelineTraceURLTemplate *template.Template 80 | if len(r.PipelineTraceURLTemplate) > 0 { 81 | pipelineTraceURLTemplate, err = template.New("pipelineTraceURL").Funcs(sprig.TxtFuncMap()).Parse(r.PipelineTraceURLTemplate) 82 | if err != nil { 83 | return nil, err 84 | } 85 | } 86 | 87 | r.render = render.New(render.Options{ 88 | Directory: "web/templates", 89 | Layout: "layout", 90 | IsDevelopment: version.Version == "dev", 91 | Funcs: []htmltemplate.FuncMap{ 92 | sprig.HtmlFuncMap(), 93 | htmltemplate.FuncMap{ 94 | "pipelinePreviewEnvironmentApplicationURL": functions.PipelinePreviewEnvironmentApplicationURL, 95 | "traceURL": functions.TraceURLFunc(pipelineTraceURLTemplate), 96 | "repositoryURL": functions.RepositoryURL, 97 | "prURL": functions.PullRequestURL, 98 | "branchURL": functions.BranchURL, 99 | "commitURL": functions.CommitURL, 100 | "authorURL": functions.AuthorURL, 101 | "vdate": functions.VDate, 102 | "sortPipelineCounts": functions.SortPipelineCounts, 103 | "isAvailable": functions.IsAvailable, 104 | "appVersion": functions.AppVersion, 105 | }, 106 | }, 107 | }) 108 | 109 | router := mux.NewRouter() 110 | router.StrictSlash(true) 111 | 112 | router.Handle("/", &HomeHandler{ 113 | Store: r.Store, 114 | Render: r.render, 115 | Logger: r.Logger, 116 | }) 117 | 118 | router.Handle("/healthz", healthzHandler()) 119 | 120 | router.Handle("/running", &RunningHandler{ 121 | RunningPipelines: r.RunningPipelines, 122 | Render: r.render, 123 | Logger: r.Logger, 124 | }) 125 | 126 | router.Handle("/running/events", &RunningEventsHandler{ 127 | RunningPipelines: r.RunningPipelines, 128 | Broker: sse.NewBroker(nil), 129 | Logger: r.Logger, 130 | }) 131 | 132 | router.Handle("/{owner}", &OwnerHandler{ 133 | Store: r.Store, 134 | Render: r.render, 135 | Logger: r.Logger, 136 | }) 137 | 138 | router.Handle("/{owner}/{repo}", &RepositoryHandler{ 139 | Store: r.Store, 140 | Render: r.render, 141 | Logger: r.Logger, 142 | }) 143 | 144 | router.Handle("/{owner}/{repo}/{branch}", &BranchHandler{ 145 | Store: r.Store, 146 | Render: r.render, 147 | Logger: r.Logger, 148 | }) 149 | 150 | router.Handle("/{owner}/{repo}/{branch}/shields.io", &ShieldsIOHandler{ 151 | Store: r.Store, 152 | Render: r.render, 153 | Logger: r.Logger, 154 | }) 155 | 156 | router.Handle("/ns-{namespace}/{owner}/{repo}/{branch}/{build:[0-9]+}", &PipelineHandler{ 157 | PAInterfaceFactory: r.PAInterfaceFactory, 158 | DefaultJXNamespace: r.DefaultJXNamespace, 159 | StoredPipelinesURLTemplate: archivedPipelinesURLTemplate, 160 | BuildLogsURLTemplate: archivedLogsURLTemplate, 161 | Render: r.render, 162 | Logger: r.Logger, 163 | }) 164 | 165 | router.Handle("/{owner}/{repo}/{branch}/{build:[0-9]+}", &PipelineHandler{ 166 | PAInterfaceFactory: r.PAInterfaceFactory, 167 | DefaultJXNamespace: r.DefaultJXNamespace, 168 | StoredPipelinesURLTemplate: archivedPipelinesURLTemplate, 169 | BuildLogsURLTemplate: archivedLogsURLTemplate, 170 | Render: r.render, 171 | Logger: r.Logger, 172 | }) 173 | 174 | router.Handle("/ns-{namespace}/{owner}/{repo}/{branch}/{build:[0-9]+}.yaml", &PipelineHandler{ 175 | PAInterfaceFactory: r.PAInterfaceFactory, 176 | DefaultJXNamespace: r.DefaultJXNamespace, 177 | StoredPipelinesURLTemplate: archivedPipelinesURLTemplate, 178 | BuildLogsURLTemplate: archivedLogsURLTemplate, 179 | RenderYAML: true, 180 | Render: r.render, 181 | Logger: r.Logger, 182 | }) 183 | 184 | router.Handle("/{owner}/{repo}/{branch}/{build:[0-9]+}.yaml", &PipelineHandler{ 185 | PAInterfaceFactory: r.PAInterfaceFactory, 186 | DefaultJXNamespace: r.DefaultJXNamespace, 187 | StoredPipelinesURLTemplate: archivedPipelinesURLTemplate, 188 | BuildLogsURLTemplate: archivedLogsURLTemplate, 189 | RenderYAML: true, 190 | Render: r.render, 191 | Logger: r.Logger, 192 | }) 193 | 194 | router.Handle("/ns-{namespace}/{owner}/{repo}/{branch}/{build:[0-9]+}/logs", &LogsHandler{ 195 | PAInterfaceFactory: r.PAInterfaceFactory, 196 | DefaultJXNamespace: r.DefaultJXNamespace, 197 | BuildLogsURLTemplate: archivedLogsURLTemplate, 198 | Logger: r.Logger, 199 | }) 200 | 201 | router.Handle("/{owner}/{repo}/{branch}/{build:[0-9]+}/logs", &LogsHandler{ 202 | PAInterfaceFactory: r.PAInterfaceFactory, 203 | DefaultJXNamespace: r.DefaultJXNamespace, 204 | BuildLogsURLTemplate: archivedLogsURLTemplate, 205 | Logger: r.Logger, 206 | }) 207 | 208 | router.Handle("/ns-{namespace}/{owner}/{repo}/{branch}/{build:[0-9]+}/logs/live", &LiveLogsHandler{ 209 | DefaultJXNamespace: r.DefaultJXNamespace, 210 | KubeClient: kClient, 211 | JXClient: jxClient, 212 | TektonClient: tknClient, 213 | Broker: sse.NewBroker(nil), 214 | Logger: r.Logger, 215 | }) 216 | 217 | router.Handle("/{owner}/{repo}/{branch}/{build:[0-9]+}/logs/live", &LiveLogsHandler{ 218 | DefaultJXNamespace: r.DefaultJXNamespace, 219 | KubeClient: kClient, 220 | JXClient: jxClient, 221 | TektonClient: tknClient, 222 | Broker: sse.NewBroker(nil), 223 | Logger: r.Logger, 224 | }) 225 | 226 | router.Handle("/namespaces/{namespace}/pipelineruns/{pipelineRun}", &PipelineRunHandler{ 227 | PAInterfaceFactory: r.PAInterfaceFactory, 228 | TektonClient: tknClient, 229 | StoredPipelineRunsURLTemplate: archivedPipelineRunsURLTemplate, 230 | Namespace: r.Namespace, 231 | Store: r.Store, 232 | Render: r.render, 233 | Logger: r.Logger, 234 | }) 235 | 236 | router.Handle("/teams/{team}/projects/{owner}/{repo}/{branch}/{build:[0-9]+}", jxuiCompatibilityHandler(r.Namespace)) 237 | 238 | handler := negroni.New( 239 | negroni.NewRecovery(), 240 | &negroni.Static{ 241 | Dir: http.Dir("web/static"), 242 | Prefix: "/static", 243 | IndexFile: "index.html", 244 | }, 245 | negroni.Wrap(router), 246 | ) 247 | 248 | return handler, nil 249 | } 250 | 251 | func jxuiCompatibilityHandler(namespace string) http.Handler { 252 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 253 | vars := mux.Vars(r) 254 | team := vars["team"] 255 | owner := vars["owner"] 256 | repo := vars["repo"] 257 | branch := vars["branch"] 258 | build := vars["build"] 259 | 260 | if team != namespace { 261 | http.NotFound(w, r) 262 | return 263 | } 264 | 265 | redirectURL := fmt.Sprintf("/%s/%s/%s/%s", owner, repo, branch, build) 266 | http.Redirect(w, r, redirectURL, http.StatusMovedPermanently) 267 | }) 268 | } 269 | 270 | func healthzHandler() http.Handler { 271 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 272 | w.WriteHeader(http.StatusOK) 273 | }) 274 | } 275 | -------------------------------------------------------------------------------- /web/handlers/running.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "net/http" 5 | 6 | visualizer "github.com/jenkins-x/jx-pipelines-visualizer" 7 | 8 | "github.com/sirupsen/logrus" 9 | "github.com/unrolled/render" 10 | ) 11 | 12 | type RunningHandler struct { 13 | RunningPipelines *visualizer.RunningPipelines 14 | Render *render.Render 15 | Logger *logrus.Logger 16 | } 17 | 18 | func (h *RunningHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 19 | err := h.Render.HTML(w, http.StatusOK, "running", struct { 20 | Pipelines []visualizer.RunningPipeline 21 | }{ 22 | h.RunningPipelines.Get(), 23 | }) 24 | if err != nil { 25 | http.Error(w, err.Error(), http.StatusInternalServerError) 26 | return 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /web/handlers/running_events.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | visualizer "github.com/jenkins-x/jx-pipelines-visualizer" 8 | 9 | "github.com/rs/xid" 10 | "github.com/sirupsen/logrus" 11 | sse "github.com/subchord/go-sse" 12 | ) 13 | 14 | type RunningEventsHandler struct { 15 | RunningPipelines *visualizer.RunningPipelines 16 | Broker *sse.Broker 17 | Logger *logrus.Logger 18 | } 19 | 20 | func (h *RunningEventsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 21 | clientID := xid.New().String() 22 | clientConnection, err := h.Broker.Connect(clientID, w, r) 23 | if err != nil { 24 | // streaming unsupported. http.Error() already used in broker.Connect() 25 | return 26 | } 27 | 28 | watcher := visualizer.Watcher{ 29 | Name: clientID, 30 | Added: make(chan visualizer.RunningPipeline), 31 | Deleted: make(chan visualizer.RunningPipeline), 32 | } 33 | h.RunningPipelines.Register(watcher) 34 | 35 | for { 36 | select { 37 | case running := <-watcher.Added: 38 | h.send(r.Context(), clientConnection, "added", running.JSON()) 39 | case running := <-watcher.Deleted: 40 | h.send(r.Context(), clientConnection, "deleted", running.JSON()) 41 | case <-clientConnection.Done(): 42 | h.RunningPipelines.UnRegister(watcher) 43 | return 44 | case <-r.Context().Done(): 45 | h.RunningPipelines.UnRegister(watcher) 46 | return 47 | } 48 | } 49 | } 50 | 51 | func (h *RunningEventsHandler) send(ctx context.Context, clientConnection *sse.ClientConnection, eventType, eventData string) { 52 | select { 53 | case <-clientConnection.Done(): 54 | return 55 | case <-ctx.Done(): 56 | return 57 | default: 58 | clientConnection.Send(sse.StringEvent{ 59 | Id: xid.New().String(), 60 | Event: eventType, 61 | Data: eventData, 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /web/handlers/shields_io.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gorilla/mux" 8 | visualizer "github.com/jenkins-x/jx-pipelines-visualizer" 9 | "github.com/sirupsen/logrus" 10 | "github.com/unrolled/render" 11 | ) 12 | 13 | // ShieldsIOBadge is documented at https://shields.io/endpoint 14 | type ShieldsIOBadge struct { 15 | SchemaVersion int `json:"schemaVersion"` 16 | Label string `json:"label"` 17 | Message string `json:"message"` 18 | Color string `json:"color"` 19 | } 20 | 21 | type ShieldsIOHandler struct { 22 | Store *visualizer.Store 23 | Render *render.Render 24 | Logger *logrus.Logger 25 | } 26 | 27 | func (h *ShieldsIOHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 28 | vars := mux.Vars(r) 29 | owner := vars["owner"] 30 | repository := vars["repo"] 31 | branch := vars["branch"] 32 | 33 | pipelines, err := h.Store.Query(visualizer.Query{ 34 | Owner: owner, 35 | Repository: repository, 36 | Branch: branch, 37 | }) 38 | if err != nil { 39 | http.Error(w, err.Error(), http.StatusInternalServerError) 40 | return 41 | } 42 | 43 | if len(pipelines.Pipelines) == 0 { 44 | http.NotFound(w, r) 45 | return 46 | } 47 | lastPipeline := pipelines.Pipelines[0] 48 | 49 | shieldsIOBadge := ShieldsIOBadge{ 50 | SchemaVersion: 1, 51 | Label: "Jenkins X", 52 | Message: fmt.Sprintf("%s #%v %s", lastPipeline.Branch, lastPipeline.Build, lastPipeline.Status), 53 | Color: h.pipelineStatusToColor(lastPipeline.Status), 54 | } 55 | 56 | err = h.Render.JSON(w, http.StatusOK, shieldsIOBadge) 57 | if err != nil { 58 | http.Error(w, err.Error(), http.StatusInternalServerError) 59 | return 60 | } 61 | } 62 | 63 | func (h *ShieldsIOHandler) pipelineStatusToColor(status string) string { 64 | switch status { 65 | case "Succeeded": 66 | return "green" 67 | case "Failed": 68 | return "red" 69 | case "Running": 70 | return "blue" 71 | default: 72 | return "grey" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /web/static/app.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-background: #ffffff; 3 | --color-background-darker: #f3f6fa; 4 | --color-grey: #eaebec; 5 | 6 | --color-error: #ef476f; 7 | --color-pending: #073b4c; 8 | --color-running: #118ab2; 9 | --color-success: #06d6a0; 10 | --color-text-primary: #24292e; 11 | --color-warning: #ffd166; 12 | 13 | --main-color: #6873f9; 14 | --main-color-light: #8E96FC; 15 | --main-color-lighter: #ABB1FC; 16 | --main-color-darker: #222BA2; 17 | } 18 | 19 | * { 20 | box-sizing: border-box; 21 | } 22 | 23 | body { 24 | margin: 0; 25 | font-family: "Helvetica Neue", Helvetica, "Liberation Sans", Arial, sans-serif; 26 | } 27 | 28 | .menu-bar { 29 | background-color: var(--main-color-darker); 30 | width: 50px; 31 | height: 100%; 32 | position: fixed; 33 | } 34 | 35 | .top-header { 36 | background-color: var(--color-background); 37 | font-family: "Helvetica Neue", Helvetica, "Liberation Sans", Arial, sans-serif; 38 | height: 75px; 39 | display: flex; 40 | justify-content: space-between; 41 | padding-left: 10px; 42 | margin-bottom: 20px; 43 | overflow-x: auto; 44 | } 45 | 46 | .top-header input { 47 | margin: auto; 48 | width: 250px; 49 | border: 1px solid #f4f7fa; 50 | border-radius: 5px; 51 | color: #f4f7fa; 52 | height: 30px; 53 | } 54 | 55 | .top-header .header-title { 56 | margin: auto; 57 | margin-left: 0; 58 | line-height: 1; 59 | font-size: 22px; 60 | } 61 | 62 | .top-header .header-title .logo { 63 | margin-right: 30px; 64 | } 65 | 66 | .main-container { 67 | background-color: var(--color-background-darker); 68 | padding-left: 50px; 69 | height: 100%; 70 | } 71 | 72 | .in-building { 73 | display: flex; 74 | flex-direction: column; 75 | padding: 10px 20px 30px; 76 | justify-content: space-between; 77 | background-color: var(--color-background-darker); 78 | flex: 0 0 auto; 79 | } 80 | 81 | .pipeline-card { 82 | display: flex; 83 | flex-direction: column; 84 | border-radius: 7px; 85 | padding: 10px; 86 | } 87 | 88 | .pipeline-card span.title { 89 | font-weight: bold; 90 | } 91 | 92 | .pipeline-card ul { 93 | list-style-type: none; 94 | } 95 | 96 | .pipeline-card ul li span.count { 97 | display: inline-block; 98 | width: 30px; 99 | } 100 | 101 | .pipeline-card span.pipeline-status a, .pipeline-card span.pipeline-status a:visited, .pipeline-card span.pipeline-status a:hover, .pipeline-card span.pipeline-status a:active { 102 | color: inherit; 103 | } 104 | 105 | .pipeline { 106 | font-weight: 200; 107 | margin-bottom: 10px; 108 | } 109 | 110 | #dataTable_wrapper { 111 | background-color: #fff; 112 | padding: 20px; 113 | } 114 | 115 | pre#logs { 116 | margin: 0; 117 | padding: 10px; 118 | font-family: "Monaco", monospace; 119 | font-weight: 400; 120 | font-size: 12px; 121 | line-height: 19px; 122 | background: black; 123 | color: #d6d6d6; 124 | overflow-x: scroll; 125 | } 126 | 127 | .status-succeeded { 128 | color: var(--color-success); 129 | } 130 | .status-running { 131 | color: var(--color-running); 132 | } 133 | .status-failed { 134 | color: var(--color-error); 135 | } 136 | .status-pending { 137 | color: var(--color-pending); 138 | } 139 | 140 | .header-metadata { 141 | margin-left: auto; 142 | margin-top: auto; 143 | margin-bottom: auto; 144 | margin-right: 10px; 145 | } 146 | 147 | .header-metadata span { 148 | margin: 2px; 149 | } 150 | .header-metadata span .icon { 151 | color: var(--color-running); 152 | } 153 | 154 | .owner-img { 155 | border-radius: 5px; 156 | width: 40px; 157 | } 158 | 159 | /* Rework clear framework */ 160 | 161 | .timeline { 162 | padding-left: 0; 163 | overflow-x: auto; 164 | } 165 | 166 | .clr-timeline-step { 167 | min-width: auto; 168 | } 169 | 170 | .clr-timeline-step clr-icon[shape=success-standard] { 171 | color: var(--color-success); 172 | } 173 | 174 | .clr-timeline-step clr-icon[shape=error-standard] { 175 | color: var(--color-error); 176 | } 177 | 178 | .clr-timeline-step clr-icon[shape=dot-circle] { 179 | color: var(--color-running); 180 | } 181 | 182 | footer { 183 | margin-top: 10px; 184 | text-align: center; 185 | } 186 | 187 | @media (max-width: 600px) { 188 | .header-metadata { 189 | display: none; 190 | } 191 | 192 | .menu-bar { 193 | display: none; 194 | } 195 | 196 | .main-container { 197 | padding-left: 0; 198 | } 199 | } -------------------------------------------------------------------------------- /web/static/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkins-x/jx-pipelines-visualizer/9ffad52c64afa3bc49ff2d6d9ecc34a54e1261db/web/static/avatar.png -------------------------------------------------------------------------------- /web/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkins-x/jx-pipelines-visualizer/9ffad52c64afa3bc49ff2d6d9ecc34a54e1261db/web/static/favicon-16x16.png -------------------------------------------------------------------------------- /web/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkins-x/jx-pipelines-visualizer/9ffad52c64afa3bc49ff2d6d9ecc34a54e1261db/web/static/favicon-32x32.png -------------------------------------------------------------------------------- /web/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkins-x/jx-pipelines-visualizer/9ffad52c64afa3bc49ff2d6d9ecc34a54e1261db/web/static/favicon.ico -------------------------------------------------------------------------------- /web/static/home/index.js: -------------------------------------------------------------------------------- 1 | $(document).ready(() => { 2 | $.fn.dataTable.ext.order['dom-order'] = function( _, col ) { 3 | return this.api().column( col, {order:'index'} ).nodes().map(td => $(td).data('order')); 4 | }; 5 | 6 | $('#dataTable').DataTable({ 7 | lengthMenu: [ [10, 25, 50, 100, -1], [10, 25, 50, 100, "All"] ], 8 | pageLength: 25, 9 | order: [[5, 'desc']], 10 | columnDefs: [ 11 | { targets: 'branch', orderDataType: 'dom-order' }, 12 | { targets: 'start', orderDataType: 'dom-order' }, 13 | { targets: 'end', orderDataType: 'dom-order' }, 14 | { targets: 'duration', orderDataType: 'dom-order', type: 'numeric' }, 15 | { targets: 'author', visible: false } 16 | ] 17 | }); 18 | }); -------------------------------------------------------------------------------- /web/static/jenkins-x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 12 | 13 | 14 | 16 | 18 | 20 | 22 | 23 | 25 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 46 | 47 | 48 | 50 | 54 | 55 | 56 | 67 | 69 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /web/static/lib/ansi_up.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"ansi_up.js","sourceRoot":"","sources":["../ansi_up.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /web/static/lib/clr-icons.min.css: -------------------------------------------------------------------------------- 1 | clr-icon{display:inline-block;margin:0;height:16px;width:16px;vertical-align:middle;fill:currentColor}clr-icon .transparent-fill-stroke{stroke:currentColor}clr-icon.is-green,clr-icon.is-success{fill:#2e8500}clr-icon.is-green .transparent-fill-stroke,clr-icon.is-success .transparent-fill-stroke{stroke:#2e8500}clr-icon.is-red,clr-icon.is-danger,clr-icon.is-error{fill:#e02200}clr-icon.is-red .transparent-fill-stroke,clr-icon.is-danger .transparent-fill-stroke,clr-icon.is-error .transparent-fill-stroke{stroke:#e02200}clr-icon.is-warning{fill:#c27b00}clr-icon.is-warning .transparent-fill-stroke{stroke:#c27b00}clr-icon.is-blue,clr-icon.is-info{fill:#0077b8}clr-icon.is-blue .transparent-fill-stroke,clr-icon.is-info .transparent-fill-stroke{stroke:#0077b8}clr-icon.is-white,clr-icon.is-inverse{fill:#fff}clr-icon.is-white .transparent-fill-stroke,clr-icon.is-inverse .transparent-fill-stroke{stroke:#fff}clr-icon.is-highlight{fill:#0077b8}clr-icon.is-highlight .transparent-fill-stroke{stroke:#0077b8}clr-icon[shape$=" up"] svg,clr-icon[dir=up] svg{transform:rotate(0deg)}clr-icon[shape$=" down"] svg,clr-icon[dir=down] svg{transform:rotate(180deg)}clr-icon[shape$=" right"] svg,clr-icon[dir=right] svg{transform:rotate(90deg)}clr-icon[shape$=" left"] svg,clr-icon[dir=left] svg{transform:rotate(270deg)}clr-icon[flip=horizontal] svg{transform:scale(-1) rotateX(180deg)}clr-icon[flip=vertical] svg{transform:scale(-1) rotateY(180deg)}clr-icon .clr-i-badge{fill:#e02200}clr-icon .clr-i-badge .transparent-fill-stroke{stroke:#e02200}clr-icon>*{height:100%;width:100%;display:block;pointer-events:none}clr-icon>svg{transition:inherit}clr-icon>svg title{display:none}clr-icon .clr-i-solid,clr-icon .clr-i-solid--badged,clr-icon .clr-i-solid--alerted{display:none}clr-icon .clr-i-outline--alerted:not(.clr-i-outline),clr-icon .clr-i-outline--badged:not(.clr-i-outline){display:none}clr-icon[class*=has-alert] .can-alert .clr-i-outline--alerted{display:block}clr-icon[class*=has-alert] .can-alert .clr-i-outline:not(.clr-i-outline--alerted){display:none}clr-icon[class*=has-badge] .can-badge .clr-i-outline--badged{display:block}clr-icon[class*=has-badge] .can-badge .clr-i-outline:not(.clr-i-outline--badged){display:none}clr-icon.is-solid .has-solid .clr-i-solid{display:block}clr-icon.is-solid .has-solid .clr-i-outline,clr-icon.is-solid .has-solid .clr-i-outline--badged{display:none}clr-icon.is-solid .has-solid .clr-i-solid--alerted:not(.clr-i-solid),clr-icon.is-solid .has-solid .clr-i-solid--badged:not(.clr-i-solid){display:none}clr-icon.is-solid[class*=has-badge] .can-badge.has-solid .clr-i-solid--badged{display:block}clr-icon.is-solid[class*=has-badge] .can-badge.has-solid .clr-i-outline,clr-icon.is-solid[class*=has-badge] .can-badge.has-solid .clr-i-outline--badged,clr-icon.is-solid[class*=has-badge] .can-badge.has-solid .clr-i-solid:not(.clr-i-solid--badged){display:none}clr-icon.is-solid[class*=has-alert] .can-alert.has-solid .clr-i-solid--alerted{display:block}clr-icon.is-solid[class*=has-alert] .can-alert.has-solid .clr-i-outline,clr-icon.is-solid[class*=has-alert] .can-alert.has-solid .clr-i-outline--alerted,clr-icon.is-solid[class*=has-alert] .can-alert.has-solid .clr-i-solid:not(.clr-i-solid--alerted){display:none}clr-icon.has-badge--success .clr-i-badge{fill:#2e8500}clr-icon.has-badge--success .clr-i-badge .transparent-fill-stroke{stroke:#2e8500}clr-icon.has-badge--error .clr-i-badge{fill:#e02200}clr-icon.has-badge--error .clr-i-badge .transparent-fill-stroke{stroke:#e02200}clr-icon.has-badge--info .clr-i-badge{fill:#0077b8}clr-icon.has-badge--info .clr-i-badge .transparent-fill-stroke{stroke:#0077b8}clr-icon.has-alert .clr-i-alert{fill:#c27b00}clr-icon.has-alert .clr-i-alert .transparent-fill-stroke{stroke:#c27b00}clr-icon .is-off-screen{position:fixed!important;border:0!important;height:1px!important;width:1px!important;left:0!important;top:-1px!important;overflow:hidden!important;padding:0!important;margin:0 0 -1px 0!important} 2 | /*# sourceMappingURL=clr-icons.min.css.map */ -------------------------------------------------------------------------------- /web/static/lib/custom-elements.min.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | /* 3 | 4 | Copyright (c) 2020 The Polymer Project Authors. All rights reserved. 5 | This code may only be used under the BSD style license found at 6 | http://polymer.github.io/LICENSE.txt The complete set of authors may be found 7 | at http://polymer.github.io/AUTHORS.txt The complete set of contributors may 8 | be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by 9 | Google as part of the polymer project is also subject to an additional IP 10 | rights grant found at http://polymer.github.io/PATENTS.txt 11 | */ 12 | 'use strict';/* 13 | 14 | Copyright (c) 2016 The Polymer Project Authors. All rights reserved. 15 | This code may only be used under the BSD style license found at 16 | http://polymer.github.io/LICENSE.txt The complete set of authors may be found 17 | at http://polymer.github.io/AUTHORS.txt The complete set of contributors may 18 | be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by 19 | Google as part of the polymer project is also subject to an additional IP 20 | rights grant found at http://polymer.github.io/PATENTS.txt 21 | */ 22 | var n=window.Document.prototype.createElement,p=window.Document.prototype.createElementNS,aa=window.Document.prototype.importNode,ba=window.Document.prototype.prepend,ca=window.Document.prototype.append,da=window.DocumentFragment.prototype.prepend,ea=window.DocumentFragment.prototype.append,q=window.Node.prototype.cloneNode,r=window.Node.prototype.appendChild,t=window.Node.prototype.insertBefore,u=window.Node.prototype.removeChild,v=window.Node.prototype.replaceChild,w=Object.getOwnPropertyDescriptor(window.Node.prototype, 23 | "textContent"),y=window.Element.prototype.attachShadow,z=Object.getOwnPropertyDescriptor(window.Element.prototype,"innerHTML"),A=window.Element.prototype.getAttribute,B=window.Element.prototype.setAttribute,C=window.Element.prototype.removeAttribute,D=window.Element.prototype.getAttributeNS,E=window.Element.prototype.setAttributeNS,F=window.Element.prototype.removeAttributeNS,G=window.Element.prototype.insertAdjacentElement,H=window.Element.prototype.insertAdjacentHTML,fa=window.Element.prototype.prepend, 24 | ha=window.Element.prototype.append,ia=window.Element.prototype.before,ja=window.Element.prototype.after,ka=window.Element.prototype.replaceWith,la=window.Element.prototype.remove,ma=window.HTMLElement,I=Object.getOwnPropertyDescriptor(window.HTMLElement.prototype,"innerHTML"),na=window.HTMLElement.prototype.insertAdjacentElement,oa=window.HTMLElement.prototype.insertAdjacentHTML;var pa=new Set;"annotation-xml color-profile font-face font-face-src font-face-uri font-face-format font-face-name missing-glyph".split(" ").forEach(function(a){return pa.add(a)});function qa(a){var b=pa.has(a);a=/^[a-z][.0-9_a-z]*-[-.0-9_a-z]*$/.test(a);return!b&&a}var ra=document.contains?document.contains.bind(document):document.documentElement.contains.bind(document.documentElement); 25 | function J(a){var b=a.isConnected;if(void 0!==b)return b;if(ra(a))return!0;for(;a&&!(a.__CE_isImportDocument||a instanceof Document);)a=a.parentNode||(window.ShadowRoot&&a instanceof ShadowRoot?a.host:void 0);return!(!a||!(a.__CE_isImportDocument||a instanceof Document))}function K(a){var b=a.children;if(b)return Array.prototype.slice.call(b);b=[];for(a=a.firstChild;a;a=a.nextSibling)a.nodeType===Node.ELEMENT_NODE&&b.push(a);return b} 26 | function L(a,b){for(;b&&b!==a&&!b.nextSibling;)b=b.parentNode;return b&&b!==a?b.nextSibling:null} 27 | function M(a,b,c){for(var f=a;f;){if(f.nodeType===Node.ELEMENT_NODE){var d=f;b(d);var e=d.localName;if("link"===e&&"import"===d.getAttribute("rel")){f=d.import;void 0===c&&(c=new Set);if(f instanceof Node&&!c.has(f))for(c.add(f),f=f.firstChild;f;f=f.nextSibling)M(f,b,c);f=L(a,d);continue}else if("template"===e){f=L(a,d);continue}if(d=d.__CE_shadowRoot)for(d=d.firstChild;d;d=d.nextSibling)M(d,b,c)}f=f.firstChild?f.firstChild:L(a,f)}};function N(){var a=!(null===O||void 0===O||!O.noDocumentConstructionObserver),b=!(null===O||void 0===O||!O.shadyDomFastWalk);this.h=[];this.a=[];this.f=!1;this.shadyDomFastWalk=b;this.C=!a}function P(a,b,c,f){var d=window.ShadyDom;if(a.shadyDomFastWalk&&d&&d.inUse){if(b.nodeType===Node.ELEMENT_NODE&&c(b),b.querySelectorAll)for(a=d.nativeMethods.querySelectorAll.call(b,"*"),b=0;b document.querySelectorAll('tr[data-is-parent-step=true]'); 19 | const getParentStep = name => document.querySelector(`tr[data-step=${name}][data-is-parent-step=true]`); 20 | 21 | const goToAnchor = () => { 22 | if (location.hash) { 23 | const elem = document.querySelector(location.hash); 24 | if (elem) { 25 | if(location.hash.includes('logsL')) { 26 | // We open the parent before 27 | const stepParent = getParentStep(elem.dataset.step); 28 | toggleStep(stepParent, true); 29 | elem.scrollIntoView({block: 'center', inline: 'center', behavior: 'smooth'}); 30 | elem.classList.add(cssLineSelected); 31 | return true; 32 | } 33 | } 34 | } 35 | return false; 36 | }; 37 | 38 | const toggleClassName = (selector, cssClass) => { 39 | const element = document.querySelector(selector); 40 | if (element.classList.contains(cssClass)) { 41 | element.classList.remove(cssClass); 42 | } else { 43 | element.classList.add(cssClass); 44 | } 45 | }; 46 | 47 | const toggleStep = (stepElement, toOpen = null) => { 48 | const stepName = stepElement.dataset.step; 49 | 50 | if ((stepElement.dataset.open === 'true' && toOpen === null) || toOpen === false) { 51 | stepElement.dataset.open = false; 52 | document.querySelectorAll(`tr[data-step=${stepName}]`).forEach(childStep => childStep.classList.add('step-line-hidden')); 53 | } else { 54 | stepElement.dataset.open = true; 55 | document.querySelectorAll(`tr[data-step=${stepName}]`).forEach(childStep => childStep.classList.remove('step-line-hidden')); 56 | } 57 | }; 58 | 59 | // Listeners 60 | 61 | const addClickEventToStep = () => document.getElementById('toggle-steps').addEventListener('click', toggleAllSteps); 62 | const addClickOpenTrace = () => { 63 | const elem = document.getElementById('open-trace'); 64 | if (elem) { 65 | elem.addEventListener('click', openTrace); 66 | } 67 | }; 68 | const addClickShowTimeline = () => document.getElementById('show-timeline').addEventListener('click', showTimeline); 69 | const addLinks = () => document.querySelectorAll('.log-number').forEach(elem => elem.addEventListener('click', onClickLineNumber)); 70 | 71 | const addScrollEvent = () => { 72 | window.addEventListener('scroll', function(e) { 73 | if (window.scrollY > 300) { 74 | stickyHeader.classList.add('sticky-header'); 75 | stickyOption.classList.add('sticky-option') 76 | } else if (window.scrollY < 300) { 77 | stickyHeader.classList.remove('sticky-header'); 78 | stickyOption.classList.remove('sticky-option') 79 | } 80 | }); 81 | }; 82 | 83 | const addStageStepsLinksEvent = () => { 84 | const openShowTimeline = () => { 85 | document.querySelector('#pipeline-timeline').classList.remove('steps-hidden'); 86 | }; 87 | document.querySelectorAll('.stages .stage-steps-link').forEach(link => link.addEventListener('click', openShowTimeline)); 88 | 89 | document.querySelectorAll('.link-to-console').forEach(link => { 90 | link.addEventListener('click', () => { 91 | const stepToOpen = document.querySelector(link.getAttribute('href')); 92 | toggleStep(stepToOpen.parentElement, true); 93 | }); 94 | }); 95 | }; 96 | 97 | // Options 98 | 99 | const openTrace = () => { 100 | const elem = document.getElementById('open-trace'); 101 | if (elem) { 102 | const traceURL = elem.getAttribute('href'); 103 | window.open(traceURL); 104 | } 105 | }; 106 | 107 | const showTimeline = () => toggleClassName('#pipeline-timeline', 'steps-hidden'); 108 | 109 | const addColorThemeOption = () => { 110 | const themeSwitch = document.querySelector("#theme-switch"); 111 | themeSwitch.addEventListener('click', (e) => { 112 | if(e.target.checked) { 113 | logsTable.classList.add('logs-dark-theme'); 114 | localStorage.setItem('logs-dark-theme', true); 115 | } else { 116 | logsTable.classList.remove('logs-dark-theme'); 117 | localStorage.removeItem('logs-dark-theme'); 118 | } 119 | }); 120 | 121 | // Init 122 | if (localStorage.getItem('logs-dark-theme')) { 123 | themeSwitch.click(); 124 | } 125 | }; 126 | 127 | const onClickParentStep = event => toggleStep(event.currentTarget); 128 | 129 | const onClickLineNumber = event => { 130 | const elem = event.target; 131 | 132 | if (location.hash) { 133 | const previousClicked = document.querySelector(location.hash); 134 | previousClicked.classList.remove(cssLineSelected); 135 | } 136 | 137 | history.pushState(null, null, `#logsL${elem.dataset.lineNumber}`); 138 | elem.parentElement.classList.add(cssLineSelected); 139 | }; 140 | 141 | const toggleAllSteps = () => { 142 | allIsOpen = !allIsOpen; 143 | getAllParentSteps().forEach(step => toggleStep(step, allIsOpen)); 144 | }; 145 | 146 | const generateDownloadLink = (logs) => { 147 | var blob = new Blob([logs], { type : "text/plain;charset=utf-8"}); 148 | downloadUrl = URL.createObjectURL(blob); 149 | 150 | downloadLink.setAttribute("href", downloadUrl); 151 | }; 152 | 153 | // Fetch + Read + Enhance logs 154 | 155 | const updateStepStatusIcon = () => { 156 | const currentRunning = document.querySelector('.log-status-icon[data-status=Running]'); 157 | if(currentRunning) { 158 | const containerId = "log-" + currentRunning.parentElement.dataset.step; 159 | currentRunning.dataset.status = STEPS[containerId] ? STEPS[containerId].status : ''; 160 | } 161 | if(currentStep){ 162 | document.querySelector(`tr[data-step=${currentStep}][data-is-parent-step=true] .log-status-icon`).dataset.status = 'Running'; 163 | } 164 | }; 165 | 166 | const transformLogIntoHtml = (lineNumber, text, type='') => { 167 | let containerId = ''; 168 | if (text.startsWith('Showing logs for build ')) { 169 | const regex = /\[32m([^\[])+\[0m/g; 170 | const matches = text.match(regex); 171 | if (matches.length == 3) { 172 | const stage = matches[1].replace('[32m', '').replace('[0m', '').slice(0, -1); 173 | const container = matches[2].replace('[32m', '').replace('[0m', '').slice(0, -1); 174 | containerId = "log-" + stage + "-" + container; 175 | currentStep = stage + "-" + container; 176 | } 177 | } 178 | 179 | let cssClass = 'step-line-hidden'; 180 | if (type === 'line-error') { 181 | cssClass = 'step-line'; 182 | } 183 | 184 | const html = ansi_up.ansi_to_html(text) 185 | 186 | // Transform url to link element 187 | const transformedText = html.replace(/(https?:\/\/\S+)/g, '$1'); 188 | 189 | return ` 190 | 191 | 192 | 193 | 194 | ${STEPS[containerId] ? STEPS[containerId].timer : ''} 195 | 196 | ${transformedText} 197 | 198 | 199 | `; 200 | } 201 | 202 | const transformLogsIntoHtml = (logsString, type='', givenIndex) => 203 | logsString 204 | .split('\n') 205 | .slice(1, -1) 206 | .map((line, index) => transformLogIntoHtml(givenIndex ? givenIndex() : index+1, line, type)) 207 | .join('\n'); 208 | 209 | const loadByBuildLogUrl = () => { 210 | const hostnameWithoutLogin = window.location.origin.replace(/\/\/[^@]*@/, '//'); 211 | 212 | fetch(`${hostnameWithoutLogin}${LOGS_URL}/logs`).then((response) => { 213 | if (response.status == 404) { 214 | throw new Error('Archived logs not found in the long term storage'); 215 | } 216 | if (!response.ok) { 217 | throw new Error('Failed to retrieve the archived logs from the long term storage: ' + response.status + ' ' + response.statusText); 218 | } 219 | return response.text(); 220 | }).then((response) => { 221 | logs.innerHTML = transformLogsIntoHtml(response); 222 | addLinks(); 223 | goToAnchor(); 224 | generateDownloadLink(response); 225 | getAllParentSteps().forEach(parentStep => parentStep.addEventListener('click', onClickParentStep)); 226 | }).catch((error) => { 227 | logs.innerHTML = transformLogIntoHtml(0, error.toString(), 'line-error'); 228 | }); 229 | }; 230 | 231 | const loadByEventSource = () => { 232 | const eventSource = new EventSource(`${LOGS_URL}/logs/live`); 233 | let lineNumber = 0; 234 | let logsBuffer = ""; 235 | let getAnchor = false; 236 | let isFinished = false; 237 | 238 | downloadLink.remove(); 239 | 240 | const repeatOften = () => { 241 | if(logsBuffer) { 242 | if(lineNumber === 0) { 243 | logs.innerHTML = ""; 244 | } 245 | 246 | logs.insertAdjacentHTML('beforeend', transformLogsIntoHtml(logsBuffer, '', () => ++lineNumber)); 247 | addLinks(); 248 | getAllParentSteps().forEach(parentStep => parentStep.addEventListener('click', onClickParentStep)); 249 | 250 | if (!getAnchor) { 251 | getAnchor = goToAnchor(); 252 | } 253 | 254 | if(followLogsCheckbox.checked) { 255 | const lastLog = document.getElementById(`logsL${lineNumber}`); 256 | getAllParentSteps().forEach(step => toggleStep(step, false)); 257 | if(currentStep) { 258 | // Open current step 259 | toggleStep(getParentStep(currentStep), true); 260 | } 261 | // Update step status icon 262 | updateStepStatusIcon(); 263 | lastLog.scrollIntoView({block: 'end', inline: 'end', behavior: 'smooth'}); 264 | } 265 | 266 | logsBuffer = ""; 267 | } 268 | if(!isFinished) { 269 | requestAnimationFrame(repeatOften); 270 | } 271 | }; 272 | 273 | eventSource.addEventListener("log", function(e) { 274 | logsBuffer += e.data + "\n"; 275 | }, {passive: true}); 276 | eventSource.addEventListener("error", function(e) { 277 | logs.innerHTML = transformLogIntoHtml(0, e.data, 'line-error'); 278 | }); 279 | eventSource.addEventListener("EOF", function(e) { 280 | eventSource.close(); 281 | isFinished = true; 282 | }); 283 | 284 | // Waiting the next animation frame to add DOM element 285 | requestAnimationFrame(repeatOften); 286 | }; 287 | 288 | // Init 289 | 290 | const init = () => { 291 | addScrollEvent(); 292 | addColorThemeOption(); 293 | addClickEventToStep(); 294 | 295 | if (!ARCHIVE) { 296 | addStageStepsLinksEvent(); 297 | addClickShowTimeline(); 298 | addClickOpenTrace(); 299 | } 300 | 301 | if (BUILD_LOG_URL) { 302 | loadByBuildLogUrl(); 303 | } else { 304 | loadByEventSource(); 305 | } 306 | } 307 | 308 | // Run 309 | init(); 310 | })(); -------------------------------------------------------------------------------- /web/static/pipeline/main.css: -------------------------------------------------------------------------------- 1 | .option-button, 2 | .option-button:visited, 3 | .option-button:focus, 4 | .option-button:link { 5 | border: 1px solid var(--main-color); 6 | color: var(--main-color); 7 | padding: 8px; 8 | cursor: pointer; 9 | line-height: 16px; 10 | background-color: var(--color-background); 11 | } 12 | 13 | .option-button:hover { 14 | border: 1px solid var(--color-background); 15 | color: var(--color-background); 16 | text-decoration: none; 17 | background-color: var(--main-color); 18 | } 19 | 20 | .option-button:focus { 21 | outline: none; 22 | } 23 | 24 | .line-text { 25 | overflow: visible; 26 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; 27 | font-size: 12px; 28 | color: var(--color-text-log); 29 | white-space: pre-wrap; 30 | word-break: break-word; 31 | } 32 | 33 | .line-error { 34 | color: var(--color-error); 35 | } 36 | 37 | .log-number { 38 | width: 1%; 39 | min-width: 50px; 40 | padding-right: 10px; 41 | padding-left: 10px; 42 | font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; 43 | font-size: 12px; 44 | line-height: 20px; 45 | color: var(--color-line-number); 46 | text-align: right; 47 | white-space: nowrap; 48 | vertical-align: top; 49 | cursor: pointer; 50 | user-select: none; 51 | } 52 | 53 | .log-number:before { 54 | content: attr(data-line-number); 55 | } 56 | 57 | .log-number:hover { 58 | color: var(--color-line-number-hover); 59 | } 60 | 61 | .log-line { 62 | padding-left: 10px; 63 | position: relative; 64 | padding-right: 10px; 65 | line-height: 20px; 66 | vertical-align: top; 67 | } 68 | 69 | .logs-table { 70 | --color-text-log: #24292e; 71 | --color-bg-log: #ffffff; 72 | 73 | --color-line-number: #1b1f234d; 74 | --color-line-number-hover: #1b1f2399; 75 | --color-line-selected: #6874f970; 76 | 77 | background-color: var(--color-bg-log); 78 | padding-top: 20px; 79 | width: 100%; 80 | } 81 | 82 | .logs-dark-theme { 83 | --color-bg-log: #1e1e1e; 84 | --color-text-log: #f7f7f7; 85 | --color-line-number: #d0d0d0; 86 | --color-line-number-hover: #ffffff; 87 | } 88 | 89 | .logs-options { 90 | display: flex; 91 | flex-direction: row-reverse; 92 | background-color: var(--color-background); 93 | padding: 10px; 94 | border-bottom: 1px solid var(--color-background-darker); 95 | padding:5px; 96 | } 97 | 98 | .logs-options > div { 99 | margin: 5px; 100 | margin-top: auto; 101 | margin-bottom: auto; 102 | } 103 | 104 | .logs-options .color-theme-option { 105 | width: 90px; 106 | height: 20px; 107 | margin-left: 40px; 108 | margin-top: auto; 109 | margin-bottom: auto; 110 | } 111 | 112 | .selected-line .log-number { 113 | position: relative; 114 | } 115 | 116 | .selected-line .log-number:after { 117 | position: absolute; 118 | top: 0; 119 | left: 0; 120 | display: block; 121 | width: 100%; 122 | height: 100%; 123 | content: ""; 124 | background: var(--color-line-selected); 125 | border-bottom: 1px solid var(--main-color); 126 | border-top: 1px solid var(--main-color); 127 | border-left: 1px solid var(--main-color); 128 | } 129 | 130 | .selected-line .log-line:after { 131 | position: absolute; 132 | top: 0; 133 | left: 0; 134 | display: block; 135 | width: 100%; 136 | height: 100%; 137 | content: ""; 138 | background: var(--color-line-selected); 139 | border-bottom: 1px solid var(--main-color); 140 | border-top: 1px solid var(--main-color); 141 | border-right: 1px solid var(--main-color); 142 | } 143 | 144 | .steps { 145 | background: var(--color-background); 146 | } 147 | 148 | .pipeline-card ul li span.title { 149 | display: inline-block; 150 | width: 100px; 151 | } 152 | 153 | .pipeline-card ul.stages { 154 | list-style-type: decimal; 155 | } 156 | 157 | .pipeline-card a img.author-avatar { 158 | margin-right: 5px; 159 | } 160 | 161 | .stage { 162 | transition: all 0.3s ease; 163 | opacity: 1; 164 | } 165 | 166 | .stage .stage-name { 167 | font-weight: bold; 168 | } 169 | 170 | .steps-hidden { 171 | width: 0; 172 | height: 0; 173 | opacity: 0; 174 | display: none; 175 | } 176 | 177 | .step-line-hidden[data-is-parent-step=false] { 178 | display: none; 179 | visibility: hidden; 180 | } 181 | 182 | tr[data-is-parent-step=true] { 183 | cursor: pointer; 184 | } 185 | 186 | .log-timer { 187 | width: 55px; 188 | text-align: right; 189 | color: var(--main-color-lighter); 190 | font-size: 12px; 191 | } 192 | 193 | .log-dropdown-icon { 194 | font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace; 195 | cursor: pointer; 196 | user-select: none; 197 | width: 15px; 198 | } 199 | 200 | tr[data-is-parent-step=true] .log-dropdown-icon::before, 201 | tr[data-is-parent-step=true][data-open=false] .log-dropdown-icon::before { 202 | content: '+'; 203 | } 204 | 205 | tr[data-is-parent-step=true][data-open=true] .log-dropdown-icon::before { 206 | content: '-'; 207 | } 208 | 209 | tr[data-is-parent-step=true] .log-status-icon[data-status=Succeeded]::before { 210 | content: '\2713'; 211 | color: var(--color-success); 212 | } 213 | 214 | tr[data-is-parent-step=true] .log-status-icon[data-status=Failed]::before { 215 | content: '\2715'; 216 | color: var(--color-error); 217 | font-size: 15px; 218 | } 219 | 220 | tr[data-is-parent-step=true] .log-status-icon[data-status=Running] { 221 | transform-origin: 50% 50%; 222 | animation: rotate 1.5s linear infinite; 223 | text-align: center; 224 | } 225 | 226 | tr[data-is-parent-step=true] .log-status-icon[data-status=Running]::before { 227 | content: '\21BA'; 228 | color: var(--color-running); 229 | font-size: 17px; 230 | bottom: 2px; 231 | position: relative; 232 | } 233 | 234 | @keyframes rotate { 235 | from{ 236 | transform: rotate(0deg); 237 | } 238 | to{ 239 | transform: rotate(-360deg); 240 | } 241 | } 242 | 243 | .header-hidden { 244 | height: 0; 245 | overflow: hidden; 246 | transition: height 0.3s ease; 247 | } 248 | 249 | /* Card Header */ 250 | 251 | .pipeline-card span.title { 252 | display: flex; 253 | justify-content: space-between; 254 | } 255 | 256 | .card-header .option-button { 257 | padding: 5px; 258 | font-size: 14px; 259 | line-height: 10px; 260 | } 261 | 262 | .timeline { 263 | margin-top: 25px; 264 | } 265 | 266 | /* Sticky header */ 267 | 268 | .sticky-header { 269 | position: fixed; 270 | display: flex; 271 | top: 0; 272 | right: 0; 273 | left: 50px; 274 | height: 50px; 275 | z-index: 100; 276 | height: 50px; 277 | content: ""; 278 | background-color: var(--color-background); 279 | border-bottom: 1px solid var(--color-background-darker); 280 | } 281 | 282 | .sticky-option { 283 | position: fixed; 284 | z-index: 101; 285 | top: 12px; 286 | right: 30px; 287 | } 288 | 289 | .sticky-header h1 { 290 | margin: 0 10px; 291 | font-size: 15px; 292 | } 293 | 294 | .sticky-header h1 a{ 295 | font-size: 15px; 296 | } 297 | 298 | /* Theme switch */ 299 | 300 | .theme-switch__input, 301 | .theme-switch__label { 302 | position: absolute; 303 | z-index: 1; 304 | } 305 | 306 | .theme-switch__input { 307 | opacity: 0; 308 | } 309 | 310 | .theme-switch__input:hover + .theme-switch__label, 311 | .theme-switch__input:focus + .theme-switch__label { 312 | background-color: var(--color-grey); 313 | } 314 | 315 | .theme-switch__input:hover + .theme-switch__label span::after, 316 | .theme-switch__input:focus + .theme-switch__label span::after { 317 | background-color: lighten(lightBlue, 10%); 318 | } 319 | 320 | .theme-switch__label { 321 | transition: background-color 200ms ease-in-out; 322 | width: 50px; 323 | height: 20px; 324 | border-radius: 50px; 325 | text-align: center; 326 | background-color: var(--color-background-darker); 327 | } 328 | 329 | .theme-switch__label::before, 330 | .theme-switch__label::after { 331 | font-size: 1rem; 332 | position: absolute; 333 | transform: translate3d(0, -50%, 0); 334 | top: 50%; 335 | } 336 | 337 | .theme-switch__label::before { 338 | content: '\263C'; 339 | right: 100%; 340 | margin-right: 10px; 341 | color: orange; 342 | } 343 | 344 | .theme-switch__label::after { 345 | content: '\263E'; 346 | left: 100%; 347 | margin-left: 10px; 348 | color: lightSlateGray; 349 | } 350 | 351 | .theme-switch__label span { 352 | position: absolute; 353 | bottom: calc(100% + 10px); 354 | left: 0; 355 | width: 100%; 356 | } 357 | 358 | .theme-switch__label span::after { 359 | position: absolute; 360 | top: calc(100% + 12px); 361 | left: 5px; 362 | width: 15px; 363 | height: 15px; 364 | content: ''; 365 | border-radius: 50%; 366 | background-color: var(--main-color); 367 | transition: transform 200ms, background-color 200ms; 368 | } 369 | 370 | .theme-switch__input:checked ~ .theme-switch__label::before { 371 | color: lightSlateGray; 372 | } 373 | 374 | .theme-switch__input:checked ~ .theme-switch__label::after { 375 | color: turquoise; 376 | } 377 | 378 | .theme-switch__input:checked ~ .theme-switch__label span::after { 379 | transform: translate3d(25px, 0, 0); 380 | } 381 | 382 | @media (max-width: 600px) { 383 | .pipeline-root-links { 384 | display: none; 385 | } 386 | 387 | .sticky-header { 388 | left: 0; 389 | } 390 | } -------------------------------------------------------------------------------- /web/static/running/index.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | const dataTable = $('#dataTable').DataTable({ 3 | lengthMenu: [ [10, 25, 50, 100, -1], [10, 25, 50, 100, "All"] ], 4 | pageLength: 25, 5 | order: [ 6 | [0, 'desc'], 7 | [1, 'desc'], 8 | [2, 'desc'] 9 | ], 10 | columnDefs: [ 11 | { targets: 'start', visible: false } 12 | ] 13 | }); 14 | 15 | const loadByEventSource = () => { 16 | const eventSource = new EventSource(`/running/events`); 17 | 18 | eventSource.addEventListener("added", function(e) { 19 | let pipeline = JSON.parse(e.data); 20 | var row = dataTable.row.add([ 21 | '' + pipeline.Owner + '/' + pipeline.Repository + '', 22 | '' + pipeline.Branch + '', 23 | '' + pipeline.Build + '', 24 | pipeline.Context, 25 | pipeline.Stage, 26 | pipeline.Step, 27 | moment.duration(moment().diff(moment(pipeline.StepStartTime))).seconds() + 's', 28 | pipeline.StepStartTime 29 | ]); 30 | row.node().setAttribute('id', pipeline.Name + '-' + pipeline.Stage.replaceAll(' ', '-') + '-' + pipeline.Step.replaceAll(' ', '-')); 31 | row.draw(); 32 | }, {passive: true}); 33 | eventSource.addEventListener("deleted", function(e) { 34 | let pipeline = JSON.parse(e.data); 35 | let id = '#' + pipeline.Name + '-' + pipeline.Stage.replaceAll(' ', '-') + '-' + pipeline.Step.replaceAll(' ', '-'); 36 | dataTable.row(id).remove().draw(); 37 | }, {passive: true}); 38 | }; 39 | 40 | const refreshDuration = () => { 41 | dataTable.rows().every( function ( rowIdx, tableLoop, rowLoop ) { 42 | let startTime = dataTable.cell({row: rowIdx, column: 7}).data(); 43 | var duration = moment.duration(moment().diff(moment(startTime))); 44 | if (duration.asSeconds() < 60) { 45 | dataTable.cell({row: rowIdx, column: 6}).data(duration.seconds() + 's'); 46 | } else { 47 | dataTable.cell({row: rowIdx, column: 6}).data(duration.minutes() + 'm' + duration.seconds() + 's'); 48 | } 49 | }); 50 | } 51 | 52 | // Init 53 | 54 | const init = () => { 55 | loadByEventSource(); 56 | refreshDuration(); 57 | setInterval(refreshDuration, 1000); 58 | } 59 | 60 | // Run 61 | init(); 62 | })(); -------------------------------------------------------------------------------- /web/templates/archived_logs.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "css-archived_logs" }} 2 | 3 | {{ end }} 4 | 5 | {{ define "js-archived_logs" }} 6 | 12 | 13 | 14 | {{ end }} 15 | 16 | {{ define "header-archived_logs" }} 17 |

18 | 21 | 22 | Archived Logs for 23 | Pipelines > 24 | {{.Owner}} > 25 | {{.Repository}} > 26 | {{.Branch}} Build {{.Build}} 27 | 28 |

29 | {{ end }} 30 | 31 |
32 |

33 | 34 | Archived Logs for 35 | Pipelines > 36 | {{.Owner}} > 37 | {{.Repository}} > 38 | {{.Branch}} Build {{.Build}} 39 | 40 |

41 |
42 | 43 |
44 |
45 |
46 | 47 | 50 |
51 | 52 |
53 | 54 | View raw logs 55 | Download raw logs 56 |
57 |
58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 69 | 70 | 71 |
67 | Loading the logs... 68 |
72 |
73 | -------------------------------------------------------------------------------- /web/templates/home.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "css-home" }} 2 | 3 | 4 | {{ end }} 5 | 6 | {{ define "js-home" }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{ end }} 14 | 15 | {{ define "header-home" }} 16 |

17 | 20 | Pipelines 21 | {{ if (isAvailable . "Owner") }} 22 | > {{ .Owner }} 23 | {{ if (isAvailable . "Repository") }} 24 | > {{ .Repository }} 25 | {{ if (isAvailable . "Branch") }} 26 | > {{ .Branch }} 27 | {{ end }} 28 | {{ end }} 29 | {{ end }} 30 | {{ if and (isAvailable . "Query") .Query }} 31 | > {{ .Query }} 32 | {{ end }} 33 |

34 | 35 | 41 | {{ end }} 42 | 43 |
44 |
45 |
46 |
47 | Top Statuses 48 |
    49 | {{- range (sortPipelineCounts .Pipelines.Counts.Statuses) -}} 50 | {{- if and .value -}} 51 |
  • 52 | {{ .value }} 53 | 54 | {{- if or (eq .key "Other") (eq .key "") -}} 55 | {{ .key | default "None" }} 56 | {{- else -}} 57 | {{ .key }} 58 | {{- end -}} 59 | 60 |
  • 61 | {{- end -}} 62 | {{- end -}} 63 |
64 |
65 |
66 |
67 |
68 | Top Repositories 69 |
    70 | {{- range (sortPipelineCounts .Pipelines.Counts.Repositories) -}} 71 | {{- if and .key .value -}} 72 |
  • 73 | {{ .value }} 74 | 75 | {{- if eq .key "Other" -}} 76 | {{ .key }} 77 | {{- else -}} 78 | {{ .key }} 79 | {{- end -}} 80 | 81 |
  • 82 | {{- end -}} 83 | {{- end -}} 84 |
85 |
86 |
87 |
88 |
89 | Top Authors 90 |
    91 | {{- range (sortPipelineCounts .Pipelines.Counts.Authors) -}} 92 | {{- if and .key .value -}} 93 |
  • 94 | {{ .value }} 95 | 96 | {{- if eq .key "Other" -}} 97 | {{ .key }} 98 | {{- else -}} 99 | {{ .key }} 100 | {{- end -}} 101 | 102 |
  • 103 | {{- end -}} 104 | {{- end -}} 105 |
106 |
107 |
108 |
109 |
110 | Top Durations 111 |
    112 | {{- range (sortPipelineCounts .Pipelines.Counts.Durations) -}} 113 | {{- if and .key .value -}} 114 |
  • 115 | {{ .value }} 116 | {{ .key }} 117 |
  • 118 | {{- end -}} 119 | {{- end -}} 120 |
121 |
122 |
123 |
124 |
125 |
126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | {{ range .Pipelines.Pipelines }} 142 | 143 | 151 | 166 | 167 | 168 | 169 | 176 | 185 | 186 | 187 | 188 | {{ end }} 189 | 190 |
RepositoryBranchBuildContextStatusStartEndDuration
144 | {{ .Owner }}/{{ .Repository }} 145 | {{- if repositoryURL . -}} 146 | 147 | 148 | 149 | {{- end -}} 150 | 152 | {{- if authorURL . -}} 153 | 154 | {{- else -}} 155 | 156 | {{- end -}} 157 | 158 | {{ .Branch }} 159 | 160 | {{- if branchURL . -}} 161 | 162 | 163 | 164 | {{- end -}} 165 | {{ .Build }}{{ .Context }}{{ .Status }} 170 | {{- if (vdate .Start).IsToday -}} 171 | {{ .Start.Format "15:04:05" }} 172 | {{- else -}} 173 | {{ .Start.Format "2006-01-02 15:04:05" }} 174 | {{- end -}} 175 | 177 | {{- if not .End.IsZero }} 178 | {{- if (vdate .End).IsToday -}} 179 | {{ .End.Format "15:04:05" }} 180 | {{- else -}} 181 | {{ .End.Format "2006-01-02 15:04:05" }} 182 | {{- end -}} 183 | {{- end -}} 184 | {{ with .Duration }}{{ . }}{{ end }}{{ .Author }}
191 |
-------------------------------------------------------------------------------- /web/templates/layout.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ partial "css" }} 4 | 5 | 6 | 7 | 8 | 9 | {{ partial "js" }} 10 | 11 | 12 | 13 |
14 |
15 | {{ partial "header" }} 16 |
17 | {{ yield }} 18 | 24 |
25 | 26 | -------------------------------------------------------------------------------- /web/templates/running.tmpl: -------------------------------------------------------------------------------- 1 | {{ define "css-running" }} 2 | 3 | 4 | {{ end }} 5 | 6 | {{ define "js-running" }} 7 | 8 | 9 | 10 | 11 | 12 | {{ end }} 13 | 14 | {{ define "header-running" }} 15 |

16 | 19 | Pipelines 20 | > Running 21 |

22 | {{ end }} 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {{ range .Pipelines }} 40 | 41 | 44 | 47 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {{ end }} 57 | 58 |
RepositoryBranchBuildContextStageStepDurationStart
42 | {{ .Owner }}/{{ .Repository }} 43 | 45 | {{ .Branch }} 46 | 48 | {{ .Build }} 49 | {{ .Context }}{{ .Stage }}{{ .Step }}{{ now.Sub .StepStartTime }}{{ .StepStartTime.Format "2006-01-02 15:04:05" }}
59 |
--------------------------------------------------------------------------------