├── .github
└── workflows
│ ├── merge.yaml
│ ├── pull-request.yaml
│ └── release.yml
├── .gitignore
├── Dockerfile
├── LICENSE.txt
├── Makefile
├── README.md
├── cmd
└── main.go
├── deploy
├── configmap.yaml
├── deploy.yaml
└── secret.yaml
├── go.mod
├── go.sum
├── medias
└── cli-artwork.png
└── pkg
└── controllers
├── database_acls.go
├── node_controller.go
├── redis_acls.go
├── reserved_ip.go
├── reverse_ip.go
├── security_group.go
├── svc_controller.go
├── types.go
└── utils.go
/.github/workflows/merge.yaml:
--------------------------------------------------------------------------------
1 | name: merge
2 | on:
3 | push:
4 | branches:
5 | - 'main'
6 | jobs:
7 | merge_main:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v2
12 | - name: Unshallow
13 | run: git fetch --prune --unshallow
14 | - name: Extract branch name
15 | shell: bash
16 | run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
17 | id: extract_branch
18 | - name: Enable experimental on dockerd
19 | run: |
20 | echo $'{\n "experimental": true\n}' | sudo tee /etc/docker/daemon.json
21 | sudo service docker restart
22 | - name: Set up Docker Buildx
23 | id: buildx
24 | uses: crazy-max/ghaction-docker-buildx@v1
25 | with:
26 | version: latest
27 | - name: Docker login
28 | run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USER }} --password-stdin
29 | - name: Make latest release
30 | run: make release
31 | env:
32 | DOCKER_CLI_EXPERIMENTAL: enabled
33 | IMAGE_TAG: latest
34 | - name: Make tagged release
35 | run: make release
36 | env:
37 | DOCKER_CLI_EXPERIMENTAL: enabled
38 | IMAGE_TAG: ${{ github.sha }}
39 |
--------------------------------------------------------------------------------
/.github/workflows/pull-request.yaml:
--------------------------------------------------------------------------------
1 | name: pull-request
2 |
3 | on:
4 | - pull_request
5 |
6 | jobs:
7 | unit-test:
8 | strategy:
9 | matrix:
10 | go-version: [1.20.x]
11 | platform: [ubuntu-latest]
12 | runs-on: ${{ matrix.platform }}
13 | steps:
14 | - name: Install Go
15 | uses: actions/setup-go@v1
16 | with:
17 | go-version: ${{ matrix.go-version }}
18 | - name: checkout
19 | uses: actions/checkout@v2
20 | with:
21 | fetch-depth: 1
22 | - name: Run unit tests
23 | run: make test
24 | build-test:
25 | strategy:
26 | matrix:
27 | go-version: [1.20.x]
28 | platform: [ubuntu-latest]
29 | arch: [amd64, arm, arm64]
30 | runs-on: ${{ matrix.platform }}
31 | steps:
32 | - name: Install Go
33 | uses: actions/setup-go@v1
34 | with:
35 | go-version: ${{ matrix.go-version }}
36 | - name: checkout
37 | uses: actions/checkout@v2
38 | with:
39 | fetch-depth: 1
40 | - name: Building binary
41 | run: GOARCH=${{ matrix.arch }} make
42 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 | on:
3 | push:
4 | tags:
5 | - '*'
6 | jobs:
7 | release:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v2
12 | - name: Unshallow
13 | run: git fetch --prune --unshallow
14 | - name: Highest tag
15 | id: highestTag
16 | run: |
17 | git fetch --depth=1 origin +refs/tags/*:refs/tags/*
18 | echo ::set-output name=highest::$(git tag | grep -E "^v?([0-9]+\.)+[0-9]+$" | sort -r -V | head -n1)
19 | - name: Enable experimental on dockerd
20 | run: |
21 | echo $'{\n "experimental": true\n}' | sudo tee /etc/docker/daemon.json
22 | sudo service docker restart
23 | - name: Set up Docker Buildx
24 | id: buildx
25 | uses: crazy-max/ghaction-docker-buildx@v1
26 | with:
27 | version: latest
28 | - name: Docker login
29 | run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USER }} --password-stdin
30 | - name: Get tag
31 | uses: olegtarasov/get-tag@v1
32 | id: tagName
33 | - name: Make release
34 | run: make release
35 | env:
36 | DOCKER_CLI_EXPERIMENTAL: enabled
37 | IMAGE_TAG: ${{ steps.tagName.outputs.tag }}
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Binaries for programs and plugins
3 | *.exe
4 | *.exe~
5 | *.dll
6 | *.so
7 | *.dylib
8 | bin
9 |
10 | # Test binary, build with `go test -c`
11 | *.test
12 |
13 | # Output of the go coverage tool, specifically when used with LiteIDE
14 | *.out
15 |
16 | # Kubernetes Generated files - skip generated files, except for vendored files
17 |
18 | !vendor/**/zz_generated.*
19 |
20 | # editor and IDE paraphernalia
21 | .idea
22 | *.swp
23 | *.swo
24 | *~
25 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.20 as builder
2 |
3 | WORKDIR /workspace
4 |
5 | COPY go.mod go.mod
6 | COPY go.sum go.sum
7 |
8 | RUN go mod download
9 |
10 | COPY cmd/ cmd/
11 | COPY pkg/ pkg/
12 |
13 | RUN CGO_ENABLED=0 GOOS=linux GO111MODULE=on go build -a -o scaleway-k8s-node-coffee ./cmd/
14 |
15 | FROM gcr.io/distroless/static:nonroot
16 | WORKDIR /
17 | COPY --from=builder /workspace/scaleway-k8s-node-coffee .
18 | USER nonroot:nonroot
19 |
20 | ENTRYPOINT ["/scaleway-k8s-node-coffee"]
21 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright 2021 Patirk Cyvoct
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | OS ?= $(shell go env GOOS)
2 | ARCH ?= $(shell go env GOARCH)
3 | ALL_PLATFORM = linux/amd64
4 |
5 | # Image URL to use all building/pushing image targets
6 | REGISTRY ?= sh4d1
7 | IMG ?= scaleway-k8s-node-coffee
8 | FULL_IMG ?= $(REGISTRY)/$(IMG)
9 |
10 | IMAGE_TAG ?= $(shell git rev-parse HEAD)
11 |
12 | DOCKER_CLI_EXPERIMENTAL ?= enabled
13 |
14 | all: fmt vet
15 | go build -o bin/scaleway-k8s-node-coffee ./cmd/
16 |
17 | # Run tests
18 | test: fmt vet
19 | go test ./... -coverprofile cover.out
20 |
21 | run: fmt vet
22 | go run ./cmd/main.go
23 |
24 | # Run go fmt against code
25 | fmt:
26 | go fmt ./...
27 |
28 | # Run go vet against code
29 | vet:
30 | go vet ./...
31 |
32 | # Deploy the controller
33 | deploy:
34 | kubectl apply -f ./deploy -n scaleway-k8s-node-coffee
35 |
36 | # Build the docker image
37 | docker-build: test
38 | docker build --platform=linux/$(ARCH) -f Dockerfile . -t ${FULL_IMG}
39 |
40 | # Push the docker image
41 | docker-push:
42 | docker push ${FULL_IMG}
43 |
44 | docker-buildx-all:
45 | @echo "Making release for tag $(IMAGE_TAG)"
46 | docker buildx build --platform=$(ALL_PLATFORM) -f Dockerfile --push -t $(FULL_IMG):$(IMAGE_TAG) .
47 |
48 | release: docker-buildx-all
49 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 | # scaleway-k8s-node-coffee ☕
4 |
5 | Kubernetes toolkit controller project for Scaleway k8s nodes, that does a lot of different things based on changes in a Kubernetes cluster (especially Kapsule).
6 |
7 | # Getting started 🚀
8 |
9 | ## Configuration
10 |
11 | Below environment variables have to be defined in the controller container to configure it. Leaving a feature-related variable empty will disable it
12 |
13 | | Variable | Description | Example |
14 | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
15 | | `KUBECONFIG` | *optional*. `KUBECONFIG` file path to the cluster you want to run the controller against | `~/.kube/config/my-kubeconfig.yaml` |
16 | | `SCW_ACCESS_KEY` | *required*. Your scaleway project access key ([docs](https://www.scaleway.com/en/docs/console/my-project/how-to/generate-api-key/)) | `SCWxxxxxxxxxxxxxxxxx` |
17 | | `SCW_SECRET_KEY` | *required*. Your scaleway project secret key ([docs](https://www.scaleway.com/en/docs/console/my-project/how-to/generate-api-key/)) | `11111111-1111-1111-2111-111111111111` |
18 | | `SCW_DEFAULT_REGION` | Your Scaleway DBaaS default region ([docs](https://www.scaleway.com/en/docs/compute/instances/concepts/#availability-zone), [guides](https://registry.terraform.io/providers/scaleway/scaleway/latest/docs/guides/regions_and_zones)) | `fr-par` |
19 | | `SCW_DEFAULT_ZONE` | Your Scaleway DBaaS default zone ([docs](https://www.scaleway.com/en/docs/compute/instances/concepts/#availability-zone), [guides](https://registry.terraform.io/providers/scaleway/scaleway/latest/docs/guides/regions_and_zones)) | `fr-par-1` |
20 | | `RESERVED_IPS_POOL` | List of already existing reserved IP, comma-separated | `51.15.15.15,51.15.15.32` |
21 | | `REVERSE_IP_DOMAIN` | Your desired domain name | `example.com` |
22 | | `DATABASE_IDS` | List of DBaaS IDs (with optional regional IDs), comma-separated | `11111111-1111-1111-2111-111111111111,nl-ams/11111111-1111-1111-2111-111111111112` |
23 | | `REDIS_IDS` | List of Redis IDs (with optional zonal IDs), comma-separated | `11111111-1111-1111-2111-111111111111,nl-ams-1/11111111-1111-1111-2111-111111111112` |
24 | | `SECURITY_GROUP_IDS` | List of security group IDs (with optional zonal IDs), comma-separated | `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` |
25 | | `NUMBER_RETRIES` | *optional*. Retries on error amount (default: `30`) | `15` |
26 | ## Local tests
27 |
28 | You can test it against a remote cluster by providing the corresponding `KUBECONFIG` environment variable to the container, like the following :
29 |
30 | ```bash
31 | docker run sh4d1/scaleway-k8s-node-coffee \
32 | # Remote cluster to run against
33 | --env KUBECONFIG="~/.kube/config/my-kubeconfig.yaml" \
34 | ...
35 | ```
36 |
37 | Below snippet will build and run the controller (all features enabled) locally against the given cluster (`kubeconfig`)
38 |
39 | ```bash
40 | # Run the tests and build the image (if working on the project)
41 | make docker-build
42 |
43 | # Build the image
44 | docker build -t sh4d1/scaleway-k8s-node-coffee .
45 |
46 | # Run it with the required environment variables
47 | docker run sh4d1/scaleway-k8s-node-coffee \
48 | # Remote cluster to run against
49 | --env KUBECONFIG="~/.kube/config/my-kubeconfig.yaml" \
50 | # Authentication/global configuration
51 | --env SCW_ACCESS_KEY="SCWxxxxxxxxxxxxxxxxx" \
52 | --env SCW_SECRET_KEY="11111111-1111-1111-2111-111111111111" \
53 | --env RETRIES_NUMBER="15" \
54 | # Reserved IP
55 | --env RESERVED_IPS_POOL="51.15.15.15,51.15.15.32" \
56 | # Reverse IP
57 | --env REVERSE_IP_DOMAIN="example.com" \
58 | # Database ACL
59 | --env SCW_DEFAULT_REGION="fr-par" \
60 | --env DATABASE_IDS="11111111-1111-1111-2111-111111111111,nl-ams/11111111-1111-1111-2111-111111111112" \
61 | # Redis ACL
62 | --env REDIS_IDS="11111111-1111-1111-2111-111111111111,nl-ams-1/11111111-1111-1111-2111-111111111112" \
63 | --env SCW_DEFAULT_ZONE="fr-par-1" \
64 | # Security groups
65 | --env SECURITY_GROUP_IDS=my-value \
66 | ```
67 |
68 | ## Deploy
69 |
70 | Below snippet will deploy the controller on the current cluster context you're authenticated against (`kubectl config current-context`)
71 |
72 | ⚠️ Please note that you'll have to edit `./deploy/{configmap,secret}.yaml` to define your credentials and custimze your needs!
73 |
74 | ```bash
75 | # Using make (requires to clone the repository)
76 | make deploy
77 |
78 | # Using kubectl
79 | kubectl create -f https://raw.githubusercontent.com/Sh4d1/scaleway-k8s-node-coffee/main/deploy/deploy.yaml
80 | kubectl create -f https://raw.githubusercontent.com/Sh4d1/scaleway-k8s-node-coffee/main/deploy/secret.yaml --edit --namespace scaleway-k8s-node-coffee
81 | kubectl create -f https://raw.githubusercontent.com/Sh4d1/scaleway-k8s-node-coffee/main/deploy/configmap.yaml --edit --namespace scaleway-k8s-node-coffee
82 | ```
83 |
84 | ### Helm
85 |
86 | A helm implementation of this controller is in progress, feel free to contribute (https://github.com/Sh4d1/scaleway-k8s-node-coffee/pull/7)
87 |
88 | # Features ✨
89 |
90 | ## Reserved IP
91 |
92 | This feature allows a set of predefined reserved IP to be used as the nodes IP. Once a new node appears, it will try to assign a free reserved IP out of the given list to the node.
93 |
94 | **Variable(s)** 📝
95 |
96 | - `RESERVED_IPS_POOL`
97 | - list of already existing reserved IP, comma-separated
98 | - e.g. `51.15.15.15,51.15.15.32`
99 |
100 | **Notes**
101 |
102 | - ℹ️ A label `reserved-ip: true` will be added to the nodes with a reserved IP.
103 |
104 | ## Reverse IP
105 |
106 | This feature allows you to set the reverse IP of the reserved IP to a custom one. It will only work if a reserved IP is already set on the node (to use with the Reserved IP feature).
107 |
108 | **Variable(s)** 📝
109 |
110 | - `REVERSE_IP_DOMAIN`
111 | - desired domain name
112 | - e.g. `example.com` will update the reserved IP `51.16.17.18` with the reverse `18-17-16-51.example.com`
113 |
114 | **Notes**
115 |
116 | - ℹ️ If your domain is hosted on Scaleway, the record such as `18-17-16-51.example.com` will be added (and removed if not needed anymore).
117 |
118 | ## Database ACLs
119 |
120 | This feature allows to update the ACL rules of several DB to allow of all the cluster nodes (adding new ones, and removing old ones).
121 |
122 | **Variable(s)** 📝
123 |
124 | - `DATABASE_IDS`
125 | - list of DBaaS IDs (with optional regional IDs), comma-separated
126 | - e.g. `11111111-1111-1111-2111-111111111111,nl-ams/11111111-1111-1111-2111-111111111112`
127 | - `SCW_DEFAULT_REGION`
128 | - Default DBaaS resources region
129 | - e.g. `fr-par`
130 |
131 | **Notes**
132 |
133 | - ℹ️ Will update the ACL of the database with ID `11111111-1111-1111-2111-111111111111` in the region specified by the environment variable `SCW_DEFAULT_REGION` and the database `11111111-1111-1111-2111-111111111112` in the `nl-ams` region.
134 |
135 | - ℹ️ If your database is in a different project than the cluster nodes, please set the environment variable `NODES_IP_SOURCE` to `kubernetes`.
136 |
137 | - ℹ️ If your DBaaS already have ACL rules allowing your k8s nodes' IPs, and not named following their IDs, you'll have to delete them or rename them with the corresponding nodes' IDs
138 |
139 | ## Redis ACLs
140 |
141 | This feature allows to update the ACL rules of several Redis instances to allow of all the cluster nodes (adding new ones, and removing old ones).
142 |
143 | **Variable(s)** 📝
144 |
145 | - `REDIS_IDS`
146 | - list of Redis IDs (with optional zonal IDs), comma-separated
147 | - e.g. `11111111-1111-1111-2111-111111111111,nl-ams-1/11111111-1111-1111-2111-111111111112`
148 | - `SCW_DEFAULT_ZONE`
149 | - Default Redis resources zone
150 | - e.g. `fr-par-1`
151 |
152 | **Notes**
153 |
154 | - ℹ️ Will update the ACL of the redis instance with ID `11111111-1111-1111-2111-111111111111` in the zone specified by the environment variable `SCW_DEFAULT_ZONE` and the instance `11111111-1111-1111-2111-111111111112` in the `nl-ams-1` zone.
155 |
156 | - ℹ️ If your redis instance is in a different project than the cluster nodes, please set the environment variable `NODES_IP_SOURCE` to `kubernetes`.
157 |
158 | ## Security Group
159 |
160 | This feature allows you to update multiple security groups with:
161 | - The Public and Private IPs of all nodes of the cluster
162 | - The Node Ports of the NodePort and LoadBalancer services
163 |
164 | **Variable(s)** 📝
165 |
166 | - `SECURITY_GROUP_IDS`
167 | - list of security group IDs (with optional zonal IDs), comma-separated
168 | - e.g. `11111111-1111-1111-2111-111111111111,nl-ams-1/11111111-1111-1111-2111-111111111112`
169 |
170 | **Notes**
171 |
172 | - ℹ️ However due to several lack of features, the deletion of the rules if best effort for the nodes, and non existent for the services.
173 |
174 | ## Contribution
175 |
176 | Feel free to submit any issue, feature request or pull request :smile:!
177 |
178 | Artwork edited, initially from [scaleway-cli](https://github.com/scaleway/scaleway-cli)
--------------------------------------------------------------------------------
/cmd/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "os"
6 | "os/signal"
7 | "syscall"
8 |
9 | "github.com/Sh4d1/scaleway-k8s-node-coffee/pkg/controllers"
10 | "k8s.io/client-go/kubernetes"
11 | "k8s.io/client-go/tools/clientcmd"
12 | klog "k8s.io/klog/v2"
13 | )
14 |
15 | var (
16 | kubeconfig string
17 | masterURL string
18 | )
19 |
20 | func init() {
21 | klog.InitFlags(nil)
22 | flag.StringVar(&kubeconfig, "kubeconfig", os.Getenv("KUBECONFIG"), "Path to a kubeconfig. Only required if out-of-cluster.")
23 | flag.StringVar(&masterURL, "master", os.Getenv("MASTER_URL"), "URL of the Kubernetes API server. Optional")
24 | }
25 |
26 | func main() {
27 | flag.Parse()
28 | klog.Infof("Collecting coffee beans")
29 |
30 | restConfig, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)
31 | if err != nil {
32 | klog.Fatalf("Error building kubeconfig for shoot: %v", err)
33 | }
34 |
35 | clientset, err := kubernetes.NewForConfig(restConfig)
36 | if err != nil {
37 | klog.Fatalf("could not build kubernetes clientset: %v", err)
38 | }
39 |
40 | nodeController, err := controllers.NewNodeController(clientset)
41 | if err != nil {
42 | klog.Fatalf("could not create node controller: %v", err)
43 | }
44 | svcController, err := controllers.NewSvcController(clientset)
45 | if err != nil {
46 | klog.Fatalf("could not create svc controller: %v", err)
47 | }
48 |
49 | stop := make(chan struct{})
50 | klog.Infof("Starting the coffee machine")
51 | nodeController.Wg.Add(1)
52 | go nodeController.Run(stop)
53 | svcController.Wg.Add(1)
54 | go svcController.Run(stop)
55 |
56 | c := make(chan os.Signal, 1)
57 | signal.Notify(c, syscall.SIGINT|syscall.SIGTERM)
58 | <-c
59 | klog.Infof("Stopping the coffee machine")
60 | nodeController.Wg.Wait()
61 | svcController.Wg.Wait()
62 | close(stop)
63 | }
64 |
--------------------------------------------------------------------------------
/deploy/configmap.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ConfigMap
3 | metadata:
4 | name: scaleway-k8s-node-coffee
5 | namespace: scaleway-k8s-node-coffee
6 | data:
7 | REVERSE_IP_DOMAIN: "" # example ptrk.io will yield 134-134-15-15.ptrk.io as reverse
8 | DATABASE_IDS: "" # example 11111111-1111-1111-2111-111111111111
9 | # or fr-par/11111111-1111-1111-2111-111111111111
10 | # or 11111111-1111-1111-2111-111111111111,fr-par/11111111-1111-1111-2111-111111111112
11 | REDIS_IDS: "" # example 11111111-1111-1111-2111-111111111111
12 | # or fr-par-1/11111111-1111-1111-2111-111111111111
13 | # or 11111111-1111-1111-2111-111111111111,fr-par-1/11111111-1111-1111-2111-111111111112
14 | RESERVED_IPS_POOL: "" # example 51.15.24.24 or 51.15.15.15,51.15.24.24
15 | SECURITY_GROUP_IDS: "" # example 11111111-1111-1111-2111-111111111111
16 | # or fr-par/11111111-1111-1111-2111-111111111111
17 | # or 11111111-1111-1111-2111-111111111111,fr-par/11111111-1111-1111-2111-111111111112
18 | NUMBER_RETRIES: "30" # Set to a value if you want the controller to retry on errors
19 |
--------------------------------------------------------------------------------
/deploy/deploy.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: Namespace
3 | metadata:
4 | labels:
5 | control-plane: scaleway-k8s-node-coffee
6 | name: scaleway-k8s-node-coffee
7 | ---
8 | apiVersion: v1
9 | kind: ServiceAccount
10 | metadata:
11 | name: scaleway-k8s-node-coffee
12 | namespace: scaleway-k8s-node-coffee
13 | ---
14 | apiVersion: rbac.authorization.k8s.io/v1
15 | kind: ClusterRole
16 | metadata:
17 | creationTimestamp: null
18 | name: scaleway-k8s-node-coffee
19 | rules:
20 | - apiGroups:
21 | - ""
22 | resources:
23 | - nodes
24 | verbs:
25 | - get
26 | - list
27 | - watch
28 | - update
29 | - apiGroups:
30 | - ""
31 | resources:
32 | - services
33 | verbs:
34 | - get
35 | - list
36 | - watch
37 | ---
38 | apiVersion: rbac.authorization.k8s.io/v1
39 | kind: ClusterRoleBinding
40 | metadata:
41 | name: scaleway-k8s-node-coffee
42 | roleRef:
43 | apiGroup: rbac.authorization.k8s.io
44 | kind: ClusterRole
45 | name: scaleway-k8s-node-coffee
46 | subjects:
47 | - kind: ServiceAccount
48 | name: scaleway-k8s-node-coffee
49 | namespace: scaleway-k8s-node-coffee
50 | ---
51 | apiVersion: apps/v1
52 | kind: Deployment
53 | metadata:
54 | name: scaleway-k8s-node-coffee
55 | namespace: scaleway-k8s-node-coffee
56 | labels:
57 | control-plane: scaleway-k8s-node-coffee
58 | spec:
59 | selector:
60 | matchLabels:
61 | control-plane: scaleway-k8s-node-coffee
62 | replicas: 1
63 | template:
64 | metadata:
65 | labels:
66 | control-plane: scaleway-k8s-node-coffee
67 | spec:
68 | serviceAccountName: scaleway-k8s-node-coffee
69 | containers:
70 | - env:
71 | - name: CONFIGMAP_NAMESPACE
72 | valueFrom:
73 | fieldRef:
74 | fieldPath: metadata.namespace
75 | envFrom:
76 | - secretRef:
77 | name: scaleway-k8s-node-coffee
78 | - configMapRef:
79 | name: scaleway-k8s-node-coffee
80 | image: sh4d1/scaleway-k8s-node-coffee:latest
81 | name: scaleway-k8s-node-coffee
82 | resources:
83 | limits:
84 | cpu: 100m
85 | memory: 30Mi
86 | requests:
87 | cpu: 100m
88 | memory: 20Mi
89 | terminationGracePeriodSeconds: 10
90 |
--------------------------------------------------------------------------------
/deploy/secret.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | stringData:
3 | SCW_ACCESS_KEY:
4 | SCW_SECRET_KEY:
5 | SCW_DEFAULT_ZONE:
6 | SCW_DEFAULT_REGION:
7 | kind: Secret
8 | metadata:
9 | name: scaleway-k8s-node-coffee
10 | namespace: scaleway-k8s-node-coffee
11 | type: Opaque
12 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/Sh4d1/scaleway-k8s-node-coffee
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/scaleway/scaleway-sdk-go v1.0.0-beta.12
7 | k8s.io/api v0.20.1
8 | k8s.io/apimachinery v0.20.1
9 | k8s.io/client-go v0.20.1
10 | k8s.io/klog/v2 v2.4.0
11 | )
12 |
13 | require (
14 | github.com/davecgh/go-spew v1.1.1 // indirect
15 | github.com/go-logr/logr v0.2.0 // indirect
16 | github.com/gogo/protobuf v1.3.1 // indirect
17 | github.com/golang/protobuf v1.4.3 // indirect
18 | github.com/google/go-cmp v0.5.2 // indirect
19 | github.com/google/gofuzz v1.1.0 // indirect
20 | github.com/googleapis/gnostic v0.4.1 // indirect
21 | github.com/hashicorp/golang-lru v0.5.1 // indirect
22 | github.com/imdario/mergo v0.3.11 // indirect
23 | github.com/json-iterator/go v1.1.10 // indirect
24 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
25 | github.com/modern-go/reflect2 v1.0.1 // indirect
26 | github.com/spf13/pflag v1.0.5 // indirect
27 | golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect
28 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect
29 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect
30 | golang.org/x/sys v0.0.0-20201112073958-5cba982894dd // indirect
31 | golang.org/x/text v0.3.4 // indirect
32 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
33 | google.golang.org/appengine v1.6.5 // indirect
34 | google.golang.org/protobuf v1.25.0 // indirect
35 | gopkg.in/inf.v0 v0.9.1 // indirect
36 | gopkg.in/yaml.v2 v2.4.0 // indirect
37 | k8s.io/utils v0.0.0-20210111153108-fddb29f9d009 // indirect
38 | sigs.k8s.io/structured-merge-diff/v4 v4.0.2 // indirect
39 | sigs.k8s.io/yaml v1.2.0 // indirect
40 | )
41 |
42 | replace k8s.io/kubernetes => k8s.io/kubernetes v0.20.1
43 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
12 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
13 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
14 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
15 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
16 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
17 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
18 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
19 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
20 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
21 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
22 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
23 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
24 | github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
25 | github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
26 | github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg=
27 | github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
28 | github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
29 | github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
30 | github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
31 | github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
32 | github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
33 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
34 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
35 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
36 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
37 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
38 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
39 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
40 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
41 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
42 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
43 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
44 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
45 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
46 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
47 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
48 | github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
49 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
50 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
51 | github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
52 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
53 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
54 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
55 | github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
56 | github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
57 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
58 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
59 | github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
60 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
61 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
62 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
63 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
64 | github.com/go-logr/logr v0.2.0 h1:QvGt2nLcHH0WK9orKa+ppBPAxREcH364nPUedEpK0TY=
65 | github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
66 | github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
67 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
68 | github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
69 | github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
70 | github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo=
71 | github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
72 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
73 | github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
74 | github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
75 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
76 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
77 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
78 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
79 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
80 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
81 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
82 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
83 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
84 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
85 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
86 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
87 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
88 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
89 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
90 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
91 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
92 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
93 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
94 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
95 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
96 | github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
97 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
98 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
99 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
100 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
101 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
102 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
103 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
104 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
105 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
106 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
107 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
108 | github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
109 | github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
110 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
111 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
112 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
113 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
114 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
115 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
116 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
117 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
118 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
119 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
120 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
121 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
122 | github.com/googleapis/gnostic v0.4.1 h1:DLJCy1n/vrD4HPjOvYcT8aYQXpPIzoRZONaYwyycI+I=
123 | github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
124 | github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
125 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
126 | github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
127 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
128 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
129 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
130 | github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
131 | github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
132 | github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
133 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
134 | github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68=
135 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
136 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
137 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
138 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
139 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
140 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
141 | github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
142 | github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
143 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
144 | github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
145 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
146 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
147 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
148 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
149 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
150 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
151 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
152 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
153 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
154 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
155 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
156 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
157 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
158 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
159 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
160 | github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
161 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
162 | github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
163 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
164 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
165 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
166 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
167 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
168 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
169 | github.com/scaleway/scaleway-sdk-go v1.0.0-beta.12 h1:Aaz4T7dZp7cB2cv7D/tGtRdSMh48sRaDYr7Jh0HV4qQ=
170 | github.com/scaleway/scaleway-sdk-go v1.0.0-beta.12/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
171 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
172 | github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
173 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
174 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
175 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
176 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
177 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
178 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
179 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
180 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
181 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
182 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
183 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
184 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
185 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
186 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
187 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
188 | golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
189 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
190 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
191 | golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
192 | golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
193 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
194 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
195 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
196 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
197 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
198 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
199 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
200 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
201 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
202 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
203 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
204 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
205 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
206 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
207 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
208 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
209 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
210 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
211 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
212 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
213 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
214 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
215 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
216 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
217 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
218 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
219 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
220 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
221 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
222 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
223 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
224 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
225 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
226 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
227 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
228 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
229 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
230 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
231 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
232 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
233 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
234 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
235 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
236 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
237 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
238 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
239 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
240 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
241 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
242 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
243 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
244 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
245 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
246 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
247 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
248 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
249 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
250 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
251 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
252 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
253 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
254 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
255 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
256 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
257 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
258 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
259 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
260 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
261 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
262 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
263 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
264 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
265 | golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
266 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
267 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
268 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
269 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
270 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
271 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
272 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
273 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
274 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
275 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
276 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
277 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
278 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
279 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
280 | golang.org/x/sys v0.0.0-20201112073958-5cba982894dd h1:5CtCZbICpIOFdgO940moixOPjc0178IU44m4EjOO5IY=
281 | golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
282 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
283 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
284 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
285 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
286 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
287 | golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
288 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
289 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
290 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
291 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
292 | golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
293 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
294 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
295 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
296 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
297 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
298 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
299 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
300 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
301 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
302 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
303 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
304 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
305 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
306 | golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
307 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
308 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
309 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
310 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
311 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
312 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
313 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
314 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
315 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
316 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
317 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
318 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
319 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
320 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
321 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
322 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
323 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
324 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
325 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
326 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
327 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
328 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
329 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
330 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
331 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
332 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
333 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
334 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
335 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
336 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
337 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
338 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
339 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
340 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
341 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
342 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
343 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
344 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
345 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
346 | google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
347 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
348 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
349 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
350 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
351 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
352 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
353 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
354 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
355 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
356 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
357 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
358 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
359 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
360 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
361 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
362 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
363 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
364 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
365 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
366 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
367 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
368 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
369 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
370 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
371 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
372 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
373 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
374 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
375 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
376 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
377 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
378 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
379 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
380 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
381 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
382 | google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
383 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
384 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
385 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
386 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
387 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
388 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
389 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
390 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
391 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
392 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
393 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
394 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
395 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
396 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
397 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
398 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
399 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
400 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
401 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
402 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
403 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
404 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
405 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
406 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
407 | k8s.io/api v0.20.1 h1:ud1c3W3YNzGd6ABJlbFfKXBKXO+1KdGfcgGGNgFR03E=
408 | k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo=
409 | k8s.io/apimachinery v0.20.1 h1:LAhz8pKbgR8tUwn7boK+b2HZdt7MiTu2mkYtFMUjTRQ=
410 | k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
411 | k8s.io/client-go v0.20.1 h1:Qquik0xNFbK9aUG92pxHYsyfea5/RPO9o9bSywNor+M=
412 | k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y=
413 | k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
414 | k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
415 | k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ=
416 | k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
417 | k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
418 | k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
419 | k8s.io/utils v0.0.0-20210111153108-fddb29f9d009 h1:0T5IaWHO3sJTEmCP6mUlBvMukxPKUQWqiI/YuiBNMiQ=
420 | k8s.io/utils v0.0.0-20210111153108-fddb29f9d009/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
421 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
422 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
423 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
424 | sigs.k8s.io/structured-merge-diff/v4 v4.0.2 h1:YHQV7Dajm86OuqnIR6zAelnDWBRjo+YhYV9PmGrh1s8=
425 | sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
426 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
427 | sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
428 | sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
429 |
--------------------------------------------------------------------------------
/medias/cli-artwork.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Sh4d1/scaleway-k8s-node-coffee/01f4f840de1be4cdf5143e7b9500f1bfde10c2f5/medias/cli-artwork.png
--------------------------------------------------------------------------------
/pkg/controllers/database_acls.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "os"
7 |
8 | rdb "github.com/scaleway/scaleway-sdk-go/api/rdb/v1"
9 | "github.com/scaleway/scaleway-sdk-go/scw"
10 | "k8s.io/api/core/v1"
11 | klog "k8s.io/klog/v2"
12 | )
13 |
14 | func (c *NodeController) syncDatabaseACLs(nodeName string) error {
15 | if len(c.databaseIDs) == 0 {
16 | return nil
17 | }
18 |
19 | var node *v1.Node
20 |
21 | retryOnError := false
22 |
23 | nodeObj, exists, err := c.indexer.GetByKey(nodeName)
24 | if err != nil {
25 | klog.Errorf("could not get node %s by key: %v", nodeName, err)
26 | return err
27 | }
28 | if exists {
29 | var ok bool
30 | node, ok = nodeObj.(*v1.Node)
31 | if !ok {
32 | klog.Errorf("could not get node %s from object", nodeName)
33 | return fmt.Errorf("could not get node %s from object", nodeName)
34 | }
35 | }
36 |
37 | dbAPI := rdb.NewAPI(c.scwClient)
38 |
39 | for _, dbID := range c.databaseIDs {
40 | klog.Infof("whitelisting IP on node %s on database %s", nodeName, dbID)
41 |
42 | id, region, err := getRegionalizedID(dbID)
43 | if err != nil {
44 | klog.Errorf("could not get id and region from %s: %v", dbID, err)
45 | continue
46 | }
47 |
48 | dbInstance, err := dbAPI.GetInstance(&rdb.GetInstanceRequest{
49 | Region: scw.Region(region),
50 | InstanceID: id,
51 | })
52 | if err != nil {
53 | klog.Errorf("could not get rdb instance %s: %v", id, err)
54 | continue
55 | }
56 |
57 | acls, err := dbAPI.ListInstanceACLRules(&rdb.ListInstanceACLRulesRequest{
58 | Region: dbInstance.Region,
59 | InstanceID: dbInstance.ID,
60 | }, scw.WithAllPages())
61 | if err != nil {
62 | klog.Errorf("could not get rdb acl rule for instance %s: %v", id, err)
63 | continue
64 | }
65 |
66 | var rule *rdb.ACLRule
67 |
68 | for _, acl := range acls.Rules {
69 | if acl.Description == nodeName {
70 | rule = acl
71 | break
72 | }
73 | }
74 |
75 | if !exists && rule != nil {
76 | _, err := dbAPI.DeleteInstanceACLRules(&rdb.DeleteInstanceACLRulesRequest{
77 | Region: dbInstance.Region,
78 | ACLRuleIPs: []string{rule.IP.String()},
79 | InstanceID: dbInstance.ID,
80 | })
81 | if err != nil {
82 | klog.Errorf("could not delete acl rule for node %s on db %s: %v", nodeName, dbInstance.ID, err)
83 | retryOnError = true
84 | }
85 | continue
86 | }
87 |
88 | var nodePublicIP net.IP
89 |
90 | if os.Getenv(NodesIPSource) == NodesIPSourceKubernetes {
91 | for _, addr := range node.Status.Addresses {
92 | if addr.Type == v1.NodeExternalIP {
93 | // prefer ipv4 over ipv6 since RDB instances are only accessible via ipv4
94 | nodePublicIP = net.ParseIP(addr.Address).To4()
95 | if nodePublicIP != nil {
96 | // use first external ipv4 available
97 | break
98 | }
99 | }
100 | }
101 | } else {
102 | server, err := c.getInstanceFromNodeName(nodeName)
103 | if err != nil {
104 | klog.Errorf("could not get instance %s: %v", nodeName, err)
105 | continue
106 | }
107 |
108 | if server.PublicIP == nil {
109 | klog.Warningf("skipping node %s without public IP", nodeName)
110 | continue
111 | }
112 |
113 | nodePublicIP = server.PublicIP.Address
114 | }
115 |
116 | if nodePublicIP == nil {
117 | klog.Warningf("skipping node %s without public IP", nodeName)
118 | continue
119 | }
120 |
121 | nodeIP := net.IPNet{
122 | IP: nodePublicIP,
123 | Mask: net.IPv4Mask(255, 255, 255, 255), // TODO better idea?
124 | }
125 |
126 | if rule == nil || nodeIP.String() != rule.IP.String() {
127 | _, err := dbAPI.AddInstanceACLRules(&rdb.AddInstanceACLRulesRequest{
128 | Region: dbInstance.Region,
129 | InstanceID: dbInstance.ID,
130 | Rules: []*rdb.ACLRuleRequest{
131 | {
132 | IP: scw.IPNet{IPNet: nodeIP},
133 | Description: nodeName,
134 | },
135 | },
136 | })
137 | if err != nil {
138 | klog.Errorf("could not add acl rule for node %s with ip %s on db %s: %v", nodeName, nodeIP.String(), dbInstance.ID, err)
139 | retryOnError = true
140 | continue
141 | }
142 | }
143 | }
144 |
145 | if retryOnError {
146 | return fmt.Errorf("got retryable error")
147 | }
148 |
149 | return nil
150 | }
151 |
--------------------------------------------------------------------------------
/pkg/controllers/node_controller.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strconv"
7 | "strings"
8 | "time"
9 |
10 | dns "github.com/scaleway/scaleway-sdk-go/api/domain/v2beta1"
11 | "github.com/scaleway/scaleway-sdk-go/scw"
12 | v1 "k8s.io/api/core/v1"
13 | "k8s.io/apimachinery/pkg/fields"
14 | "k8s.io/apimachinery/pkg/util/runtime"
15 | "k8s.io/apimachinery/pkg/util/wait"
16 | "k8s.io/client-go/kubernetes"
17 | "k8s.io/client-go/tools/cache"
18 | "k8s.io/client-go/util/workqueue"
19 | klog "k8s.io/klog/v2"
20 | )
21 |
22 | const (
23 | ReverseIPDomainEnv = "REVERSE_IP_DOMAIN"
24 | DatabaseIDsEnv = "DATABASE_IDS"
25 | RedisIDsEnv = "REDIS_IDS"
26 | ReservedIPsPoolEnv = "RESERVED_IPS_POOL"
27 | SecurityGroupIDs = "SECURITY_GROUP_IDS"
28 | NumberRetries = "NUMBER_RETRIES"
29 | NodesIPSource = "NODES_IP_SOURCE"
30 | NodesIPSourceKubernetes = "kubernetes"
31 | NodeLabelReservedIP = "reserved-ip"
32 | )
33 |
34 | func NewNodeController(clientset *kubernetes.Clientset) (*NodeController, error) {
35 | nodeListWatcher := cache.NewListWatchFromClient(clientset.CoreV1().RESTClient(), "nodes", "", fields.Everything())
36 |
37 | queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
38 |
39 | indexer, informer := cache.NewIndexerInformer(nodeListWatcher, &v1.Node{}, 0, cache.ResourceEventHandlerFuncs{
40 | AddFunc: func(obj interface{}) {
41 | key, err := cache.MetaNamespaceKeyFunc(obj)
42 | if err == nil {
43 | queue.Add(key)
44 | }
45 | },
46 | UpdateFunc: func(old interface{}, new interface{}) {
47 | key, err := cache.MetaNamespaceKeyFunc(new)
48 | if err == nil {
49 |
50 | oldNode, oldOk := old.(*v1.Node)
51 | newNode, newOk := new.(*v1.Node)
52 | if oldOk && newOk {
53 | if oldNode.ResourceVersion == newNode.ResourceVersion {
54 | queue.Add(key)
55 | return
56 | }
57 | for _, oldAddress := range oldNode.Status.Addresses {
58 | for _, newAddress := range newNode.Status.Addresses {
59 | if oldAddress.Type == newAddress.Type && oldAddress.Address != newAddress.Address {
60 | queue.Add(key)
61 | return
62 | }
63 | }
64 | }
65 | }
66 | }
67 | },
68 | DeleteFunc: func(obj interface{}) {
69 | key, err := cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
70 | if err == nil {
71 | queue.Add(key)
72 | }
73 | },
74 | }, cache.Indexers{})
75 |
76 | scwClient, err := scw.NewClient(scw.WithEnv())
77 | if err != nil {
78 | return nil, err
79 | }
80 |
81 | controller := &NodeController{
82 | indexer: indexer,
83 | informer: informer,
84 | queue: queue,
85 | scwClient: scwClient,
86 | numberRetries: defaultNumberRetries,
87 | clientset: clientset,
88 | scwZoneFound: false,
89 | scwZone: "",
90 | }
91 |
92 | // TODO handle validation here ?
93 | if os.Getenv(ReverseIPDomainEnv) != "" {
94 | controller.reverseIPDomain = os.Getenv(ReverseIPDomainEnv)
95 | }
96 |
97 | if os.Getenv(DatabaseIDsEnv) != "" {
98 | controller.databaseIDs = strings.Split(os.Getenv(DatabaseIDsEnv), ",")
99 | }
100 |
101 | if os.Getenv(RedisIDsEnv) != "" {
102 | controller.redisIDs = strings.Split(os.Getenv(RedisIDsEnv), ",")
103 | }
104 |
105 | if os.Getenv(ReservedIPsPoolEnv) != "" {
106 | controller.reservedIPs = strings.Split(os.Getenv(ReservedIPsPoolEnv), ",")
107 | }
108 |
109 | if os.Getenv(SecurityGroupIDs) != "" {
110 | controller.securityGroupIDs = strings.Split(os.Getenv(SecurityGroupIDs), ",")
111 | }
112 |
113 | if os.Getenv(NumberRetries) != "" {
114 | numberRetriesValue, err := strconv.Atoi(os.Getenv(NumberRetries))
115 | controller.numberRetries = numberRetriesValue
116 | if err != nil {
117 | klog.Errorf("could not parse the desired number of retries %s: %v", os.Getenv(NumberRetries), err)
118 | controller.numberRetries = defaultNumberRetries
119 | }
120 | }
121 | if controller.reverseIPDomain != "" {
122 | // try to find a parent zone
123 | maxPage := uint32(100)
124 | dnsAPI := dns.NewAPI(controller.scwClient)
125 | listing, err := dnsAPI.ListDNSZones(&dns.ListDNSZonesRequest{PageSize: &maxPage})
126 | if err != nil {
127 | klog.Errorf("could not get list of scaleway zones : %v", err)
128 | } else {
129 | for i := range listing.DNSZones {
130 | if strings.HasSuffix(controller.reverseIPDomain, listing.DNSZones[i].Domain) {
131 | subDomain := strings.TrimRight(controller.scwZone, listing.DNSZones[i].Domain)
132 | if subDomain == "" || subDomain[len(subDomain)-1] == '.' {
133 | zone := fmt.Sprintf("%s.%s", listing.DNSZones[i].Subdomain, listing.DNSZones[i].Domain)
134 | if strings.HasPrefix(zone, ".") {
135 | zone = zone[1:]
136 | }
137 | controller.scwZoneFound = true
138 | if len(controller.scwZone) < len(zone) {
139 | controller.scwZone = zone
140 | }
141 | }
142 | }
143 | }
144 | }
145 | if controller.scwZoneFound {
146 | klog.Infof("found an scaleway zone %s", controller.scwZone)
147 | }
148 | }
149 |
150 | return controller, nil
151 | }
152 |
153 | func (c *NodeController) syncNeeded(nodeName string) error {
154 | var errs []error
155 |
156 | err := c.syncReservedIP(nodeName)
157 | if err != nil {
158 | klog.Errorf("failed to sync reserved IP for node %s: %v", nodeName, err)
159 | errs = append(errs, err)
160 | }
161 | err = c.syncReverseIP(nodeName)
162 | if err != nil {
163 | klog.Errorf("failed to sync reverse IP for node %s: %v", nodeName, err)
164 | errs = append(errs, err)
165 | }
166 | err = c.syncDatabaseACLs(nodeName)
167 | if err != nil {
168 | klog.Errorf("failed to sync database acl for node %s: %v", nodeName, err)
169 | errs = append(errs, err)
170 | }
171 | err = c.syncRedisACLs(nodeName)
172 | if err != nil {
173 | klog.Errorf("failed to sync redis acl for node %s: %v", nodeName, err)
174 | errs = append(errs, err)
175 | }
176 | err = c.syncSecurityGroup(nodeName)
177 | if err != nil {
178 | klog.Errorf("failed to sync security group for node %s: %v", nodeName, err)
179 | errs = append(errs, err)
180 | }
181 |
182 | if len(errs) == 0 {
183 | return nil
184 | }
185 |
186 | return fmt.Errorf("got several error")
187 | }
188 |
189 | func (c *NodeController) processNextItem() bool {
190 | key, quit := c.queue.Get()
191 | if quit {
192 | return false
193 | }
194 | defer c.queue.Done(key)
195 |
196 | err := c.syncNeeded(key.(string))
197 | c.handleErr(err, key)
198 | return true
199 | }
200 |
201 | func (c *NodeController) handleErr(err error, key interface{}) {
202 | if err == nil {
203 | c.queue.Forget(key)
204 | return
205 | }
206 |
207 | if c.queue.NumRequeues(key) < c.numberRetries {
208 | c.queue.AddRateLimited(key)
209 | return
210 | }
211 |
212 | c.queue.Forget(key)
213 | runtime.HandleError(err)
214 | klog.Infof("too many retries for key %s: %v", key, err)
215 | }
216 |
217 | func (c *NodeController) Run(stopCh chan struct{}) {
218 | defer runtime.HandleCrash()
219 | defer c.Wg.Done()
220 |
221 | defer c.queue.ShutDown()
222 |
223 | go c.informer.Run(stopCh)
224 |
225 | if !cache.WaitForCacheSync(stopCh, c.informer.HasSynced) {
226 | runtime.HandleError(fmt.Errorf("timed out waiting for caches to sync"))
227 | return
228 | }
229 |
230 | go wait.Until(c.runWorker, time.Second, stopCh)
231 |
232 | <-stopCh
233 | }
234 |
235 | func (c *NodeController) runWorker() {
236 | for c.processNextItem() {
237 | }
238 | }
239 |
--------------------------------------------------------------------------------
/pkg/controllers/redis_acls.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "os"
7 |
8 | redis "github.com/scaleway/scaleway-sdk-go/api/redis/v1"
9 | "github.com/scaleway/scaleway-sdk-go/scw"
10 | "k8s.io/api/core/v1"
11 | klog "k8s.io/klog/v2"
12 | )
13 |
14 | func (c *NodeController) syncRedisACLs(nodeName string) error {
15 | if len(c.redisIDs) == 0 {
16 | return nil
17 | }
18 |
19 | var node *v1.Node
20 |
21 | retryOnError := false
22 |
23 | nodeObj, exists, err := c.indexer.GetByKey(nodeName)
24 | if err != nil {
25 | klog.Errorf("could not get node %s by key: %v", nodeName, err)
26 | return err
27 | }
28 | if exists {
29 | var ok bool
30 | node, ok = nodeObj.(*v1.Node)
31 | if !ok {
32 | klog.Errorf("could not get node %s from object", nodeName)
33 | return fmt.Errorf("could not get node %s from object", nodeName)
34 | }
35 | }
36 |
37 | dbAPI := redis.NewAPI(c.scwClient)
38 |
39 | for _, redisID := range c.redisIDs {
40 | klog.Infof("whitelisting IP on node %s on redis instance %s", nodeName, redisID)
41 |
42 | id, zone, err := getRegionalizedID(redisID)
43 | if err != nil {
44 | klog.Errorf("could not get id and zone from %s: %v", redisID, err)
45 | continue
46 | }
47 |
48 | dbInstance, err := dbAPI.GetCluster(&redis.GetClusterRequest{
49 | Zone: scw.Zone(zone),
50 | ClusterID: id,
51 | })
52 | if err != nil {
53 | klog.Errorf("could not get redis instance %s: %v", id, err)
54 | continue
55 | }
56 |
57 | var rule *redis.ACLRule
58 |
59 | for _, acl := range dbInstance.ACLRules {
60 | if *acl.Description == nodeName {
61 | rule = acl
62 | break
63 | }
64 | }
65 |
66 | if !exists && rule != nil {
67 | _, err := dbAPI.DeleteACLRule(&redis.DeleteACLRuleRequest{
68 | Zone: dbInstance.Zone,
69 | ACLID: rule.ID,
70 | })
71 | if err != nil {
72 | klog.Errorf("could not delete acl rule for node %s on redis instance %s: %v", nodeName, dbInstance.ID, err)
73 | retryOnError = true
74 | }
75 | continue
76 | }
77 |
78 | var nodePublicIP net.IP
79 |
80 | if os.Getenv(NodesIPSource) == NodesIPSourceKubernetes {
81 | for _, addr := range node.Status.Addresses {
82 | if addr.Type == v1.NodeExternalIP {
83 | // prefer ipv4 over ipv6 since Redis instances are only accessible via ipv4
84 | nodePublicIP = net.ParseIP(addr.Address).To4()
85 | if nodePublicIP != nil {
86 | // use first external ipv4 available
87 | break
88 | }
89 | }
90 | }
91 | } else {
92 | server, err := c.getInstanceFromNodeName(nodeName)
93 | if err != nil {
94 | klog.Errorf("could not get instance %s: %v", nodeName, err)
95 | continue
96 | }
97 |
98 | if server.PublicIP == nil {
99 | klog.Warningf("skipping node %s without public IP", nodeName)
100 | continue
101 | }
102 |
103 | nodePublicIP = server.PublicIP.Address
104 | }
105 |
106 | if nodePublicIP == nil {
107 | klog.Warningf("skipping node %s without public IP", nodeName)
108 | continue
109 | }
110 |
111 | nodeIP := net.IPNet{
112 | IP: nodePublicIP,
113 | Mask: net.IPv4Mask(255, 255, 255, 255), // TODO better idea?
114 | }
115 |
116 | if rule == nil || nodeIP.String() != rule.IPCidr.String() {
117 | _, err := dbAPI.AddACLRules(&redis.AddACLRulesRequest{
118 | Zone: dbInstance.Zone,
119 | ClusterID: dbInstance.ID,
120 | ACLRules: []*redis.ACLRuleSpec{
121 | {
122 | IPCidr: scw.IPNet{IPNet: nodeIP},
123 | Description: nodeName,
124 | },
125 | },
126 | })
127 | if err != nil {
128 | klog.Errorf("could not add acl rule for node %s with ip %s on redis instance %s: %v", nodeName, nodeIP.String(), dbInstance.ID, err)
129 | retryOnError = true
130 | continue
131 | }
132 | }
133 | }
134 |
135 | if retryOnError {
136 | return fmt.Errorf("got retryable error")
137 | }
138 |
139 | return nil
140 | }
141 |
--------------------------------------------------------------------------------
/pkg/controllers/reserved_ip.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | instance "github.com/scaleway/scaleway-sdk-go/api/instance/v1"
8 | "k8s.io/api/core/v1"
9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10 | klog "k8s.io/klog/v2"
11 | )
12 |
13 | func (c *NodeController) syncReservedIP(nodeName string) error {
14 | if len(c.reservedIPs) == 0 {
15 | return nil
16 | }
17 |
18 | klog.Infof("adding a reserved IP on node %s", nodeName)
19 |
20 | _, exists, err := c.indexer.GetByKey(nodeName)
21 | if err != nil {
22 | klog.Errorf("could not get node %s by key: %v", nodeName, err)
23 | return err
24 | }
25 |
26 | if !exists {
27 | // ip will be detached on delete
28 | klog.Infof("node %s was deleted, ignoring", nodeName)
29 | return nil
30 | }
31 |
32 | server, err := c.getInstanceFromNodeName(nodeName)
33 | if err != nil {
34 | klog.Errorf("could not get server %s: %v", nodeName, err)
35 | return err
36 | }
37 |
38 | if server.PublicIP == nil {
39 | klog.Warning("node %s does not have a public IP")
40 | return nil
41 | }
42 |
43 | if !server.PublicIP.Dynamic {
44 | klog.Warningf("node %s already have a public IP", nodeName)
45 | err = c.addReservedIPLabel(nodeName)
46 | if err != nil {
47 | return err
48 | }
49 |
50 | return nil
51 | }
52 |
53 | ip, err := c.getFreeIP()
54 | if err != nil {
55 | klog.Errorf("could not get a free IP for node %s: %v", nodeName, err)
56 | return err
57 | }
58 |
59 | if ip == nil {
60 | klog.Warningf("no available reserved IPs for node %s", nodeName)
61 | return nil
62 | }
63 |
64 | instanceAPI := instance.NewAPI(c.scwClient)
65 |
66 | _, err = instanceAPI.UpdateIP(&instance.UpdateIPRequest{
67 | IP: ip.ID,
68 | Server: &instance.NullableStringValue{
69 | Value: server.ID,
70 | },
71 | })
72 | if err != nil {
73 | klog.Errorf("could not attach IP %s for node %s: %v", ip.ID, nodeName, err)
74 | return err
75 | }
76 |
77 | err = c.addReservedIPLabel(nodeName)
78 | if err != nil {
79 | return err
80 | }
81 |
82 | return nil
83 | }
84 |
85 | func (c *NodeController) addReservedIPLabel(nodeName string) error {
86 | nodeObj, _, err := c.indexer.GetByKey(nodeName)
87 | if err != nil {
88 | klog.Errorf("could not get node %s by key: %v", nodeName, err)
89 | return err
90 | }
91 |
92 | node, ok := nodeObj.(*v1.Node)
93 | if !ok {
94 | klog.Errorf("could not get node %s from obejct", nodeName)
95 | return fmt.Errorf("could not get node %s from obejct", nodeName)
96 | }
97 |
98 | if node.Labels == nil {
99 | node.Labels = make(map[string]string)
100 | }
101 |
102 | if value, ok := node.Labels[NodeLabelReservedIP]; ok && value == "true" {
103 | return nil
104 | }
105 |
106 | node.Labels[NodeLabelReservedIP] = "true"
107 |
108 | _, err = c.clientset.CoreV1().Nodes().Update(context.Background(), node, metav1.UpdateOptions{})
109 | if err != nil {
110 | klog.Errorf("could not add reserved IP label to node %s: %v", nodeName, err)
111 | return err
112 | }
113 | return nil
114 | }
115 |
--------------------------------------------------------------------------------
/pkg/controllers/reverse_ip.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "time"
7 |
8 | dns "github.com/scaleway/scaleway-sdk-go/api/domain/v2beta1"
9 | instance "github.com/scaleway/scaleway-sdk-go/api/instance/v1"
10 | klog "k8s.io/klog/v2"
11 | )
12 |
13 | const (
14 | waitingPropagation = time.Second * 30
15 | )
16 |
17 | func (c *NodeController) syncReverseIP(nodeName string) error {
18 | if c.reverseIPDomain == "" {
19 | return nil
20 | }
21 |
22 | klog.Infof("adding a reverse for IP on node %s", nodeName)
23 |
24 | _, exists, err := c.indexer.GetByKey(nodeName)
25 | if err != nil {
26 | klog.Errorf("could not get node %s by key: %v", nodeName, err)
27 | return err
28 | }
29 |
30 | dnsAPI := dns.NewAPI(c.scwClient)
31 | instanceAPI := instance.NewAPI(c.scwClient)
32 |
33 | if !exists {
34 | if c.scwZoneFound {
35 | maxPage := uint32(1000)
36 | records := []*dns.Record{}
37 | page := int32(0)
38 |
39 | for {
40 | page = page + 1
41 | listing, err := dnsAPI.ListDNSZoneRecords(&dns.ListDNSZoneRecordsRequest{
42 | DNSZone: c.scwZone,
43 | Page: &page,
44 | PageSize: &maxPage,
45 | Type: "A",
46 | })
47 | if err != nil {
48 | klog.Errorf("could not get checking record dns for node %s: %v", nodeName, err)
49 | return err
50 | }
51 | records = append(records, listing.Records...)
52 | if len(listing.Records) < int(maxPage) {
53 | break
54 | }
55 | }
56 |
57 | var recordToDelete *dns.Record
58 | for i := range records {
59 | if records[i].Comment != nil && *records[i].Comment == fmt.Sprintf("k8s node %s", nodeName) {
60 | recordToDelete = records[i]
61 | break
62 | }
63 | }
64 |
65 | if recordToDelete != nil {
66 | klog.Infof("try to remove record dns for node %s", nodeName)
67 |
68 | _, err := dnsAPI.UpdateDNSZoneRecords(&dns.UpdateDNSZoneRecordsRequest{
69 | DNSZone: c.scwZone,
70 | Changes: []*dns.RecordChange{
71 | &dns.RecordChange{
72 | Delete: &dns.RecordChangeDelete{
73 | ID: &recordToDelete.ID,
74 | },
75 | },
76 | },
77 | })
78 | if err != nil {
79 | klog.Errorf("could delete record dns for node %s: %v", nodeName, err)
80 | return err
81 | }
82 |
83 | klog.Infof("try to remove reverse for node %s", nodeName)
84 | instanceAPI.UpdateIP(&instance.UpdateIPRequest{
85 | IP: recordToDelete.Data,
86 | Reverse: &instance.NullableStringValue{Null: true},
87 | })
88 |
89 | }
90 | }
91 |
92 | klog.Infof("node %s was deleted, ignoring", nodeName)
93 | return nil
94 | }
95 |
96 | server, err := c.getInstanceFromNodeName(nodeName)
97 | if err != nil {
98 | klog.Errorf("could not get server %s: %v", nodeName, err)
99 | return err
100 | }
101 |
102 | if server.PublicIP == nil {
103 | klog.Warning("node %s does not have a public IP")
104 | return nil
105 | }
106 |
107 | if server.PublicIP.Dynamic {
108 | klog.Warningf("can't update the reverse of a dynamic IP for node %s", nodeName)
109 | return nil
110 | }
111 |
112 | if c.scwZoneFound {
113 | comment := fmt.Sprintf("k8s node %s", nodeName)
114 | _, err := dnsAPI.UpdateDNSZoneRecords(&dns.UpdateDNSZoneRecordsRequest{
115 | DNSZone: c.scwZone,
116 | Changes: []*dns.RecordChange{
117 | &dns.RecordChange{
118 | Add: &dns.RecordChangeAdd{
119 | Records: []*dns.Record{
120 | &dns.Record{
121 | Data: server.PublicIP.Address.String(),
122 | Name: fmt.Sprintf("%s.%s.", getReversePrefix(server.PublicIP.Address), c.reverseIPDomain),
123 | TTL: 600,
124 | Type: "A",
125 | Comment: &comment,
126 | },
127 | },
128 | },
129 | },
130 | },
131 | })
132 | if err != nil {
133 | klog.Errorf("could not update record dns for node %s: %v", nodeName, err)
134 | return err
135 | }
136 | klog.Infof("waiting propagation for record dns for node %s", nodeName)
137 | time.Sleep(waitingPropagation)
138 | }
139 |
140 | _, err = instanceAPI.UpdateIP(&instance.UpdateIPRequest{
141 | IP: server.PublicIP.Address.String(),
142 | Reverse: &instance.NullableStringValue{
143 | Value: fmt.Sprintf("%s.%s", getReversePrefix(server.PublicIP.Address), c.reverseIPDomain),
144 | },
145 | })
146 | if err != nil {
147 | klog.Errorf("could not update reverse on IP %s for node %s: %v", server.PublicIP.Address.String(), nodeName, err)
148 | return err
149 | }
150 |
151 | return nil
152 | }
153 |
154 | func getReversePrefix(ip net.IP) string {
155 | ip = ip.To4()
156 | // TODO better handling, error prone
157 | return fmt.Sprintf("%d-%d-%d-%d", ip[3], ip[2], ip[1], ip[0])
158 | }
159 |
--------------------------------------------------------------------------------
/pkg/controllers/security_group.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "strings"
7 |
8 | instance "github.com/scaleway/scaleway-sdk-go/api/instance/v1"
9 | "github.com/scaleway/scaleway-sdk-go/scw"
10 | "k8s.io/api/core/v1"
11 | klog "k8s.io/klog/v2"
12 | )
13 |
14 | func (c *SvcController) syncSecurityGroup(svcName string) error {
15 | if len(c.securityGroupIDs) == 0 {
16 | return nil
17 | }
18 |
19 | svcObj, exists, err := c.indexer.GetByKey(svcName)
20 | if err != nil {
21 | klog.Errorf("could not get service %s by key: %v", svcName, err)
22 | return err
23 | }
24 |
25 | if !exists {
26 | klog.Warningf("service %s does not exists, ignoring", svcName)
27 | return nil
28 | }
29 |
30 | svc, ok := svcObj.(*v1.Service)
31 | if !ok {
32 | klog.Errorf("could not get service %s from obejct", svcName)
33 | return fmt.Errorf("could not get service %s from obejct", svcName)
34 | }
35 |
36 | instanceAPI := instance.NewAPI(c.scwClient)
37 |
38 | gotErr := false
39 |
40 | for _, id := range c.securityGroupIDs {
41 | klog.Infof("syncing security group %s with service %s", id, svcName)
42 | sgID, zone, err := getZonalID(id)
43 | if err != nil {
44 | klog.Errorf("could not get id and zone from %s: %v", sgID, err)
45 | gotErr = true
46 | continue
47 | }
48 |
49 | sgRulesResp, err := instanceAPI.ListSecurityGroupRules(&instance.ListSecurityGroupRulesRequest{
50 | SecurityGroupID: sgID,
51 | Zone: scw.Zone(zone),
52 | }, scw.WithAllPages())
53 | if err != nil {
54 | klog.Errorf("could not list rules for security group %s: %v", sgID, err)
55 | gotErr = true
56 | continue
57 | }
58 |
59 | for _, port := range svc.Spec.Ports {
60 | found := false
61 | if port.NodePort == 0 {
62 | continue
63 | }
64 | for _, sgRule := range sgRulesResp.Rules {
65 | if sgRule.Action != instance.SecurityGroupRuleActionAccept || sgRule.Direction != instance.SecurityGroupRuleDirectionInbound || sgRule.Protocol.String() != string(port.Protocol) {
66 | continue
67 | }
68 | if sgRule.DestPortFrom != nil && sgRule.DestPortTo != nil && *sgRule.DestPortFrom == *sgRule.DestPortTo && *sgRule.DestPortFrom == uint32(port.NodePort) {
69 | found = true
70 | break
71 | }
72 | }
73 | if !found {
74 | _, err := instanceAPI.CreateSecurityGroupRule(&instance.CreateSecurityGroupRuleRequest{
75 | SecurityGroupID: sgID,
76 | Zone: scw.Zone(zone),
77 | Action: instance.SecurityGroupRuleActionAccept,
78 | Direction: instance.SecurityGroupRuleDirectionInbound,
79 | Protocol: instance.SecurityGroupRuleProtocol(port.Protocol),
80 | IPRange: scw.IPNet{
81 | IPNet: net.IPNet{
82 | IP: net.ParseIP("0.0.0.0"),
83 | Mask: net.IPv4Mask(0, 0, 0, 0), // TODO better idea?
84 | },
85 | },
86 | DestPortFrom: scw.Uint32Ptr(uint32(port.NodePort)),
87 | DestPortTo: scw.Uint32Ptr(uint32(port.NodePort)),
88 | })
89 | if err != nil {
90 | klog.Errorf("could not create security group rule for svc %s port %s: %v", svcName, port.NodePort, err)
91 | gotErr = true
92 | continue
93 | }
94 | }
95 | }
96 | }
97 |
98 | if gotErr {
99 | return fmt.Errorf("got some errors")
100 | }
101 |
102 | return nil
103 |
104 | }
105 |
106 | func (c *NodeController) syncSecurityGroup(nodeName string) error {
107 | if len(c.securityGroupIDs) == 0 {
108 | return nil
109 | }
110 |
111 | _, exists, err := c.indexer.GetByKey(nodeName)
112 | if err != nil {
113 | klog.Errorf("could not get node %s by key: %v", nodeName, err)
114 | return err
115 | }
116 |
117 | server, err := c.getInstanceFromNodeName(nodeName)
118 | if err != nil {
119 | klog.Warningf("could not get instance %s: %v", nodeName, err)
120 | if exists {
121 | return err
122 | }
123 | // end here if node does not exists anymore and we couldn't get the server
124 | // in order to delete the old IP
125 | return nil
126 | }
127 |
128 | instanceAPI := instance.NewAPI(c.scwClient)
129 |
130 | gotErr := false
131 |
132 | for _, id := range c.securityGroupIDs {
133 | klog.Infof("syncing security group %s with node %s", id, nodeName)
134 | sgID, zone, err := getZonalID(id)
135 | if err != nil {
136 | klog.Errorf("could not get id and zone from %s: %v", sgID, err)
137 | gotErr = true
138 | continue
139 | }
140 | if zone != "" && zone != server.Zone.String() {
141 | klog.Warningf("ignoring security group %s as it's not in the same zone as the node %s", sgID, nodeName)
142 | continue
143 | }
144 |
145 | sgRulesResp, err := instanceAPI.ListSecurityGroupRules(&instance.ListSecurityGroupRulesRequest{
146 | SecurityGroupID: sgID,
147 | Zone: server.Zone,
148 | }, scw.WithAllPages())
149 | if err != nil {
150 | klog.Errorf("could not list rules for security group %s: %v", sgID, err)
151 | gotErr = true
152 | continue
153 | }
154 |
155 | toDelete := []string{}
156 | foundPrivate := false
157 | foundPublic := false
158 |
159 | for _, sgRule := range sgRulesResp.Rules {
160 | if server.PublicIP != nil && sgRule.IPRange.IP.Equal(server.PublicIP.Address) {
161 | foundPublic = true
162 | if !exists {
163 | toDelete = append(toDelete, sgRule.ID)
164 | }
165 | }
166 | if server.PrivateIP != nil && *server.PrivateIP != "" && sgRule.IPRange.IP.Equal(net.ParseIP(*server.PrivateIP)) {
167 | foundPrivate = true
168 | if !exists {
169 | toDelete = append(toDelete, sgRule.ID)
170 | }
171 | }
172 | }
173 |
174 | for _, delID := range toDelete {
175 | err := instanceAPI.DeleteSecurityGroupRule(&instance.DeleteSecurityGroupRuleRequest{
176 | Zone: server.Zone,
177 | SecurityGroupID: sgID,
178 | SecurityGroupRuleID: delID,
179 | })
180 | if err != nil {
181 | klog.Errorf("could not delete security group rule %s for SG %s: %v", delID, sgID, err)
182 | gotErr = true
183 | continue
184 | }
185 | }
186 |
187 | toAdd := []net.IP{}
188 | if !foundPrivate && exists {
189 | toAdd = append(toAdd, net.ParseIP(*server.PrivateIP))
190 | }
191 | if !foundPublic && exists {
192 | toAdd = append(toAdd, server.PublicIP.Address)
193 | }
194 |
195 | for _, ip := range toAdd {
196 | _, err := instanceAPI.CreateSecurityGroupRule(&instance.CreateSecurityGroupRuleRequest{
197 | SecurityGroupID: sgID,
198 | Zone: server.Zone,
199 | Action: instance.SecurityGroupRuleActionAccept,
200 | Direction: instance.SecurityGroupRuleDirectionInbound,
201 | Protocol: instance.SecurityGroupRuleProtocolANY,
202 | IPRange: scw.IPNet{
203 | IPNet: net.IPNet{
204 | IP: ip,
205 | Mask: net.IPv4Mask(255, 255, 255, 255), // TODO better idea?
206 | },
207 | },
208 | })
209 | if err != nil {
210 | klog.Errorf("could not add security group rule for node %s on %s: %v", nodeName, sgID, err)
211 | gotErr = true
212 | continue
213 | }
214 | }
215 | }
216 |
217 | if gotErr {
218 | return fmt.Errorf("got some errors")
219 | }
220 |
221 | return nil
222 | }
223 |
224 | func getZonalID(r string) (string, string, error) {
225 | split := strings.Split(r, "/")
226 | switch len(split) {
227 | case 1:
228 | return split[0], "", nil
229 | case 2:
230 | return split[1], split[0], nil
231 | default:
232 | return "", "", fmt.Errorf("couldn't parse ID %s", r)
233 | }
234 | }
235 |
--------------------------------------------------------------------------------
/pkg/controllers/svc_controller.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strings"
7 | "time"
8 |
9 | "github.com/scaleway/scaleway-sdk-go/scw"
10 | "k8s.io/api/core/v1"
11 | "k8s.io/apimachinery/pkg/fields"
12 | "k8s.io/apimachinery/pkg/util/runtime"
13 | "k8s.io/apimachinery/pkg/util/wait"
14 | "k8s.io/client-go/kubernetes"
15 | "k8s.io/client-go/tools/cache"
16 | "k8s.io/client-go/util/workqueue"
17 | klog "k8s.io/klog/v2"
18 | )
19 |
20 | func NewSvcController(clientset *kubernetes.Clientset) (*SvcController, error) {
21 | svcListWatcher := cache.NewListWatchFromClient(clientset.CoreV1().RESTClient(), "services", "", fields.Everything())
22 |
23 | queue := workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter())
24 |
25 | indexer, informer := cache.NewIndexerInformer(svcListWatcher, &v1.Service{}, 0, cache.ResourceEventHandlerFuncs{
26 | AddFunc: func(obj interface{}) {
27 | key, err := cache.MetaNamespaceKeyFunc(obj)
28 | if err == nil {
29 | if svc, ok := obj.(*v1.Service); ok && isPublicSvc(svc) {
30 | queue.Add(key)
31 | }
32 | }
33 | },
34 | UpdateFunc: func(old interface{}, new interface{}) {
35 | key, err := cache.MetaNamespaceKeyFunc(new)
36 | if err == nil {
37 | newSvc, newOk := new.(*v1.Service)
38 | if newOk && isPublicSvc(newSvc) {
39 | queue.Add(key)
40 | }
41 | }
42 | },
43 | }, cache.Indexers{})
44 |
45 | scwClient, err := scw.NewClient(scw.WithEnv())
46 | if err != nil {
47 | return nil, err
48 | }
49 |
50 | controller := &SvcController{
51 | indexer: indexer,
52 | informer: informer,
53 | queue: queue,
54 | scwClient: scwClient,
55 | numberRetries: defaultNumberRetries,
56 | }
57 |
58 | // TODO handle validation here ?
59 | if os.Getenv(SecurityGroupIDs) != "" {
60 | controller.securityGroupIDs = strings.Split(os.Getenv(SecurityGroupIDs), ",")
61 | }
62 |
63 | return controller, nil
64 | }
65 |
66 | func (c *SvcController) syncNeeded(nodeName string) error {
67 | var errs []error
68 |
69 | err := c.syncSecurityGroup(nodeName)
70 | if err != nil {
71 | klog.Errorf("failed to sync security group for node %s: %v", nodeName, err)
72 | errs = append(errs, err)
73 | }
74 |
75 | if len(errs) == 0 {
76 | return nil
77 | }
78 |
79 | return fmt.Errorf("got several error")
80 | }
81 |
82 | func (c *SvcController) processNextItem() bool {
83 | key, quit := c.queue.Get()
84 | if quit {
85 | return false
86 | }
87 | defer c.queue.Done(key)
88 |
89 | err := c.syncNeeded(key.(string))
90 | c.handleErr(err, key)
91 | return true
92 | }
93 |
94 | func (c *SvcController) handleErr(err error, key interface{}) {
95 | if err == nil {
96 | c.queue.Forget(key)
97 | return
98 | }
99 |
100 | if c.queue.NumRequeues(key) < c.numberRetries {
101 | c.queue.AddRateLimited(key)
102 | return
103 | }
104 |
105 | c.queue.Forget(key)
106 | runtime.HandleError(err)
107 | klog.Infof("too many retries for key %s: %v", key, err)
108 | }
109 |
110 | func (c *SvcController) Run(stopCh chan struct{}) {
111 | defer runtime.HandleCrash()
112 | defer c.Wg.Done()
113 |
114 | defer c.queue.ShutDown()
115 |
116 | go c.informer.Run(stopCh)
117 |
118 | if !cache.WaitForCacheSync(stopCh, c.informer.HasSynced) {
119 | runtime.HandleError(fmt.Errorf("timed out waiting for caches to sync"))
120 | return
121 | }
122 |
123 | go wait.Until(c.runWorker, time.Second, stopCh)
124 |
125 | <-stopCh
126 | }
127 |
128 | func (c *SvcController) runWorker() {
129 | for c.processNextItem() {
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/pkg/controllers/types.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "sync"
5 |
6 | "github.com/scaleway/scaleway-sdk-go/scw"
7 | "k8s.io/client-go/kubernetes"
8 | "k8s.io/client-go/tools/cache"
9 | "k8s.io/client-go/util/workqueue"
10 | )
11 |
12 | const (
13 | defaultNumberRetries = 30
14 | )
15 |
16 | type NodeController struct {
17 | Wg sync.WaitGroup
18 |
19 | clientset kubernetes.Interface
20 | indexer cache.Indexer
21 | queue workqueue.RateLimitingInterface
22 | informer cache.Controller
23 |
24 | scwClient *scw.Client
25 |
26 | reverseIPDomain string
27 | scwZoneFound bool
28 | scwZone string
29 | databaseIDs []string
30 | redisIDs []string
31 | reservedIPs []string
32 | securityGroupIDs []string
33 |
34 | numberRetries int
35 | }
36 |
37 | type SvcController struct {
38 | Wg sync.WaitGroup
39 |
40 | indexer cache.Indexer
41 | queue workqueue.RateLimitingInterface
42 | informer cache.Controller
43 |
44 | scwClient *scw.Client
45 |
46 | securityGroupIDs []string
47 |
48 | numberRetries int
49 | }
50 |
--------------------------------------------------------------------------------
/pkg/controllers/utils.go:
--------------------------------------------------------------------------------
1 | package controllers
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 |
7 | instance "github.com/scaleway/scaleway-sdk-go/api/instance/v1"
8 | "github.com/scaleway/scaleway-sdk-go/scw"
9 | "k8s.io/api/core/v1"
10 | )
11 |
12 | func (c *NodeController) getInstanceFromNodeName(nodeName string) (*instance.Server, error) {
13 | instanceAPI := instance.NewAPI(c.scwClient)
14 |
15 | instanceResp, err := instanceAPI.ListServers(&instance.ListServersRequest{
16 | Name: scw.StringPtr(nodeName),
17 | })
18 | if err != nil {
19 | return nil, err
20 | }
21 | if len(instanceResp.Servers) != 1 {
22 | return nil, fmt.Errorf("got %d servers instead of 1", len(instanceResp.Servers))
23 | }
24 | return instanceResp.Servers[0], nil
25 | }
26 |
27 | func (c *NodeController) getFreeIP() (*instance.IP, error) {
28 | instanceAPI := instance.NewAPI(c.scwClient)
29 |
30 | ipsList, err := instanceAPI.ListIPs(&instance.ListIPsRequest{}, scw.WithAllPages())
31 | if err != nil {
32 | return nil, err
33 | }
34 |
35 | for _, ip := range ipsList.IPs {
36 | if ip.Server == nil && stringInSlice(ip.Address.String(), c.reservedIPs) {
37 | return ip, nil
38 | }
39 | }
40 | return nil, nil
41 | }
42 |
43 | func stringInSlice(s string, slice []string) bool {
44 | for _, i := range slice {
45 | if i == s {
46 | return true
47 | }
48 | }
49 | return false
50 | }
51 |
52 | func isPublicSvc(svc *v1.Service) bool {
53 | return svc.Spec.Type == v1.ServiceTypeLoadBalancer || svc.Spec.Type == v1.ServiceTypeNodePort
54 | }
55 |
56 | func getRegionalizedID(r string) (string, string, error) {
57 | split := strings.Split(r, "/")
58 | switch len(split) {
59 | case 1:
60 | return split[0], "", nil
61 | case 2:
62 | return split[1], split[0], nil
63 | default:
64 | return "", "", fmt.Errorf("couldn't parse ID %s", r)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------