├── .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 | --------------------------------------------------------------------------------