├── .dockerignore ├── CHANGELOG.md ├── Dockerfile ├── slack ├── types.go └── client.go ├── glide.yaml ├── kubernetes ├── kubernetes.go ├── types.go └── client.go ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── main.go └── glide.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !Dockerfile 3 | !bin/k8s-pod-notifier 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [v0.1.0](https://github.com/dtan4/k8s-pod-notifier/releases/tag/v0.1.0) (2017-05-23) 2 | 3 | Initial release. 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.5 2 | 3 | RUN apk add --no-cache --update ca-certificates 4 | 5 | COPY bin/k8s-pod-notifier /k8s-pod-notifier 6 | 7 | ENTRYPOINT ["/k8s-pod-notifier"] 8 | -------------------------------------------------------------------------------- /slack/types.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | // AttachmentField represents the field of Slack message attachment 4 | type AttachmentField struct { 5 | Title string 6 | Value string 7 | } 8 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/dtan4/k8s-pod-notifier 2 | import: 3 | - package: k8s.io/client-go 4 | version: ~2.0.0 5 | - package: github.com/spf13/pflag 6 | - package: github.com/pkg/errors 7 | version: ~0.8.0 8 | - package: github.com/nlopes/slack 9 | - package: github.com/sirupsen/logrus 10 | version: ~0.11.5 11 | -------------------------------------------------------------------------------- /kubernetes/kubernetes.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "k8s.io/client-go/pkg/api/v1" 5 | "k8s.io/client-go/tools/clientcmd" 6 | ) 7 | 8 | // DefaultConfigFile returns the default kubeconfig file path 9 | func DefaultConfigFile() string { 10 | return clientcmd.RecommendedHomeFile 11 | } 12 | 13 | // DefaultNamespace returns the default namespace 14 | func DefaultNamespace() string { 15 | return v1.NamespaceAll 16 | } 17 | -------------------------------------------------------------------------------- /kubernetes/types.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // PodEvent represents Pod termination event 8 | type PodEvent struct { 9 | Namespace string 10 | PodName string 11 | StartedAt time.Time 12 | FinishedAt time.Time 13 | ExitCode int 14 | Reason string 15 | Message string 16 | } 17 | 18 | // NotifyFunc represents callback function for Pod event 19 | type NotifyFunc func(event *PodEvent) error 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/go 3 | 4 | ### Go ### 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 18 | .glide/ 19 | 20 | # End of https://www.gitignore.io/api/go 21 | 22 | /bin 23 | /coverage.txt 24 | /dist 25 | /vendor 26 | /.envrc 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | language: go 5 | go: 6 | - '1.8' 7 | cache: 8 | directories: 9 | - vendor 10 | before_install: 11 | - sudo add-apt-repository ppa:masterminds/glide -y 12 | - sudo apt-get update -q 13 | - sudo apt-get install glide -y 14 | - mkdir -p $GOPATH/bin 15 | install: 16 | - make deps 17 | script: 18 | - make test 19 | before_deploy: 20 | - GOOS=linux GOARCH=amd64 CGO_ENABLED=0 make 21 | - if [ ! -z $TRAVIS_TAG ]; then make cross-build; fi 22 | - if [ ! -z $TRAVIS_TAG ]; then make dist; fi 23 | deploy: 24 | - provider: releases 25 | skip_cleanup: true 26 | api_key: $GITHUB_TOKEN 27 | file_glob: true 28 | file: 'dist/*.{tar.gz,zip}' 29 | on: 30 | tags: true 31 | - provider: script 32 | skip_cleanup: true 33 | script: make ci-docker-release 34 | on: 35 | branch: master 36 | - provider: script 37 | skip_cleanup: true 38 | script: DOCKER_IMAGE_TAG=$TRAVIS_TAG make ci-docker-release 39 | on: 40 | tags: true 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Daisuke Fujita 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 | -------------------------------------------------------------------------------- /slack/client.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "github.com/nlopes/slack" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | // Client represents the wrapper of Slack API client 9 | type Client struct { 10 | api *slack.Client 11 | } 12 | 13 | // NewClient creates Client object 14 | func NewClient(token string) *Client { 15 | return &Client{ 16 | api: slack.New(token), 17 | } 18 | } 19 | 20 | // GetChannelID retrieves internal ID of the given channel 21 | func (c *Client) GetChannelID(channel string) (string, error) { 22 | chs, err := c.api.GetChannels(true) 23 | if err != nil { 24 | return "", errors.Wrap(err, "failed to list Slack channels") 25 | } 26 | 27 | for _, ch := range chs { 28 | if ch.Name == channel { 29 | return ch.ID, nil 30 | } 31 | } 32 | 33 | return "", errors.Errorf("channel %s is not found", channel) 34 | } 35 | 36 | // PostMessageWithAttachment posts message with attachment 37 | func (c *Client) PostMessageWithAttachment(channelID, color, title, text string, fields []*AttachmentField) error { 38 | attachmentFields := []slack.AttachmentField{} 39 | 40 | for _, field := range fields { 41 | attachmentFields = append(attachmentFields, slack.AttachmentField{ 42 | Title: field.Title, 43 | Value: field.Value, 44 | Short: true, 45 | }) 46 | } 47 | 48 | params := slack.PostMessageParameters{ 49 | Attachments: []slack.Attachment{ 50 | slack.Attachment{ 51 | Title: title, 52 | Text: text, 53 | Color: color, 54 | Fields: attachmentFields, 55 | }, 56 | }, 57 | } 58 | 59 | _, _, err := c.api.PostMessage(channelID, "", params) 60 | if err != nil { 61 | return errors.Wrap(err, "failed to post message to Slack") 62 | } 63 | 64 | return nil 65 | } 66 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME := k8s-pod-notifier 2 | VERSION := v0.1.0 3 | REVISION := $(shell git rev-parse --short HEAD) 4 | 5 | SRCS := $(shell find . -name '*.go' -type f) 6 | LDFLAGS := -ldflags="-s -w -X \"main.Version=$(VERSION)\" -X \"main.Revision=$(REVISION)\"" 7 | 8 | DIST_DIRS := find * -type d -exec 9 | 10 | DOCKER_REPOSITORY := quay.io 11 | DOCKER_IMAGE_NAME := $(DOCKER_REPOSITORY)/dtan4/k8s-pod-notifier 12 | DOCKER_IMAGE_TAG ?= latest 13 | DOCKER_IMAGE := $(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG) 14 | 15 | .DEFAULT_GOAL := bin/$(NAME) 16 | 17 | bin/$(NAME): $(SRCS) 18 | go build $(LDFLAGS) -o bin/$(NAME) 19 | 20 | .PHONY: ci-docker-release 21 | ci-docker-release: docker-build 22 | @docker login -u="$(DOCKER_USERNAME)" -p="$(DOCKER_PASSWORD)" $(DOCKER_REPOSITORY) 23 | docker push $(DOCKER_IMAGE) 24 | 25 | .PHONY: clean 26 | clean: 27 | rm -rf bin/* 28 | rm -rf vendor/* 29 | 30 | .PHONY: cross-build 31 | cross-build: 32 | for os in darwin linux windows; do \ 33 | for arch in amd64 386; do \ 34 | GOOS=$$os GOARCH=$$arch CGO_ENABLED=0 go build $(LDFLAGS) -o dist/$$os-$$arch/$(NAME); \ 35 | done; \ 36 | done 37 | 38 | .PHONY: deps 39 | deps: glide 40 | glide install 41 | 42 | .PHONY: dist 43 | dist: 44 | cd dist && \ 45 | $(DIST_DIRS) cp ../LICENSE {} \; && \ 46 | $(DIST_DIRS) cp ../README.md {} \; && \ 47 | $(DIST_DIRS) tar -zcf $(NAME)-$(VERSION)-{}.tar.gz {} \; && \ 48 | $(DIST_DIRS) zip -r $(NAME)-$(VERSION)-{}.zip {} \; && \ 49 | cd .. 50 | 51 | .PHONY: docker-build 52 | docker-build: 53 | ifeq ($(findstring ELF 64-bit LSB,$(shell file bin/$(NAME) 2> /dev/null)),) 54 | @echo "bin/$(NAME) is not a binary of Linux 64bit binary." 55 | @exit 1 56 | endif 57 | docker build -t $(DOCKER_IMAGE) . 58 | 59 | .PHONY: glide 60 | glide: 61 | ifeq ($(shell command -v glide 2> /dev/null),) 62 | curl https://glide.sh/get | sh 63 | endif 64 | 65 | .PHONY: install 66 | install: 67 | go install $(LDFLAGS) 68 | 69 | .PHONY: release 70 | release: 71 | git tag $(VERSION) 72 | git push origin $(VERSION) 73 | 74 | .PHONY: test 75 | test: 76 | go test -cover -v `glide novendor` 77 | 78 | .PHONY: update-deps 79 | update-deps: glide 80 | glide update 81 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # k8s-pod-notifier 2 | 3 | [![Build Status](https://travis-ci.org/dtan4/k8s-pod-notifier.svg?branch=master)](https://travis-ci.org/dtan4/k8s-pod-notifier) 4 | [![Docker Repository on Quay](https://quay.io/repository/dtan4/k8s-pod-notifier/status "Docker Repository on Quay")](https://quay.io/repository/dtan4/k8s-pod-notifier) 5 | 6 | Notify Pod status to Slack 7 | 8 | ## Requirements 9 | 10 | - Kubernetes 1.3 or above 11 | - Slack API (OAuth2) access token 12 | - Permission scopes `channels:read` and `chat:write:bot` are required 13 | 14 | ## Installation 15 | 16 | ### From source 17 | 18 | ```bash 19 | $ go get -d github.com/dtan4/k8s-pod-notifier 20 | $ cd $GOPATH/src/github.com/dtan4/k8s-pod-notifier 21 | $ make deps 22 | $ make install 23 | ``` 24 | 25 | ### Run in a Docker container 26 | 27 | Docker image is available at [quay.io/dtan4/k8s-pod-notifier](https://quay.io/repository/dtan4/k8s-pod-notifier). 28 | 29 | ```bash 30 | # -t is required to colorize logs 31 | $ docker run \ 32 | --rm \ 33 | -t \ 34 | -v $HOME/.kube/config:/.kube/config \ 35 | quay.io/dtan4/k8s-pod-notifier:latest 36 | ``` 37 | 38 | ## Usage 39 | 40 | ### In Kubernetes cluster 41 | 42 | Just add `--in-cluster` flag. 43 | 44 | Deployment manifest sample: 45 | 46 | ```yaml 47 | apiVersion: extensions/v1beta1 48 | kind: Deployment 49 | metadata: 50 | name: k8s-pod-notifier 51 | spec: 52 | minReadySeconds: 30 53 | strategy: 54 | type: RollingUpdate 55 | replicas: 1 56 | template: 57 | metadata: 58 | name: k8s-pod-notifier 59 | labels: 60 | name: k8s-pod-notifier 61 | role: daemon 62 | spec: 63 | containers: 64 | - image: quay.io/dtan4/k8s-pod-notifier:latest 65 | name: k8s-pod-notifier 66 | env: 67 | - name: SLACK_API_TOKEN 68 | valueFrom: 69 | secretKeyRef: 70 | name: k8s-pod-notifier 71 | key: slack-api-token 72 | - name: SLACK_CHANNEL 73 | valueFrom: 74 | secretKeyRef: 75 | name: k8s-pod-notifier 76 | key: slack-channel 77 | command: 78 | - "/k8s-pod-notifier" 79 | - "--in-cluster" 80 | - "--fail" 81 | - "--labels" 82 | - "role=job" 83 | ``` 84 | 85 | ### Local machine 86 | 87 | k8s-pod-notifier uses `~/.kube/config` as default. You can specify another path by `KUBECONFIG` environment variable or `--kubeconfig` option. `--kubeconfig` option always overrides `KUBECONFIG` environment variable. 88 | 89 | ```bash 90 | $ export SLACK_API_TOKEN=xxxxx 91 | $ export SLACK_CHANNEL=notifications 92 | $ KUBECONFIG=/path/to/kubeconfig k8s-pod-notifier 93 | # or 94 | $ k8s-pod-notifier --kubeconfig=/path/to/kubeconfig 95 | ``` 96 | 97 | ### Options 98 | 99 | |Option|Description|Required|Default| 100 | |---------|-----------|-------|-------| 101 | |`--context=CONTEXT`|Kubernetes context||| 102 | |`--in-cluster`|Execute in Kubernetes cluster||| 103 | |`--kubeconfig=KUBECONFIG`|Path of kubeconfig||`~/.kube/config`| 104 | |`--labels=LABELS`|Label filter query (e.g. `app=APP,role=ROLE`)||| 105 | |`--namespace=NAMESPACE`|Kubernetes namespace||All namespaces| 106 | |`--success`|Notify success of Pod only||| 107 | |`--fail`|Notify failure of Pod only||| 108 | |`--slack-api-token=SLACK_API_TOKEN`|Slack API token|Required, or set `SLACK_API_TOKEN` env|| 109 | |`--slack-channel=SLACK_CHANNEL`|Slack channel to post|Required, or set `SLACK_CHANNEL` env|| 110 | |`-h`, `-help`|Print command line usage||| 111 | |`-v`, `-version`|Print version||| 112 | 113 | ## Development 114 | 115 | Go 1.7 or above is required. 116 | Clone this repository and build using `make`. 117 | 118 | ```bash 119 | $ go get -d github.com/dtan4/k8s-pod-notifier 120 | $ cd $GOPATH/src/github.com/dtan4/k8s-pod-notifier 121 | $ make 122 | ``` 123 | 124 | ## Author 125 | 126 | Daisuke Fujita ([@dtan4](https://github.com/dtan4)) 127 | 128 | ## License 129 | 130 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) 131 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "strconv" 9 | 10 | k8s "github.com/dtan4/k8s-pod-notifier/kubernetes" 11 | "github.com/dtan4/k8s-pod-notifier/slack" 12 | log "github.com/sirupsen/logrus" 13 | flag "github.com/spf13/pflag" 14 | ) 15 | 16 | func main() { 17 | var ( 18 | debug bool 19 | kubeContext string 20 | inCluster bool 21 | kubeconfig string 22 | labels string 23 | namespace string 24 | notifyFail bool 25 | notifySuccess bool 26 | slackAPIToken string 27 | slackChannel string 28 | ) 29 | 30 | flags := flag.NewFlagSet("k8s-pod-notifier", flag.ExitOnError) 31 | flags.Usage = func() { 32 | flags.PrintDefaults() 33 | } 34 | 35 | flags.BoolVar(&debug, "debug", false, "Debug mode") 36 | flags.StringVar(&kubeContext, "context", "", "Kubernetes context") 37 | flags.BoolVar(&inCluster, "in-cluster", false, "Execute in Kubernetes cluster") 38 | flags.StringVar(&kubeconfig, "kubeconfig", "", "Path of kubeconfig") 39 | flags.StringVarP(&labels, "labels", "l", "", "Label filter query") 40 | flags.StringVarP(&namespace, "namespace", "n", "", "Kubernetes namespace") 41 | flags.BoolVar(¬ifyFail, "fail", false, "Notify failure of Pod only") 42 | flags.BoolVar(¬ifySuccess, "success", false, "Notify success of Pod only") 43 | flags.StringVar(&slackAPIToken, "slack-api-token", "", "Slack API token") 44 | flags.StringVar(&slackChannel, "slack-channel", "", "Slack channel to post") 45 | 46 | if err := flags.Parse(os.Args[1:]); err != nil { 47 | fmt.Fprintln(os.Stderr, err) 48 | os.Exit(1) 49 | } 50 | 51 | if kubeconfig == "" { 52 | if os.Getenv("KUBECONFIG") != "" { 53 | kubeconfig = os.Getenv("KUBECONFIG") 54 | } else { 55 | kubeconfig = k8s.DefaultConfigFile() 56 | } 57 | } 58 | 59 | if slackAPIToken == "" { 60 | if os.Getenv("SLACK_API_TOKEN") == "" { 61 | fmt.Fprintln(os.Stderr, "Slack API token must be set (SLACK_API_TOKEN, --slack-api-token)") 62 | os.Exit(1) 63 | } 64 | 65 | slackAPIToken = os.Getenv("SLACK_API_TOKEN") 66 | } 67 | 68 | if slackChannel == "" { 69 | if os.Getenv("SLACK_CHANNEL") == "" { 70 | fmt.Fprintln(os.Stderr, "Slack channel must be set (SLACK_CHANNEL, --slack-channel)") 71 | os.Exit(1) 72 | } 73 | 74 | slackChannel = os.Getenv("SLACK_CHANNEL") 75 | } 76 | 77 | // Neither --fail nor --success was specified => Notify both fail and success 78 | if !(notifyFail || notifySuccess) { 79 | notifyFail = true 80 | notifySuccess = true 81 | } 82 | 83 | var k8sClient *k8s.Client 84 | 85 | if inCluster { 86 | c, err := k8s.NewClientInCluster() 87 | if err != nil { 88 | fmt.Fprintln(os.Stderr, err) 89 | os.Exit(1) 90 | } 91 | 92 | if namespace == "" { 93 | namespace = k8s.DefaultNamespace() 94 | } 95 | 96 | k8sClient = c 97 | } else { 98 | c, err := k8s.NewClient(kubeconfig, kubeContext) 99 | if err != nil { 100 | fmt.Fprintln(os.Stderr, err) 101 | os.Exit(1) 102 | } 103 | 104 | if namespace == "" { 105 | namespaceInConfig, err := c.NamespaceInConfig() 106 | if err != nil { 107 | fmt.Fprintln(os.Stderr, err) 108 | os.Exit(1) 109 | } 110 | 111 | if namespaceInConfig == "" { 112 | namespace = k8s.DefaultNamespace() 113 | } else { 114 | namespace = namespaceInConfig 115 | } 116 | } 117 | 118 | k8sClient = c 119 | } 120 | 121 | slackClient := slack.NewClient(slackAPIToken) 122 | 123 | channelID, err := slackClient.GetChannelID(slackChannel) 124 | if err != nil { 125 | fmt.Fprintln(os.Stderr, err) 126 | os.Exit(1) 127 | } 128 | 129 | sigCh := make(chan os.Signal, 1) 130 | signal.Notify(sigCh, os.Interrupt) 131 | 132 | ctx, cancel := context.WithCancel(context.Background()) 133 | defer cancel() 134 | 135 | succeededFunc := func(event *k8s.PodEvent) error { 136 | title := "Pod Succeeded" 137 | text := fmt.Sprintf("[%s] %s", event.Namespace, event.PodName) 138 | 139 | if err := slackClient.PostMessageWithAttachment(channelID, "good", title, text, []*slack.AttachmentField{ 140 | &slack.AttachmentField{ 141 | Title: "StartedAt", 142 | Value: event.StartedAt.String(), 143 | }, 144 | &slack.AttachmentField{ 145 | Title: "FinishedAt", 146 | Value: event.FinishedAt.String(), 147 | }, 148 | }); err != nil { 149 | return err 150 | } 151 | 152 | log.WithFields(log.Fields{ 153 | "namespace": event.Namespace, 154 | "name": event.PodName, 155 | "exitCode": event.ExitCode, 156 | "reason": event.Reason, 157 | }).Info("success") 158 | 159 | return nil 160 | } 161 | failedFunc := func(event *k8s.PodEvent) error { 162 | title := "Pod Failed" 163 | text := fmt.Sprintf("[%s] %s", event.Namespace, event.PodName) 164 | 165 | attachments := []*slack.AttachmentField{ 166 | &slack.AttachmentField{ 167 | Title: "StartedAt", 168 | Value: event.StartedAt.String(), 169 | }, 170 | &slack.AttachmentField{ 171 | Title: "FinishedAt", 172 | Value: event.FinishedAt.String(), 173 | }, 174 | } 175 | 176 | if event.ExitCode >= 0 { 177 | attachments = append(attachments, &slack.AttachmentField{ 178 | Title: "ExitCode", 179 | Value: strconv.Itoa(event.ExitCode), 180 | }) 181 | } 182 | 183 | if event.Reason != "" { 184 | attachments = append(attachments, &slack.AttachmentField{ 185 | Title: "Reason", 186 | Value: event.Reason, 187 | }) 188 | } 189 | 190 | if event.Message != "" { 191 | attachments = append(attachments, &slack.AttachmentField{ 192 | Title: "Message", 193 | Value: event.Message, 194 | }) 195 | } 196 | 197 | if err := slackClient.PostMessageWithAttachment(channelID, "danger", title, text, attachments); err != nil { 198 | return err 199 | } 200 | 201 | log.WithFields(log.Fields{ 202 | "namespace": event.Namespace, 203 | "name": event.PodName, 204 | "exitCode": event.ExitCode, 205 | "reason": event.Reason, 206 | }).Info("failed") 207 | 208 | return nil 209 | } 210 | 211 | log.SetFormatter(&log.TextFormatter{ 212 | FullTimestamp: true, 213 | }) 214 | 215 | if debug { 216 | log.SetLevel(log.DebugLevel) 217 | log.Debug("debug mode enabled") 218 | } 219 | 220 | log.Info("Watching...") 221 | 222 | if err := k8sClient.WatchPodEvents(ctx, namespace, labels, notifySuccess, notifyFail, succeededFunc, failedFunc); err != nil { 223 | fmt.Fprintln(os.Stderr, err) 224 | os.Exit(1) 225 | } 226 | 227 | <-sigCh 228 | } 229 | -------------------------------------------------------------------------------- /kubernetes/client.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/pkg/errors" 7 | log "github.com/sirupsen/logrus" 8 | "k8s.io/client-go/kubernetes" 9 | "k8s.io/client-go/pkg/api/v1" 10 | "k8s.io/client-go/pkg/watch" 11 | "k8s.io/client-go/rest" 12 | "k8s.io/client-go/tools/clientcmd" 13 | ) 14 | 15 | // Client represents the wrapper of Kubernetes API client 16 | type Client struct { 17 | clientConfig clientcmd.ClientConfig 18 | clientset *kubernetes.Clientset 19 | } 20 | 21 | // NewClient creates Client object using local kubecfg 22 | func NewClient(kubeconfig, context string) (*Client, error) { 23 | clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( 24 | &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig}, 25 | &clientcmd.ConfigOverrides{CurrentContext: context}) 26 | 27 | config, err := clientConfig.ClientConfig() 28 | if err != nil { 29 | return nil, errors.Wrap(err, "falied to load local kubeconfig") 30 | } 31 | 32 | clientset, err := kubernetes.NewForConfig(config) 33 | if err != nil { 34 | return nil, errors.Wrap(err, "failed to load clientset") 35 | } 36 | 37 | return &Client{ 38 | clientConfig: clientConfig, 39 | clientset: clientset, 40 | }, nil 41 | } 42 | 43 | // NewClientInCluster creates Client object in Kubernetes cluster 44 | func NewClientInCluster() (*Client, error) { 45 | config, err := rest.InClusterConfig() 46 | if err != nil { 47 | return nil, errors.Wrap(err, "failed to load kubeconfig in cluster") 48 | } 49 | 50 | clientset, err := kubernetes.NewForConfig(config) 51 | if err != nil { 52 | return nil, errors.Wrap(err, "falied to load clientset") 53 | } 54 | 55 | return &Client{ 56 | clientset: clientset, 57 | }, nil 58 | } 59 | 60 | // NamespaceInConfig returns namespace set in kubeconfig 61 | func (c *Client) NamespaceInConfig() (string, error) { 62 | if c.clientConfig == nil { 63 | return "", errors.New("clientConfig is not set") 64 | } 65 | 66 | rawConfig, err := c.clientConfig.RawConfig() 67 | if err != nil { 68 | return "", errors.Wrap(err, "failed to load rawConfig") 69 | } 70 | 71 | return rawConfig.Contexts[rawConfig.CurrentContext].Namespace, nil 72 | } 73 | 74 | // WatchPodEvents watches Pod events 75 | func (c *Client) WatchPodEvents(ctx context.Context, namespace, labels string, notifySuccess, notifyFail bool, succeededFunc, failedFunc NotifyFunc) error { 76 | watcher, err := c.clientset.Core().Pods(namespace).Watch(v1.ListOptions{ 77 | LabelSelector: labels, 78 | }) 79 | if err != nil { 80 | return errors.Wrap(err, "cannot create Pod event watcher") 81 | } 82 | 83 | go func() { 84 | for { 85 | select { 86 | case e := <-watcher.ResultChan(): 87 | if e.Object == nil { 88 | return 89 | } 90 | 91 | pod, ok := e.Object.(*v1.Pod) 92 | if !ok { 93 | continue 94 | } 95 | 96 | log.WithFields(log.Fields{ 97 | "action": e.Type, 98 | "namespace": pod.Namespace, 99 | "name": pod.Name, 100 | "phase": pod.Status.Phase, 101 | "reason": pod.Status.Reason, 102 | "container#": len(pod.Status.ContainerStatuses), 103 | }).Debug("event notified") 104 | 105 | switch e.Type { 106 | case watch.Modified: 107 | if pod.DeletionTimestamp != nil { 108 | continue 109 | } 110 | 111 | startedAt := pod.CreationTimestamp.Time 112 | 113 | switch pod.Status.Phase { 114 | case v1.PodSucceeded: 115 | for _, cst := range pod.Status.ContainerStatuses { 116 | if cst.State.Terminated == nil { 117 | continue 118 | } 119 | 120 | finishedAt := cst.State.Terminated.FinishedAt.Time 121 | 122 | if cst.State.Terminated.Reason == "Completed" { 123 | if notifySuccess { 124 | succeededFunc(&PodEvent{ 125 | Namespace: pod.Namespace, 126 | PodName: pod.Name, 127 | StartedAt: startedAt, 128 | FinishedAt: finishedAt, 129 | ExitCode: 0, 130 | Reason: "", 131 | Message: "", 132 | }) 133 | } 134 | } else { 135 | if notifyFail { 136 | failedFunc(&PodEvent{ 137 | Namespace: pod.Namespace, 138 | PodName: pod.Name, 139 | StartedAt: startedAt, 140 | FinishedAt: finishedAt, 141 | ExitCode: int(cst.State.Terminated.ExitCode), 142 | Reason: cst.State.Terminated.Reason, 143 | Message: "", 144 | }) 145 | } 146 | } 147 | 148 | break 149 | } 150 | case v1.PodFailed: 151 | if len(pod.Status.ContainerStatuses) == 0 { // e.g. Pod was evicted 152 | if notifyFail { 153 | failedFunc(&PodEvent{ 154 | Namespace: pod.Namespace, 155 | PodName: pod.Name, 156 | StartedAt: startedAt, 157 | FinishedAt: startedAt, 158 | ExitCode: -1, 159 | Reason: pod.Status.Reason, 160 | Message: pod.Status.Message, 161 | }) 162 | } 163 | } else { 164 | for _, cst := range pod.Status.ContainerStatuses { 165 | if cst.State.Terminated == nil { 166 | continue 167 | } 168 | 169 | if notifyFail { 170 | finishedAt := cst.State.Terminated.FinishedAt.Time 171 | 172 | failedFunc(&PodEvent{ 173 | Namespace: pod.Namespace, 174 | PodName: pod.Name, 175 | StartedAt: startedAt, 176 | FinishedAt: finishedAt, 177 | ExitCode: int(cst.State.Terminated.ExitCode), 178 | Reason: cst.State.Terminated.Reason, 179 | Message: pod.Status.Message, 180 | }) 181 | } 182 | 183 | break 184 | } 185 | } 186 | default: 187 | for _, cst := range pod.Status.ContainerStatuses { 188 | if cst.State.Terminated == nil { 189 | continue 190 | } 191 | 192 | if notifyFail { 193 | finishedAt := cst.State.Terminated.FinishedAt.Time 194 | 195 | failedFunc(&PodEvent{ 196 | Namespace: pod.Namespace, 197 | PodName: pod.Name, 198 | StartedAt: startedAt, 199 | FinishedAt: finishedAt, 200 | ExitCode: int(cst.State.Terminated.ExitCode), 201 | Reason: cst.State.Terminated.Reason, 202 | Message: pod.Status.Message, 203 | }) 204 | } 205 | 206 | break 207 | } 208 | } 209 | } 210 | case <-ctx.Done(): 211 | watcher.Stop() 212 | return 213 | } 214 | } 215 | }() 216 | 217 | return nil 218 | } 219 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: 0ee1bad31616b40c29b904ed9b101080660033230600a8e83b236467d2ba2f8b 2 | updated: 2017-06-08T12:03:34.0043489+09:00 3 | imports: 4 | - name: cloud.google.com/go 5 | version: 3b1ae45394a234c385be014e9a488f2bb6eef821 6 | subpackages: 7 | - compute/metadata 8 | - internal 9 | - name: github.com/blang/semver 10 | version: 31b736133b98f26d5e078ec9eb591666edfd091f 11 | - name: github.com/coreos/go-oidc 12 | version: 5644a2f50e2d2d5ba0b474bc5bc55fea1925936d 13 | subpackages: 14 | - http 15 | - jose 16 | - key 17 | - oauth2 18 | - oidc 19 | - name: github.com/coreos/pkg 20 | version: fa29b1d70f0beaddd4c7021607cc3c3be8ce94b8 21 | subpackages: 22 | - health 23 | - httputil 24 | - timeutil 25 | - name: github.com/davecgh/go-spew 26 | version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d 27 | subpackages: 28 | - spew 29 | - name: github.com/docker/distribution 30 | version: cd27f179f2c10c5d300e6d09025b538c475b0d51 31 | subpackages: 32 | - digest 33 | - reference 34 | - name: github.com/emicklei/go-restful 35 | version: 89ef8af493ab468a45a42bb0d89a06fccdd2fb22 36 | subpackages: 37 | - log 38 | - swagger 39 | - name: github.com/ghodss/yaml 40 | version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee 41 | - name: github.com/go-openapi/jsonpointer 42 | version: 46af16f9f7b149af66e5d1bd010e3574dc06de98 43 | - name: github.com/go-openapi/jsonreference 44 | version: 13c6e3589ad90f49bd3e3bbe2c2cb3d7a4142272 45 | - name: github.com/go-openapi/spec 46 | version: 6aced65f8501fe1217321abf0749d354824ba2ff 47 | - name: github.com/go-openapi/swag 48 | version: 1d0bd113de87027671077d3c71eb3ac5d7dbba72 49 | - name: github.com/gogo/protobuf 50 | version: e18d7aa8f8c624c915db340349aad4c49b10d173 51 | subpackages: 52 | - proto 53 | - sortkeys 54 | - name: github.com/golang/glog 55 | version: 44145f04b68cf362d9c4df2182967c2275eaefed 56 | - name: github.com/golang/protobuf 57 | version: 8616e8ee5e20a1704615e6c8d7afcdac06087a67 58 | subpackages: 59 | - proto 60 | - name: github.com/google/gofuzz 61 | version: bbcb9da2d746f8bdbd6a936686a0a6067ada0ec5 62 | - name: github.com/howeyc/gopass 63 | version: 3ca23474a7c7203e0a0a070fd33508f6efdb9b3d 64 | - name: github.com/imdario/mergo 65 | version: 6633656539c1639d9d78127b7d47c622b5d7b6dc 66 | - name: github.com/jonboulle/clockwork 67 | version: 72f9bd7c4e0c2a40055ab3d0f09654f730cce982 68 | - name: github.com/juju/ratelimit 69 | version: 77ed1c8a01217656d2080ad51981f6e99adaa177 70 | - name: github.com/mailru/easyjson 71 | version: d5b7844b561a7bc640052f1b935f7b800330d7e0 72 | subpackages: 73 | - buffer 74 | - jlexer 75 | - jwriter 76 | - name: github.com/nlopes/slack 77 | version: f243c7602fdf906248fc1f165284daa4e4d2d09e 78 | - name: github.com/pborman/uuid 79 | version: ca53cad383cad2479bbba7f7a1a05797ec1386e4 80 | - name: github.com/pkg/errors 81 | version: 645ef00459ed84a119197bfb8d8205042c6df63d 82 | - name: github.com/PuerkitoBio/purell 83 | version: 8a290539e2e8629dbc4e6bad948158f790ec31f4 84 | - name: github.com/PuerkitoBio/urlesc 85 | version: 5bd2802263f21d8788851d5305584c82a5c75d7e 86 | - name: github.com/sirupsen/logrus 87 | version: ba1b36c82c5e05c4f912a88eab0dcd91a171688f 88 | - name: github.com/spf13/pflag 89 | version: 9ff6c6923cfffbcd502984b8e0c80539a94968b7 90 | - name: github.com/ugorji/go 91 | version: f1f1a805ed361a0e078bb537e4ea78cd37dcf065 92 | subpackages: 93 | - codec 94 | - name: golang.org/x/crypto 95 | version: 1f22c0103821b9390939b6776727195525381532 96 | subpackages: 97 | - ssh/terminal 98 | - name: golang.org/x/net 99 | version: e90d6d0afc4c315a0d87a568ae68577cc15149a0 100 | subpackages: 101 | - context 102 | - context/ctxhttp 103 | - http2 104 | - http2/hpack 105 | - idna 106 | - lex/httplex 107 | - websocket 108 | - name: golang.org/x/oauth2 109 | version: 3c3a985cb79f52a3190fbc056984415ca6763d01 110 | subpackages: 111 | - google 112 | - internal 113 | - jws 114 | - jwt 115 | - name: golang.org/x/sys 116 | version: 8f0908ab3b2457e2e15403d3697c9ef5cb4b57a9 117 | subpackages: 118 | - unix 119 | - name: golang.org/x/text 120 | version: 2910a502d2bf9e43193af9d68ca516529614eed3 121 | subpackages: 122 | - cases 123 | - internal/tag 124 | - language 125 | - runes 126 | - secure/bidirule 127 | - secure/precis 128 | - transform 129 | - unicode/bidi 130 | - unicode/norm 131 | - width 132 | - name: google.golang.org/appengine 133 | version: 4f7eeb5305a4ba1966344836ba4af9996b7b4e05 134 | subpackages: 135 | - internal 136 | - internal/app_identity 137 | - internal/base 138 | - internal/datastore 139 | - internal/log 140 | - internal/modules 141 | - internal/remote_api 142 | - internal/urlfetch 143 | - urlfetch 144 | - name: gopkg.in/inf.v0 145 | version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4 146 | - name: gopkg.in/yaml.v2 147 | version: 53feefa2559fb8dfa8d81baad31be332c97d6c77 148 | - name: k8s.io/client-go 149 | version: e121606b0d09b2e1c467183ee46217fa85a6b672 150 | subpackages: 151 | - discovery 152 | - kubernetes 153 | - kubernetes/typed/apps/v1beta1 154 | - kubernetes/typed/authentication/v1beta1 155 | - kubernetes/typed/authorization/v1beta1 156 | - kubernetes/typed/autoscaling/v1 157 | - kubernetes/typed/batch/v1 158 | - kubernetes/typed/batch/v2alpha1 159 | - kubernetes/typed/certificates/v1alpha1 160 | - kubernetes/typed/core/v1 161 | - kubernetes/typed/extensions/v1beta1 162 | - kubernetes/typed/policy/v1beta1 163 | - kubernetes/typed/rbac/v1alpha1 164 | - kubernetes/typed/storage/v1beta1 165 | - pkg/api 166 | - pkg/api/errors 167 | - pkg/api/install 168 | - pkg/api/meta 169 | - pkg/api/meta/metatypes 170 | - pkg/api/resource 171 | - pkg/api/unversioned 172 | - pkg/api/v1 173 | - pkg/api/validation/path 174 | - pkg/apimachinery 175 | - pkg/apimachinery/announced 176 | - pkg/apimachinery/registered 177 | - pkg/apis/apps 178 | - pkg/apis/apps/install 179 | - pkg/apis/apps/v1beta1 180 | - pkg/apis/authentication 181 | - pkg/apis/authentication/install 182 | - pkg/apis/authentication/v1beta1 183 | - pkg/apis/authorization 184 | - pkg/apis/authorization/install 185 | - pkg/apis/authorization/v1beta1 186 | - pkg/apis/autoscaling 187 | - pkg/apis/autoscaling/install 188 | - pkg/apis/autoscaling/v1 189 | - pkg/apis/batch 190 | - pkg/apis/batch/install 191 | - pkg/apis/batch/v1 192 | - pkg/apis/batch/v2alpha1 193 | - pkg/apis/certificates 194 | - pkg/apis/certificates/install 195 | - pkg/apis/certificates/v1alpha1 196 | - pkg/apis/extensions 197 | - pkg/apis/extensions/install 198 | - pkg/apis/extensions/v1beta1 199 | - pkg/apis/policy 200 | - pkg/apis/policy/install 201 | - pkg/apis/policy/v1beta1 202 | - pkg/apis/rbac 203 | - pkg/apis/rbac/install 204 | - pkg/apis/rbac/v1alpha1 205 | - pkg/apis/storage 206 | - pkg/apis/storage/install 207 | - pkg/apis/storage/v1beta1 208 | - pkg/auth/user 209 | - pkg/conversion 210 | - pkg/conversion/queryparams 211 | - pkg/fields 212 | - pkg/genericapiserver/openapi/common 213 | - pkg/labels 214 | - pkg/runtime 215 | - pkg/runtime/serializer 216 | - pkg/runtime/serializer/json 217 | - pkg/runtime/serializer/protobuf 218 | - pkg/runtime/serializer/recognizer 219 | - pkg/runtime/serializer/streaming 220 | - pkg/runtime/serializer/versioning 221 | - pkg/selection 222 | - pkg/third_party/forked/golang/reflect 223 | - pkg/third_party/forked/golang/template 224 | - pkg/types 225 | - pkg/util 226 | - pkg/util/cert 227 | - pkg/util/clock 228 | - pkg/util/errors 229 | - pkg/util/flowcontrol 230 | - pkg/util/framer 231 | - pkg/util/homedir 232 | - pkg/util/integer 233 | - pkg/util/intstr 234 | - pkg/util/json 235 | - pkg/util/jsonpath 236 | - pkg/util/labels 237 | - pkg/util/net 238 | - pkg/util/parsers 239 | - pkg/util/rand 240 | - pkg/util/runtime 241 | - pkg/util/sets 242 | - pkg/util/uuid 243 | - pkg/util/validation 244 | - pkg/util/validation/field 245 | - pkg/util/wait 246 | - pkg/util/yaml 247 | - pkg/version 248 | - pkg/watch 249 | - pkg/watch/versioned 250 | - plugin/pkg/client/auth 251 | - plugin/pkg/client/auth/gcp 252 | - plugin/pkg/client/auth/oidc 253 | - rest 254 | - tools/auth 255 | - tools/clientcmd 256 | - tools/clientcmd/api 257 | - tools/clientcmd/api/latest 258 | - tools/clientcmd/api/v1 259 | - tools/metrics 260 | - transport 261 | testImports: [] 262 | --------------------------------------------------------------------------------