mirror of
https://github.com/apricote/apricote.de.git
synced 2026-01-13 20:51:02 +00:00
199 lines
7.2 KiB
Markdown
199 lines
7.2 KiB
Markdown
---
|
|
author: "Julian Tölle"
|
|
title: "Part 1: Setting up the Boilerplate"
|
|
date: "2023-09-06"
|
|
description: |
|
|
Goal: Get a compilable binary running k/cloud-provider with no actual functionality yet.
|
|
summary: |
|
|
Get a compilable binary running k/cloud-provider with no actual functionality yet.
|
|
|
|
tags: ["kubernetes"]
|
|
ShowToc: true
|
|
ShowReadingTime: true
|
|
ShowBreadCrumbs: true
|
|
---
|
|
|
|
This is part 1 out of 5 of the series [Building a Kubernetes cloud-controller-manager]({{< ref "." >}}).
|
|
|
|
### Dependencies
|
|
|
|
As all ~~good~~ software does, we [stand on the shoulders of giants](https://xkcd.com/2347/). Let us review the two main dependencies that our code will use:
|
|
|
|
#### `k8s.io/cloud-provider`
|
|
|
|
> Repository: https://github.com/kubernetes/cloud-provider
|
|
|
|
Official Library by Kubernetes Project to implement cloud-controller-managers.
|
|
Historically part of `kubelet` but extracted a long time ago to build out-of-tree CCMs.
|
|
Handles communication with Kubernetes, CCM implementors only need to implement a narrowly-scoped interface for each functionality.
|
|
|
|
> This series will use Kubernetes `v1.28` throughout.
|
|
> If you use a different version, you may need to adjust your code.
|
|
> The `k8s.io/cloud-provider` release series associated with Kubernetes `v1.28` is `v0.28.x`.
|
|
|
|
#### `github.com/hetznercloud/hcloud-go`
|
|
|
|
> Repository: https://github.com/hetznercloud/hcloud-go
|
|
|
|
Official Go SDK by Hetzner Cloud, currently maintained by my team.
|
|
Exposes all functionality of the Hetzner Cloud API in (mostly) idiomatic Go.
|
|
|
|
### Repo initialization
|
|
|
|
```shell
|
|
mkdir ccm-from-scratch
|
|
cd ccm-from-scratch
|
|
git init
|
|
|
|
I like to make an empty initial commit, makes the occasional
|
|
rebase of the first few commits so much easier.
|
|
git commit --allow-empty -m "feat: init repo"
|
|
|
|
# Optional
|
|
git remote add git@github.com:apricote/ccm-from-scratch.git
|
|
git push
|
|
```
|
|
|
|
### `main.go`
|
|
|
|
> Source: https://github.com/kubernetes/cloud-provider/tree/v0.28.2/sample
|
|
|
|
Thankfully, `k8s.io/cloud-provider` provides an example entrypoint to get us started with out CCM. Lets review the code to make sure we understand what is happening:
|
|
|
|
```go
|
|
package main
|
|
|
|
// [Imports]
|
|
|
|
// The main function is called as the entrypoint of our Go binary.
|
|
func main() {
|
|
// k8s.io/cloud-provider has an elaborate way to read the configuration from flags.
|
|
// I found this very tedious to debug, but at least we do not have to do this ourselves.
|
|
ccmOptions, err := options.NewCloudControllerManagerOptions()
|
|
if err != nil {
|
|
klog.Fatalf("unable to initialize command options: %v", err)
|
|
}
|
|
|
|
// Can be used to add custom flags to the binary, we don't need this.
|
|
fss := cliflag.NamedFlagSets{}
|
|
|
|
// This initializes the CLI command. The arguments are interesting, so lets take a closer look:
|
|
command := app.NewCloudControllerManagerCommand(
|
|
// The options we initialized earlier.
|
|
ccmOptions,
|
|
// This is a custom function that needs to return a [cloudprovider.Interface],
|
|
// we will get to this soon.
|
|
cloudInitializer,
|
|
// This defines which controllers are started, if wanted,
|
|
// one can add additional controller loops heroe
|
|
app.DefaultInitFuncConstructors,
|
|
// Kubernetes v1.28 renamed the controllers to more sensible names, this
|
|
// map makes it so that old command-line arguments (--controllers) still work
|
|
names.CCMControllerAliases(),
|
|
// Our optional additional flags
|
|
fss,
|
|
// A [<-chan struct{}] that can be used to shut down the CCM on demand,
|
|
// we do not need this.
|
|
wait.NeverStop,
|
|
)
|
|
// Actually run the command to completion.
|
|
code := cli.Run(command)
|
|
os.Exit(code)
|
|
}
|
|
```
|
|
|
|
Now, this does not compile right now. We use the undefined `cloudInitalizer` method. The method signature we need to implement is `(*config.CompletedConfig) cloudprovider.Interface`. The sample code includes this method, but I found it overly complex for our small CCM, so we will implement it ourselves. Lets take a closer look at the interface we need to return in the next section.
|
|
|
|
### `cloudprovider.Interface`
|
|
|
|
> Source: https://github.com/kubernetes/cloud-provider/blob/v0.28.2/cloud.go#L42-L69
|
|
|
|
This is the entrypoint into the functionality we can (and will!) implement for our CCM. The interface includes an initializer, two cloud-provider metadata methods and a bunch of Getter functions to other interfaces that implement the actual functionality.
|
|
|
|
Before we can write the `cloudInitializer` method from above, lets prepare a struct that implements the interface:
|
|
|
|
```go
|
|
package ccm
|
|
// ccm/cloud.go
|
|
|
|
import (
|
|
cloudprovider "k8s.io/cloud-provider"
|
|
)
|
|
|
|
type CloudProvider struct {}
|
|
|
|
// Let's try to assign our struct to a var of the interface we try to implement.
|
|
// This way we get nice feedback from our IDE if we break the contract.
|
|
var _ cloudprovider.Interface = CloudProvider{}
|
|
|
|
|
|
// Can be used to setup our own controllers or goroutines that need to talk to
|
|
//Kubernetes. Not needed for our implementation, so we will leave it empty.
|
|
func (c CloudProvider) Initialize(clientBuilder cloudprovider.ControllerClientBuilder, stop <-chan struct{}) {}
|
|
|
|
// An arbitrary name for our provider. We will use "ccm-from-scratch" for this example.
|
|
func (c CloudProvider) ProviderName() string { return "ccm-from-scratch" }
|
|
|
|
// I have not yet figured out what Cluster ID is actually supposed to do. We can disable it.
|
|
func (c CloudProvider) HasClusterID() bool { return false }
|
|
|
|
// The following methods all expose additional interfaces. This allows us to
|
|
// modularly choose what we want to support. As long as we return "false" as the
|
|
// second argument, the associated functionality is disabled for our CCM.
|
|
// We will get to implementing [InstancesV2], [LoadBalancer] & [Routes] in later parts of this series.
|
|
func (c CloudProvider) LoadBalancer() (cloudprovider.LoadBalancer, bool) { return nil, false }
|
|
func (c CloudProvider) Instances() (cloudprovider.Instances, bool) { return nil, false }
|
|
func (c CloudProvider) InstancesV2() (cloudprovider.InstancesV2, bool) { return nil, false }
|
|
func (c CloudProvider) Zones() (cloudprovider.Zones, bool) { return nil, false }
|
|
func (c CloudProvider) Clusters() (cloudprovider.Clusters, bool) { return nil, false }
|
|
func (c CloudProvider) Routes() (cloudprovider.Routes, bool) { return nil, false }
|
|
```
|
|
|
|
Now that we have a struct implementing the interface, lets create our `cloudInitializer` method. We will actually do this in the same file as our struct:
|
|
|
|
```go
|
|
package ccm
|
|
// ccm/cloud.go
|
|
|
|
// Just create a new struct for now, we will add some more stuff to this later.
|
|
func NewCloudProvider(_ *config.CompletedConfig) cloudprovider.Interface {
|
|
return CloudProvider{}
|
|
}
|
|
```
|
|
|
|
And now we can pass this method in our `main()`:
|
|
|
|
```go {hl_lines=[7,15]}
|
|
package main
|
|
// main.go
|
|
|
|
import (
|
|
// Existing imports
|
|
|
|
"github.com/apricote/ccm-from-scratch/ccm"
|
|
)
|
|
|
|
func main() {
|
|
// [ other code ]
|
|
|
|
command := app.NewCloudControllerManagerCommand(
|
|
ccmOptions,
|
|
ccm.NewCloudProvider,
|
|
app.DefaultInitFuncConstructors,
|
|
names.CCMControllerAliases(),
|
|
fss,
|
|
wait.NeverStop,
|
|
)
|
|
}
|
|
```
|
|
|
|
### Starting our CCM
|
|
|
|
We now have all the pieces in place to try to run our program.
|
|
|
|
For now, we can use `go run` to test our binary. The next part of this series will implement a proper development environment.
|
|
|
|
```shell
|
|
$ go run .
|
|
TODO figure out actual arguments necessary and output
|
|
```
|