Skip to content

Commit

Permalink
feat: add initialization of garm instance (#30)
Browse files Browse the repository at this point in the history
This PR allows a new (uninitialized) garm instance to be initialized by
the garm operator
  • Loading branch information
H777K authored Dec 6, 2023
1 parent 036999a commit 5c34d41
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 1 deletion.
18 changes: 17 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

garmoperatorv1alpha1 "github.com/mercedes-benz/garm-operator/api/v1alpha1"
"github.com/mercedes-benz/garm-operator/internal/controller"
"github.com/mercedes-benz/garm-operator/pkg/client"
"github.com/mercedes-benz/garm-operator/pkg/config"
"github.com/mercedes-benz/garm-operator/pkg/flags"
)
Expand Down Expand Up @@ -100,6 +101,21 @@ func main() {
os.Exit(1)
}

ctx := ctrl.SetupSignalHandler()

if config.Config.Garm.Init {
initClient := client.NewInitClient()
if err = initClient.Init(ctx, client.GarmScopeParams{
BaseURL: config.Config.Garm.Server,
Username: config.Config.Garm.Username,
Password: config.Config.Garm.Password,
Email: config.Config.Garm.Email,
}); err != nil {
setupLog.Error(err, "failed to initialize GARM")
os.Exit(1)
}
}

if err = (&controller.EnterpriseReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Expand Down Expand Up @@ -175,7 +191,7 @@ func main() {
}

setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
if err := mgr.Start(ctx); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
Expand Down
110 changes: 110 additions & 0 deletions docs/adrs/garm_server_initialization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<!-- SPDX-License-Identifier: MIT -->

---
date: 2023-12-04
desc: Garm Server Initialization
state: accepted
---
<!--
What is the status, such as proposed, accepted, rejected, deprecated, superseded, etc.?
-->

<!--
This is a basic ADR template from [Documenting architecture decisions - Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).
It's possible to manage the ADR files with [adr-tools](https://github.com/npryce/adr-tools).
-->
# Garm Server Initialization

## Context and Problem Statement

<!--
Describe the context and the problem that we are trying to solve.
-->

A newly deployed garm-server instance must first be initialized before a login can take place.
Therefore, the `garm-operator` cannot perform a login if the garm-server instance has not yet been initialized.

## Decision Drivers

<!--
List possible facts which may influence the decision.
-->

* The usability of the garm-server initialization functionality
* The maintenance effort

## Considered Options

<!--
List possible options to address the problem or issue.
-->

* Attach a init container to the garm-server pod
* Attach a sidecar container to the garm-server pod
* Create entrypoint script for the garm-server image
* Implement the init functionality in the `garm-operator`

## Pros and Cons of the Options

<!--
List the pros and cons of each option.
-->

### Attach an init container to the garm-server pod

#### Pros

* The initialization is executed before the garm-server instance starts
* The init container only needs more cluster resources for a short time until the initialization has been completed

#### Cons

* The init container would have to contain a script, `garm` and `garm-cli` binary, which would lead to a higher maintenance effort
* We would have to build and maintain a garm-server-init container image
* The garm-server deployment is currently not provided by us, so other users would have to implement it by themselves

### Attach an sidecar container to the garm-server pod

#### Pros

* The sidecar container can constantly monitor the garm-server instance and initialize it if necessary
* Requires only a script and the `garm-cli` binary

#### Cons

* The sidecar container would run constantly and consume additional cluster resources, although it is only needed for initialization
* We would have to build and maintain a garm-server-sidecar container image
* The garm-server deployment is currently not provided by us, so other users would have to implement it by themselves

### Create entrypoint script for the garm-server image

#### Pros

* We only have to maintain one image
* The garm-server instance can be initialized at any time when the entrypoint script is running in the background

#### Cons

* The garm-server image is currently not provided by us, so other users would have to implement it themselves

### Implement the init functionality in the `garm-operator`

#### Pros

* The initialization functionality can be used by anyone who uses the `garm-operator`
* It allows us to implement better error handling
* With [koanf](https://github.com/knadh/koanf) we can more easily control whether initialization should be performed or not

#### Cons

* Adding an additional functionality which is not part of the core functionality of the `garm-operator`

## Decision Outcome

<!--
What option was chosen? Why?
-->

The initialization of the garm-server should be usable for everyone, for this reason we decided to implement this functionality in the `garm-operator`.
We would also like to reduce further maintaining overhead by adding more images which would be necessary, for example, with the use of an init or sidecar container.
8 changes: 8 additions & 0 deletions docs/config/configuration-parsing.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ All ENVs with the `OPERATOR_` and `GARM_` prefix will be merged by koanf. Howeve
GARM_SERVER
GARM_USERNAME
GARM_PASSWORD
GARM_INIT
GARM_EMAIL
OPERATOR_METRICS_BIND_ADDRESS
OPERATOR_HEALTH_PROBE_BIND_ADDRESS
Expand All @@ -53,6 +55,8 @@ The following flags will be parsed and can be found in the [flags package](../..
--garm-server
--garm-username
--garm-password
--garm-init
--garm-email
--operator-metrics-bind-address
--operator-health-probe-bind-address
Expand Down Expand Up @@ -80,6 +84,8 @@ garm:
server: http://garm-server:9997
username: admin
password: 123456789
init: false
email: ""
operator:
metricsbindaddress: :8080
healthprobebindaddress: :8081
Expand All @@ -98,6 +104,8 @@ garm:
server: "http://garm-server:9997"
username: "garm-username"
password: "garm-password"
init: false
email: ""

operator:
metrics_bind_address: ":7000"
Expand Down
46 changes: 46 additions & 0 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@
package client

import (
"context"
"errors"
"fmt"
"net/url"
"strings"

"github.com/cloudbase/garm/client"
apiClientFirstRun "github.com/cloudbase/garm/client/first_run"
apiClientLogin "github.com/cloudbase/garm/client/login"
"github.com/cloudbase/garm/params"
"github.com/go-openapi/runtime"
openapiRuntimeClient "github.com/go-openapi/runtime/client"
"sigs.k8s.io/controller-runtime/pkg/log"

"github.com/mercedes-benz/garm-operator/pkg/metrics"
)
Expand All @@ -21,6 +25,7 @@ type GarmScopeParams struct {
Username string
Password string
Debug bool
Email string
}

func newGarmClient(garmParams GarmScopeParams) (*client.GarmAPI, runtime.ClientAuthInfoWriter, error) {
Expand Down Expand Up @@ -73,3 +78,44 @@ func newGarmClient(garmParams GarmScopeParams) (*client.GarmAPI, runtime.ClientA

return apiCli, authToken, nil
}

func initializeGarm(ctx context.Context, garmParams GarmScopeParams) error {
log := log.FromContext(ctx)

newUserReq := apiClientFirstRun.NewFirstRunParams()
newUserReq.Body = params.NewUserParams{
Username: garmParams.Username,
Password: garmParams.Password,
Email: garmParams.Email,
}

baseURLParsed, err := url.Parse(garmParams.BaseURL)
if err != nil {
return fmt.Errorf("failed to parse base url %s: %s", garmParams.BaseURL, err)
}

apiPath, err := url.JoinPath(baseURLParsed.Path, client.DefaultBasePath)
if err != nil {
return fmt.Errorf("failed to join base url path %s with %s: %s", baseURLParsed.Path, client.DefaultBasePath, err)
}

transportCfg := client.DefaultTransportConfig().
WithHost(baseURLParsed.Host).
WithBasePath(apiPath).
WithSchemes([]string{baseURLParsed.Scheme})
apiCli := client.NewHTTPClientWithConfig(nil, transportCfg)
authToken := openapiRuntimeClient.BearerToken("")

resp, err := apiCli.FirstRun.FirstRun(newUserReq, authToken)
if err != nil {
if strings.Contains(err.Error(), "(status 409)") {
log.Info("Garm is already initialized")
return nil
}
return fmt.Errorf("failed to initialize garm: %s", err)
}

log.Info("Garm initialized successfully with the following User", "ID", resp.Payload.ID, "username", resp.Payload.Username, "email", resp.Payload.Email, "enabled", resp.Payload.Enabled)

return nil
}
30 changes: 30 additions & 0 deletions pkg/client/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: MIT

package client

import (
"context"

"github.com/mercedes-benz/garm-operator/pkg/metrics"
)

type InitClient interface {
Init(ctx context.Context, garmParams GarmScopeParams) error
}

type initClient struct{}

func NewInitClient() InitClient {
return &initClient{}
}

func (s *initClient) Init(ctx context.Context, garmParams GarmScopeParams) error {
metrics.TotalGarmCalls.WithLabelValues("Init").Inc()
err := initializeGarm(ctx, garmParams)
if err != nil {
metrics.GarmCallErrors.WithLabelValues("Init").Inc()
return err
}

return nil
}
2 changes: 2 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ type GarmConfig struct {
Server string `koanf:"server" validate:"required,url"`
Username string `koanf:"username" validate:"required"`
Password string `koanf:"password" validate:"required"`
Init bool `koanf:"init"`
Email string `koanf:"email" validate:"required_if=Init true"`
}

type OperatorConfig struct {
Expand Down
8 changes: 8 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ func TestGenerateConfig(t *testing.T) {
Server: "http://localhost:9997",
Username: "admin",
Password: "password",
Init: true,
Email: "garm-operator@localhost",
},
},
},
Expand All @@ -65,6 +67,8 @@ func TestGenerateConfig(t *testing.T) {
Server: "http://localhost:9997",
Username: "admin",
Password: "password",
Init: true,
Email: "garm-operator@localhost",
},
},
},
Expand Down Expand Up @@ -92,6 +96,8 @@ func TestGenerateConfig(t *testing.T) {
Server: "http://localhost:9997",
Username: "admin",
Password: "password",
Init: true,
Email: "garm-operator@localhost",
},
},
},
Expand All @@ -118,6 +124,8 @@ func TestGenerateConfig(t *testing.T) {
Server: "http://garm-server:9997",
Username: "garm-username",
Password: "garm-password",
Init: true,
Email: "garm-operator@localhost",
},
},
},
Expand Down
2 changes: 2 additions & 0 deletions pkg/config/test_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ garm:
username: "garm-username"
password: "garm-password"
server: "http://garm-server:9997"
init: true
email: "garm-operator@localhost"

operator:
metrics_bind_address: ":7000"
Expand Down
4 changes: 4 additions & 0 deletions pkg/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ const (
DefaultLeaderElection = false
DefaultSyncPeriod = 5 * time.Minute
DefaultWatchNamespace = ""

// default values for garm configuration
DefaultGarmInit = true
DefaultGarmEmail = "garm-operator@localhost"
)
2 changes: 2 additions & 0 deletions pkg/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ func InitiateFlags() *pflag.FlagSet {
f.String("garm-server", "", "The address of the GARM server")
f.String("garm-username", "", "The username for the GARM server")
f.String("garm-password", "", "The password for the GARM server")
f.Bool("garm-init", defaults.DefaultGarmInit, "Enable initialization of new GARM Instance")
f.String("garm-email", defaults.DefaultGarmEmail, "The email address for the GARM server (only required if garm-init is set to true)")

f.Bool("dry-run", false, "If true, only print the object that would be sent, without sending it.")

Expand Down

0 comments on commit 5c34d41

Please sign in to comment.