Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DOM-47678] Add Keycloak service account token support for image builds #101

Closed
wants to merge 4 commits into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ spec:
description: DisableCacheLayerExport will remove the "inline" cache
metadata from the image configuration.
type: boolean
enableServiceAccountTokenInjection:
description: EnableServiceAccountTokenInjection adds a service account
JWT token as build-arg to the images. This supports use cases like
model building that must access other Domino services
type: boolean
images:
description: Images is a list of images to build and push.
items:
Expand Down
8 changes: 8 additions & 0 deletions deployments/helm/hephaestus/templates/controller/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ stringData:
labels:
{{- .labels | toYaml | nindent 8 }}
{{- end }}
keycloak:
{{- with .Values.keycloak }}
enabled: {{ .enabled }}
server: {{ .server }}
realm: {{ .realm }}
clientId: {{ .clientId }}
clientSecret: {{ .clientSecret }}
{{- end }}
buildkit:
namespace: {{ .Release.Namespace }}
daemonPort: {{ .Values.buildkit.service.port }}
Expand Down
13 changes: 13 additions & 0 deletions deployments/helm/hephaestus/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ istio:
# network settings when CNI plugin is NOT installed.
cni: false

# Keycloak configuration to optionally provide a service account JWT token to executing builds
keycloak:
# Enable support for acquiring a Keycloak JWT for this service and making it available to builds
enabled: false
# Address of the Keycloak endpoint - auth may or may not be required depending on KeyCloak version and its configuration
server: "http://keycloak-http/auth/"
# Realm where the given client id logs in
realm: "DominoRealm"
# Identifies the client for use in API calls
clientId: ""
# In conjunction clientId, defines the credentials used in Keycloak
clientSecret: ""

# New Relic APM configuration
newRelic:
# Enable monitoring
Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ require (
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.6.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0
github.com/Nerzal/gocloak/v13 v13.7.0
github.com/dominodatalab/amqp-client v0.1.3
github.com/dominodatalab/controller-util v0.0.2
github.com/hashicorp/go-retryablehttp v0.7.1
Expand Down Expand Up @@ -99,6 +100,7 @@ require (
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-resty/resty/v2 v2.7.0 // indirect
github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
Expand Down Expand Up @@ -131,6 +133,7 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
Expand All @@ -139,6 +142,7 @@ require (
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tonistiigi/fsutil v0.0.0-20230105215944-fb433841cbfa // indirect
Expand Down
9 changes: 9 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VM
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/hcsshim v0.9.8 h1:lf7xxK2+Ikbj9sVf2QZsouGjRjEp2STj1yDHgoVtU5k=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/Nerzal/gocloak/v13 v13.7.0 h1:rWZdXtGJarcdTp/XC+cHgAMhLUUYSugm4qnb/qHPyKw=
github.com/Nerzal/gocloak/v13 v13.7.0/go.mod h1:rRBtEdh5N0+JlZZEsrfZcB2sRMZWbgSxI2EIv9jpJp4=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
Expand Down Expand Up @@ -238,6 +240,8 @@ github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nA
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
Expand Down Expand Up @@ -433,6 +437,8 @@ github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 h1:3snG66yBm59tKhhSPQrQ/0bCrv1LQbKt40LnUPiUxdc=
github.com/opencontainers/selinux v1.10.2 h1:NFy2xCsjn7+WspbfZkUd5zyVeisV7VFbPSP96+8/ha4=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/package-url/packageurl-go v0.1.1-0.20220428063043-89078438f170 h1:DiLBVp4DAcZlBVBEtJpNWZpZVq0AEeCY7Hqk8URVs4o=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
Expand Down Expand Up @@ -470,6 +476,8 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/FulN9fTtqYUdS5+Oxzt+DUE=
github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c=
github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE=
github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
Expand Down Expand Up @@ -631,6 +639,7 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
Expand Down
3 changes: 3 additions & 0 deletions pkg/api/hephaestus/v1/imagebuild_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ type ImageBuildSpec struct {
DisableLocalBuildCache bool `json:"disableBuildCache,omitempty"`
// DisableCacheLayerExport will remove the "inline" cache metadata from the image configuration.
DisableCacheLayerExport bool `json:"disableCacheExport,omitempty"`
// EnableServiceAccountTokenInjection adds a service account JWT token as build-arg to the images.
// This supports use cases like model building that must access other Domino services
EnableServiceAccountTokenInjection bool `json:"enableServiceAccountTokenInjection,omitempty"`
}

type ImageBuildTransition struct {
Expand Down
2 changes: 2 additions & 0 deletions pkg/api/hephaestus/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const (
PhaseFailed Phase = "Failed"
)

const ServiceTokenArgName = "SERVICE_TOKEN"

type BasicAuthCredentials struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
Expand Down
24 changes: 24 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Controller struct {
Buildkit Buildkit `json:"buildkit" yaml:"buildkit"`
Messaging Messaging `json:"messaging" yaml:"messaging"`
NewRelic NewRelic `json:"newRelic" yaml:"newRelic"`
Keycloak Keycloak `json:"keycloak" yaml:"keycloak"`

ImageBuildMaxConcurrency int `json:"imageBuildMaxConcurrency" yaml:"imageBuildMaxConcurrency"`
}
Expand Down Expand Up @@ -52,6 +53,21 @@ func (c Controller) Validate() error {
errs = append(errs, "newRelic.licenseKey cannot be blank")
}

if c.Keycloak.Enabled {
if c.Keycloak.ClientID == "" {
errs = append(errs, "keycloak.clientId cannot be blank")
}
if c.Keycloak.ClientSecret == "" {
errs = append(errs, "keycloak.clientSecret cannot be blank")
}
if c.Keycloak.Realm == "" {
errs = append(errs, "keycloak.realm cannot be blank")
}
if c.Keycloak.Server == "" {
errs = append(errs, "keycloak.server cannot be blank")
}
}

if len(errs) != 0 {
return fmt.Errorf("config is invalid: %s", strings.Join(errs, ", "))
}
Expand Down Expand Up @@ -165,6 +181,14 @@ type NewRelic struct {
LicenseKey string `json:"licenseKey" yaml:"licenseKey"`
}

type Keycloak struct {
Enabled bool `json:"enabled" yaml:"enabled"`
Server string `json:"server" yaml:"server"`
ClientID string `json:"clientId" yaml:"clientId"`
ClientSecret string `json:"clientSecret" yaml:"clientSecret"`
Realm string `json:"realm" yaml:"realm"`
dmcwhorter-ddl marked this conversation as resolved.
Show resolved Hide resolved
}

func LoadFromFile(filename string) (Controller, error) {
bs, err := os.ReadFile(filename)
if err != nil {
Expand Down
47 changes: 39 additions & 8 deletions pkg/controller/imagebuild/component/builddispatcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"sync"
"time"

"github.com/Nerzal/gocloak/v13"
"github.com/dominodatalab/controller-util/core"
"github.com/go-logr/logr"
"github.com/newrelic/go-agent/v3/newrelic"
Expand All @@ -22,10 +23,11 @@ import (
)

type BuildDispatcherComponent struct {
cfg config.Buildkit
pool worker.Pool
phase *phase.TransitionHelper
newRelic *newrelic.Application
cfg config.Buildkit
pool worker.Pool
phase *phase.TransitionHelper
newRelic *newrelic.Application
keycloakCfg config.Keycloak

delete <-chan client.ObjectKey
cancels sync.Map
Expand All @@ -35,13 +37,15 @@ func BuildDispatcher(
cfg config.Buildkit,
pool worker.Pool,
nr *newrelic.Application,
kc config.Keycloak,
ch <-chan client.ObjectKey,
) *BuildDispatcherComponent {
return &BuildDispatcherComponent{
cfg: cfg,
pool: pool,
delete: ch,
newRelic: nr,
cfg: cfg,
pool: pool,
delete: ch,
newRelic: nr,
keycloakCfg: kc,
}
}

Expand Down Expand Up @@ -179,6 +183,33 @@ func (c *BuildDispatcherComponent) Reconcile(ctx *core.Context) (ctrl.Result, er
})
return ctrl.Result{}, c.phase.SetFailed(ctx, obj, err)
}

if obj.Spec.EnableServiceAccountTokenInjection {
buildLog.Info("Acquiring Keycloak service account token")
if !c.keycloakCfg.Enabled {
buildLog.Error(err, "Keycloak configuration disabled")
txn.NoticeError(newrelic.Error{
Message: "Keycloak configuration disabled",
Class: "WorkerClientInitError",
})
return ctrl.Result{}, c.phase.SetFailed(ctx, obj, fmt.Errorf("keycloak configuration disabled"))
}

kc := gocloak.NewClient(c.keycloakCfg.Server)
jwt, err := kc.LoginClient(buildCtx, c.keycloakCfg.ClientID, c.keycloakCfg.ClientSecret, c.keycloakCfg.Realm)
ddl-ebrown marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
buildLog.Error(err, fmt.Sprintf(
"Failed to acquire %s Keycloak creds from %s", c.keycloakCfg.ClientID, c.keycloakCfg.Server))
txn.NoticeError(newrelic.Error{
Message: err.Error(),
Class: "WorkerClientInitError",
})
return ctrl.Result{}, c.phase.SetFailed(ctx, obj, fmt.Errorf("keycloak token acquire failed: %w", err))
}

obj.Spec.BuildArgs = append(obj.Spec.BuildArgs, fmt.Sprintf("%s=%s", hephv1.ServiceTokenArgName, jwt.AccessToken))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exposing as a build arg is a bad way to do this as sensitive values may find their way into logs or image metadata

log.Info(fmt.Sprintf("Injected %s token as build-arg %s", c.keycloakCfg.ClientID, hephv1.ServiceTokenArgName))
}
clientInitSeg.End()

buildOpts := buildkit.BuildOptions{
Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/imagebuild/imagebuild.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ var ch = make(chan client.ObjectKey)
func Register(mgr ctrl.Manager, cfg config.Controller, pool worker.Pool, nr *newrelic.Application) error {
return core.NewReconciler(mgr).
For(&hephv1.ImageBuild{}).
Component("build-dispatcher", component.BuildDispatcher(cfg.Buildkit, pool, nr, ch)).
Component("build-dispatcher", component.BuildDispatcher(cfg.Buildkit, pool, nr, cfg.Keycloak, ch)).
WithControllerOptions(controller.Options{MaxConcurrentReconciles: cfg.ImageBuildMaxConcurrency}).
WithWebhooks().
Complete()
Expand Down
13 changes: 13 additions & 0 deletions test/functional/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,19 @@ func (suite *GenericImageBuilderTestSuite) TestImageBuilding() {
assert.Equalf(t, ib.Status.Phase, hephv1.PhaseSucceeded, "failed build with message %q", ib.Status.Conditions[0].Message)
})

suite.T().Run("token_injection", func(t *testing.T) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note - this test doesn't run as part of PRs :(

build := newImageBuild(
buildArgBuildContext,
"docker-registry:5000/test-ns/test-repo",
nil,
)
// TODO: configure the Keycloak client and point it to a fake httptest server
build.Spec.EnableServiceAccountTokenInjection = true
ib := createBuild(t, ctx, suite.hephClient, build)

assert.Equalf(t, ib.Status.Phase, hephv1.PhaseSucceeded, "failed build with message %q", ib.Status.Conditions[0].Message)
})

suite.T().Run("build_failure", func(t *testing.T) {
build := newImageBuild(
errorBuildContext,
Expand Down