Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ The `pkg/collections` package contains multiple interfaces for collections, mode

The package also contains further packages that contain some auxiliary functions for working with slices and maps in golang, e.g. for filtering.

## clusters

The `pkg/clusters` package helps with loading kubeconfigs and creating clients for multiple clusters.
```go
foo := clusters.New("foo") // initializes a new cluster with id 'foo'
foo.RegisterConfigPathFlag(cmd.Flags()) // adds a '--foo-cluster' flag to the flag set for passing in a kubeconfig path
foo.InitializeRESTConfig() // loads the kubeconfig using the 'LoadKubeconfig' function from the 'controller' package
foo.InitializeClient(myScheme) // initializes the 'Client' and 'Cluster' interfaces from the controller-runtime
```
You can then use the different getter methods for working with the cluster.

### conditions

The `pkg/conditions` package helps with managing condition lists.
Expand Down Expand Up @@ -92,6 +103,9 @@ The `pkg/controller` package contains useful functions for setting up and runnin
#### Noteworthy Functions

- `LoadKubeconfig` creates a REST config for accessing a k8s cluster. It can be used with a path to a kubeconfig file, or a directory containing files for a trust relationship. When called with an empty path, it returns the in-cluster configuration.
- There are some functions useful for working with annotations and labels, e.g. `HasAnnotationWithValue` or `EnsureLabel`.
- There are multiple predefined predicates to help with filtering reconciliation triggers in controllers, e.g. `HasAnnotationPredicate` or `DeletionTimestampChangedPredicate`.
- The `K8sNameHash` function can be used to create a hash that can be used as a name for k8s resources.

### logging

Expand Down
175 changes: 175 additions & 0 deletions pkg/clusters/cluster.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package clusters

import (
"fmt"

flag "github.com/spf13/pflag"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/cluster"

"github.com/openmcp-project/controller-utils/pkg/controller"
)

type Cluster struct {
// identifier (for logging purposes only)
id string
// path to kubeconfig
cfgPath string
// cluster config
restCfg *rest.Config
// client
client client.Client
// cluster
cluster cluster.Cluster
}

// Initializes a new cluster.
// Panics if id is empty.
func New(id string) *Cluster {
c := &Cluster{}
c.InitializeID(id)
return c
}

// WithConfigPath sets the config path for the cluster.
// Returns the cluster for chaining.
func (c *Cluster) WithConfigPath(cfgPath string) *Cluster {
c.cfgPath = cfgPath
return c
}

// RegisterConfigPathFlag adds a flag '--<id>-cluster' for the cluster's config path to the given flag set.
// Panics if the cluster's id is not set.
func (c *Cluster) RegisterConfigPathFlag(flags *flag.FlagSet) {
if !c.HasID() {
panic("cluster id must be set before registering the config path flag")
}
flags.StringVar(&c.cfgPath, fmt.Sprintf("%s-cluster", c.id), "", fmt.Sprintf("Path to the %s cluster kubeconfig file or directory containing either a kubeconfig or host, token, and ca file. Leave empty to use in-cluster config.", c.id))
}

///////////////////
// STATUS CHECKS //
///////////////////

// HasID returns true if the cluster has an id.
// If this returns false, initialize a new cluster via New() or InitializeID().
func (c *Cluster) HasID() bool {
return c != nil && c.id != ""
}

// HasRESTConfig returns true if the cluster has a REST config.
// If this returns false, load the config via InitializeRESTConfig().
func (c *Cluster) HasRESTConfig() bool {
return c != nil && c.restCfg != nil
}

// HasClient returns true if the cluster has a client.
// If this returns false, create a client via InitializeClient().
func (c *Cluster) HasClient() bool {
return c != nil && c.client != nil
}

//////////////////
// INITIALIZERS //
//////////////////

// InitializeID sets the cluster's id.
// Panics if id is empty.
func (c *Cluster) InitializeID(id string) {
if id == "" {
panic("id must not be empty")
}
c.id = id
}

// InitializeRESTConfig loads the cluster's REST config.
// If the config has already been loaded, this is a no-op.
// Panics if the cluster's id is not set (InitializeID must be called first).
func (c *Cluster) InitializeRESTConfig() error {
if !c.HasID() {
panic("cluster id must be set before loading the config")
}
if c.HasRESTConfig() {
return nil
}
cfg, err := controller.LoadKubeconfig(c.cfgPath)
if err != nil {
return fmt.Errorf("failed to load '%s' cluster kubeconfig: %w", c.ID(), err)
}
c.restCfg = cfg
return nil
}

// InitializeClient creates a new client for the cluster.
// This also initializes the cluster's controller-runtime 'Cluster' representation.
// If the client has already been initialized, this is a no-op.
// Panics if the cluster's REST config has not been loaded (InitializeRESTConfig must be called first).
func (c *Cluster) InitializeClient(scheme *runtime.Scheme) error {
if !c.HasRESTConfig() {
panic("cluster REST config must be set before creating the client")
}
if c.HasClient() {
return nil
}
cli, err := client.New(c.restCfg, client.Options{Scheme: scheme})
if err != nil {
return fmt.Errorf("failed to create '%s' cluster client: %w", c.ID(), err)
}
clu, err := cluster.New(c.restCfg, func(o *cluster.Options) { o.Scheme = scheme })
if err != nil {
return fmt.Errorf("failed to create '%s' cluster Cluster representation: %w", c.ID(), err)
}
c.client = cli
c.cluster = clu
return nil
}

/////////////
// GETTERS //
/////////////

// ID returns the cluster's id.
func (c *Cluster) ID() string {
return c.id
}

// ConfigPath returns the cluster's config path.
func (c *Cluster) ConfigPath() string {
return c.cfgPath
}

// RESTConfig returns the cluster's REST config.
// This returns a pointer, but modification can lead to inconsistent behavior and is not recommended.
func (c *Cluster) RESTConfig() *rest.Config {
return c.restCfg
}

// Client returns the cluster's client.
func (c *Cluster) Client() client.Client {
return c.client
}

// Cluster returns the cluster's controller-runtime 'Cluster' representation.
func (c *Cluster) Cluster() cluster.Cluster {
return c.cluster
}

// Scheme returns the cluster's scheme.
// Returns nil if the client has not been initialized.
func (c *Cluster) Scheme() *runtime.Scheme {
if c.cluster == nil {
return nil
}
return c.cluster.GetScheme()
}

// APIServerEndpoint returns the cluster's API server endpoint.
// Returns an empty string if the REST config has not been initialized.
func (c *Cluster) APIServerEndpoint() string {
if c.restCfg == nil {
return ""
}
return c.restCfg.Host
}
37 changes: 37 additions & 0 deletions pkg/controller/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package controller

import (
"crypto/sha1"
"encoding/base32"
"reflect"
"strings"
)

const (
maxLength int = 63
Base32EncodeStdLowerCase = "abcdefghijklmnopqrstuvwxyz234567"
)

// K8sNameHash takes any number of string arguments and computes a hash out of it, which is then base32-encoded to be a valid k8s resource name.
// The arguments are joined with '/' before being hashed.
func K8sNameHash(ids ...string) string {
name := strings.Join(ids, "/")
h := sha1.New()
_, _ = h.Write([]byte(name))
// we need base32 encoding as some base64 (even url safe base64) characters are not supported by k8s
// see https://kubernetes.io/docs/concepts/overview/working-with-objects/names/
return base32.NewEncoding(Base32EncodeStdLowerCase).WithPadding(base32.NoPadding).EncodeToString(h.Sum(nil))
}

// IsNil checks if a given pointer is nil.
// Opposed to 'i == nil', this works for typed and untyped nil values.
func IsNil(i any) bool {
if i == nil {
return true
}
switch reflect.TypeOf(i).Kind() {
case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
return reflect.ValueOf(i).IsNil()
}
return false
}
14 changes: 7 additions & 7 deletions pkg/logging/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ func InitFlags(flagset *flag.FlagSet) {
}
fs := flag.NewFlagSet("log", flag.ExitOnError)

fs.BoolVar(&configFromFlags.Development, "dev", false, "enable development logging")
fs.BoolVar(&configFromFlags.Cli, "cli", false, "use CLI formatting for logs (color, no timestamps)")
f := fs.VarPF(&configFromFlags.Format, "format", "f", "logging format [text, json]")
fs.BoolVar(&configFromFlags.Development, "dev", false, "Enable development logging.")
fs.BoolVar(&configFromFlags.Cli, "cli", false, "Use CLI formatting for logs (color, no timestamps).")
f := fs.VarPF(&configFromFlags.Format, "format", "f", "Logging format [text, json].")
f.DefValue = "text if either dev or cli flag is set, json otherwise"
f = fs.VarPF(&configFromFlags.Level, "verbosity", "v", "logging verbosity [error, info, debug]")
f = fs.VarPF(&configFromFlags.Level, "verbosity", "v", "Logging verbosity [error, info, debug].")
f.DefValue = "info, or debug if dev flag is set"
fs.BoolVar(&configFromFlags.DisableStacktrace, "disable-stacktrace", true, "disable the stacktrace of error logs")
fs.BoolVar(&configFromFlags.DisableCaller, "disable-caller", true, "disable the caller of logs")
fs.BoolVar(&configFromFlags.DisableTimestamp, "disable-timestamp", false, "disable timestamp output")
fs.BoolVar(&configFromFlags.DisableStacktrace, "disable-stacktrace", true, "Disable the stacktrace of error logs.")
fs.BoolVar(&configFromFlags.DisableCaller, "disable-caller", true, "Disable the caller of logs.")
fs.BoolVar(&configFromFlags.DisableTimestamp, "disable-timestamp", false, "Disable timestamp output.")

configFromFlags.flagset = fs
flagset.AddFlagSet(configFromFlags.flagset)
Expand Down