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

chore: deploy custom dspa cert #579

Merged
merged 2 commits into from
Mar 5, 2024
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
4 changes: 0 additions & 4 deletions api/v1alpha1/dspipeline_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,6 @@ type APIServer struct {
// provide a PEM formatted CA bundle to be injected into the DSP
// server pod to trust this connection. CA Bundle should be provided
// as values within configmaps, mapped to keys.
//
// Note that if a global cert via ODH or the User is provided in this DSPA's
// namespace with the name "odh-trusted-ca-bundle", then that configmap
// is automatically used instead, and "caBundle" is ignored.
CABundle *CABundle `json:"cABundle,omitempty"`
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,11 @@ spec:
description: 'Default: true'
type: boolean
cABundle:
description: "If the Object store/DB is behind a TLS secured connection
description: If the Object store/DB is behind a TLS secured connection
that is unrecognized by the host OpenShift/K8s cluster, then
you can provide a PEM formatted CA bundle to be injected into
the DSP server pod to trust this connection. CA Bundle should
be provided as values within configmaps, mapped to keys. \n
Note that if a global cert via ODH or the User is provided in
this DSPA's namespace with the name \"odh-trusted-ca-bundle\",
then that configmap is automatically used instead, and \"caBundle\"
is ignored."
be provided as values within configmaps, mapped to keys.
properties:
configMapKey:
description: Key should map to a CA bundle. The key is also
Expand Down
4 changes: 2 additions & 2 deletions config/internal/apiserver/artifact_script.yaml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ data:
artifact_name=$(basename $2)

aws_cp() {
{{ if .APIServer.CABundle }}
aws s3 --endpoint {{.ObjectStorageConnection.Endpoint}} --ca-bundle {{ .PiplinesCABundleMountPath }}/{{ .APIServer.CABundle.ConfigMapKey }} cp $1.tgz s3://{{.ObjectStorageConnection.Bucket}}/artifacts/$PIPELINERUN/$PIPELINETASK/$1.tgz
{{ if .CustomCABundle }}
aws s3 --endpoint {{.ObjectStorageConnection.Endpoint}} --ca-bundle {{ .PiplinesCABundleMountPath }} cp $1.tgz s3://{{.ObjectStorageConnection.Bucket}}/artifacts/$PIPELINERUN/$PIPELINETASK/$1.tgz
{{ else }}
aws s3 --endpoint {{.ObjectStorageConnection.Endpoint}} cp $1.tgz s3://{{.ObjectStorageConnection.Bucket}}/artifacts/$PIPELINERUN/$PIPELINETASK/$1.tgz
{{ end }}
Expand Down
25 changes: 13 additions & 12 deletions config/internal/apiserver/deployment.yaml.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ spec:
value: "{{.APIServer.ArtifactImage}}"
- name: ARCHIVE_LOGS
value: "{{.APIServer.ArchiveLogs}}"
{{ if .APIServer.CABundle }}
{{ if .CustomCABundle }}
- name: ARTIFACT_COPY_STEP_CABUNDLE_CONFIGMAP_NAME
value: "{{.APIServer.CABundle.ConfigMapName}}"
value: "{{.CustomCABundle.ConfigMapName}}"
- name: ARTIFACT_COPY_STEP_CABUNDLE_CONFIGMAP_KEY
value: "{{.APIServer.CABundle.ConfigMapKey}}"
value: "{{.CustomCABundle.ConfigMapKey}}"
- name: ARTIFACT_COPY_STEP_CABUNDLE_MOUNTPATH
value: {{ .PiplinesCABundleMountPath }}
value: {{ .CustomCABundleRootMountPath }}
{{ end }}
- name: TRACK_ARTIFACTS
value: "{{.APIServer.TrackArtifacts}}"
Expand Down Expand Up @@ -102,6 +102,10 @@ spec:
value: "{{.APIServer.CacheImage}}"
- name: MOVERESULTS_IMAGE
value: "{{.APIServer.MoveResultsImage}}"
{{ if .CustomSSLCertDir }}
- name: SSL_CERT_DIR
value: {{.CustomSSLCertDir}}
{{ end }}
image: {{.APIServer.Image}}
imagePullPolicy: Always
name: ds-pipeline-api-server
Expand Down Expand Up @@ -153,7 +157,7 @@ spec:
memory: {{.APIServer.Resources.Limits.Memory}}
{{ end }}
{{ end }}
{{ if or .APIServer.EnableSamplePipeline .APIServer.CABundle }}
{{ if or .APIServer.EnableSamplePipeline .CustomCABundle }}
volumeMounts:
- name: server-config
mountPath: /config/config.json
Expand All @@ -165,8 +169,8 @@ spec:
- name: sample-pipeline
mountPath: /samples/
{{ end }}
{{ if .APIServer.CABundle }}
- mountPath: {{ .APIServerPiplinesCABundleMountPath }}
{{ if .CustomCABundle }}
- mountPath: {{ .CustomCABundleRootMountPath }}
name: ca-bundle
{{ end }}
{{ end }}
Expand Down Expand Up @@ -226,13 +230,10 @@ spec:
- name: server-config
configMap:
name: pipeline-server-config-{{.Name}}
{{ if .APIServer.CABundle }}
{{ if .CustomCABundle }}
- name: ca-bundle
configMap:
name: {{ .APIServer.CABundle.ConfigMapName }}
items:
- key: {{ .APIServer.CABundle.ConfigMapKey }}
path: {{ .APIServer.CABundle.ConfigMapKey }}
name: {{ .CustomCABundle.ConfigMapName }}
{{ end }}
{{ if .APIServer.EnableSamplePipeline }}
- name: sample-config
Expand Down
25 changes: 17 additions & 8 deletions controllers/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,24 @@ limitations under the License.
package config

import (
"fmt"
dspav1alpha1 "github.com/opendatahub-io/data-science-pipelines-operator/api/v1alpha1"
"github.com/spf13/viper"
"k8s.io/apimachinery/pkg/api/resource"
"time"
)

const (
DefaultImageValue = "MustSetInConfig"
APIServerPiplinesCABundleMountPath = "/etc/pki/tls/certs"
PiplinesCABundleMountPath = "/etc/pki/tls/certs"
DefaultImageValue = "MustSetInConfig"

// GlobalCaBundleConfigMapName key and label values are a contract with
CustomCABundleRootMountPath = "/dsp-custom-certs"

// GlobalODHCaBundleConfigMapName key and label values are a contract with
// ODH Platform https://github.com/opendatahub-io/architecture-decision-records/pull/28
GlobalCaBundleConfigMapName = "odh-trusted-ca-bundle"
GlobalODHCaBundleConfigMapName = "odh-trusted-ca-bundle"

// GlobalCaBundleConfigMapKey is the key provided by the configmap created via OCP Cluster Network Operator
// https://docs.openshift.com/container-platform/4.14/networking/configuring-a-custom-pki.html#certificate-injection-using-operators_configuring-a-custom-pki
GlobalCaBundleConfigMapKey = "ca-bundle.crt"
CustomDSPTrustedCAConfigMapNamePrefix = "dsp-trusted-ca"
CustomDSPTrustedCAConfigMapKey = "dsp-ca.crt"

MLPipelineUIConfigMapPrefix = "ds-pipeline-ui-configmap-"
ArtifactScriptConfigMapNamePrefix = "ds-pipeline-artifact-script-"
Expand Down Expand Up @@ -169,3 +169,12 @@ func GetDurationConfigWithDefault(configName string, value time.Duration) time.D
}
return viper.GetDuration(configName)
}

// GetCABundleFileMountPath provides the location in pipeline step-copy-artifact step where the
// ca bundle is mounted for aws cli to connect to s3 store.
// Since pipeline step-copy-artifact step uses aws cli, and there are issues surrounding
// passing a path to aws cli (see: https://github.com/aws/aws-cli/issues/3425#issuecomment-402289636)
// as such for pipelines, we concatenate the certs into a single cert bundle and use a separate configmap for this
func GetCABundleFileMountPath() string {
return fmt.Sprintf("%s/%s", CustomCABundleRootMountPath, CustomDSPTrustedCAConfigMapKey)
}
10 changes: 6 additions & 4 deletions controllers/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ var dbTemplates = []string{
dbSecret,
}

func tLSClientConfig(pem []byte) (*cryptoTls.Config, error) {
func tLSClientConfig(pems [][]byte) (*cryptoTls.Config, error) {
rootCertPool := x509.NewCertPool()

if f := os.Getenv("SSL_CERT_FILE"); f != "" {
Expand All @@ -51,8 +51,10 @@ func tLSClientConfig(pem []byte) (*cryptoTls.Config, error) {
}
}

if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
return nil, fmt.Errorf("error parsing CA Certificate, ensure provided certs are in valid PEM format")
for _, pem := range pems {
if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
return nil, fmt.Errorf("error parsing CA Certificate, ensure provided certs are in valid PEM format")
}
}
tlsConfig := &cryptoTls.Config{
RootCAs: rootCertPool,
Expand Down Expand Up @@ -88,7 +90,7 @@ var ConnectAndQueryDatabase = func(
host string,
log logr.Logger,
port, username, password, dbname, tls string,
pemCerts []byte,
pemCerts [][]byte,
extraParams map[string]string) (bool, error) {

mysqlConfig := createMySQLConfig(
Expand Down
140 changes: 107 additions & 33 deletions controllers/dspipeline_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ limitations under the License.
package controllers

import (
"bytes"
"context"
"encoding/base64"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/json"
"math/rand"
"strings"
"time"

"github.com/go-logr/logr"
Expand All @@ -41,11 +44,8 @@ type DSPAParams struct {
Namespace string
Owner mf.Owner
APIServer *dspa.APIServer
APIServerPiplinesCABundleMountPath string
PiplinesCABundleMountPath string
APIServerDefaultResourceName string
APIServerServiceName string
APICustomPemCerts []byte
OAuthProxy string
ScheduledWorkflow *dspa.ScheduledWorkflow
ScheduledWorkflowDefaultResourceName string
Expand All @@ -57,6 +57,23 @@ type DSPAParams struct {
MLMD *dspa.MLMD
DBConnection
ObjectStorageConnection

// TLS
// The CA bundle path used by API server
CustomCABundleRootMountPath string
// This path is used by API server to also look
// for CustomCABundleRootMountPath when
// verifying certs
CustomSSLCertDir *string
// The CA bundle path found in the pipeline pods
PiplinesCABundleMountPath string
// Collects all certs from user & global certs
APICustomPemCerts [][]byte
// Source of truth for the DSP cert configmap details
// If this is defined, then we assume we have additional certs
// we need to leverage for tls connections within dsp apiserver
// pipeline pods
CustomCABundle *dspa.CABundle
}

type DBConnection struct {
Expand Down Expand Up @@ -463,8 +480,8 @@ func (p *DSPAParams) ExtractParams(ctx context.Context, dsp *dspa.DataSciencePip
p.Minio = dsp.Spec.ObjectStorage.Minio.DeepCopy()
p.OAuthProxy = config.GetStringConfigWithDefault(config.OAuthProxyImagePath, config.DefaultImageValue)
p.MLMD = dsp.Spec.MLMD.DeepCopy()
p.APIServerPiplinesCABundleMountPath = config.APIServerPiplinesCABundleMountPath
p.PiplinesCABundleMountPath = config.PiplinesCABundleMountPath
p.CustomCABundleRootMountPath = config.CustomCABundleRootMountPath
p.PiplinesCABundleMountPath = config.GetCABundleFileMountPath()

log := loggr.WithValues("namespace", p.Namespace).WithValues("dspa_name", p.Name)

Expand All @@ -479,7 +496,6 @@ func (p *DSPAParams) ExtractParams(ctx context.Context, dsp *dspa.DataSciencePip
setStringDefault(artifactImageFromConfig, &p.APIServer.ArtifactImage)
setStringDefault(cacheImageFromConfig, &p.APIServer.CacheImage)
setStringDefault(moveResultsImageFromConfig, &p.APIServer.MoveResultsImage)

setResourcesDefault(config.APIServerResourceRequirements, &p.APIServer.Resources)

if p.APIServer.ArtifactScriptConfigMap == nil {
Expand All @@ -490,38 +506,96 @@ func (p *DSPAParams) ExtractParams(ctx context.Context, dsp *dspa.DataSciencePip
}

// Check for Global certs
// If it exists, we use this cert
// If no global cert provided, check if a custom bundle is provided via the DSPA
globalCABundleCFGMapKey, globalCABundleCFGMapName := config.GlobalCaBundleConfigMapKey, config.GlobalCaBundleConfigMapName
err, globalCerVal := util.GetConfigMapValue(ctx, globalCABundleCFGMapKey, globalCABundleCFGMapName, p.Namespace, client, log)
if err != nil && apierrs.IsNotFound(err) {
// If it exists, include this cert for tls verifications
globalCABundleCFGMapName := config.GlobalODHCaBundleConfigMapName
err, globalCerts := util.GetConfigMapValues(ctx, globalCABundleCFGMapName, p.Namespace, client)
if err != nil {
// If the global cert configmap is not available, that is OK
// proceed to check if the user has provided their
// own configmap via DSPA config
if p.APIServer.CABundle != nil {
dspaCaBundleCfgKey, dspaCaBundleCfgName := p.APIServer.CABundle.ConfigMapKey, p.APIServer.CABundle.ConfigMapName
dspaCACfgErr, dspaProvidedCABundle := util.GetConfigMapValue(ctx, dspaCaBundleCfgKey, dspaCaBundleCfgName, p.Namespace, client, log)
if dspaCACfgErr != nil && apierrs.IsNotFound(dspaCACfgErr) {
log.Info(fmt.Sprintf("ConfigMap [%s] was not found in namespace [%s]", dspaCaBundleCfgKey, p.Namespace))
return dspaCACfgErr
} else if dspaCACfgErr != nil {
log.Info(fmt.Sprintf("Encountered error when attempting to fetch ConfigMap: [%s], Error: %v", dspaCaBundleCfgName, dspaCACfgErr))
return dspaCACfgErr
}
p.APICustomPemCerts = []byte(dspaProvidedCABundle)
if !apierrs.IsNotFound(err) {
log.Info(fmt.Sprintf("Encountered error when attempting to fetch ConfigMap: [%s], Error: %v", globalCABundleCFGMapName, err))
return err
}
} else if err != nil {
log.Info(fmt.Sprintf("Encountered error when attempting to fetch ConfigMap: [%s], Error: %v", globalCABundleCFGMapKey, err))
return err
} else {
// Found a global cert, consume this cert, takes precedence over "cABundle" provided via DSPA
log.Info(fmt.Sprintf("Found global CA Bundle %s present in this namespace %s, this cert will be "+
"included to verify external tls connections in this DSPA.", config.GlobalCaBundleConfigMapName, p.Namespace))
p.APICustomPemCerts = []byte(globalCerVal)
p.APIServer.CABundle = &dspa.CABundle{
ConfigMapName: config.GlobalCaBundleConfigMapName,
ConfigMapKey: config.GlobalCaBundleConfigMapKey,
log.Info(fmt.Sprintf("Found global CA Bundle %s present in this namespace %s, this bundle will be included in external tls connections.", config.GlobalODHCaBundleConfigMapName, p.Namespace))
// "odh-trusted-ca-bundle" can have fields: "odh-ca-bundle.crt" and "ca-bundle.crt", we need to utilize both
for _, val := range globalCerts {
p.APICustomPemCerts = append(p.APICustomPemCerts, []byte(val))
}
}

// If user provided a CA bundle, include this in tls verification
if p.APIServer.CABundle != nil {
dspaCaBundleCfgKey, dspaCaBundleCfgName := p.APIServer.CABundle.ConfigMapKey, p.APIServer.CABundle.ConfigMapName
dspaCACfgErr, dspaProvidedCABundle := util.GetConfigMapValue(ctx, dspaCaBundleCfgKey, dspaCaBundleCfgName, p.Namespace, client, log)
if dspaCACfgErr != nil && apierrs.IsNotFound(dspaCACfgErr) {
log.Info(fmt.Sprintf("ConfigMap [%s] was not found in namespace [%s]", dspaCaBundleCfgKey, p.Namespace))
return dspaCACfgErr
} else if dspaCACfgErr != nil {
log.Info(fmt.Sprintf("Encountered error when attempting to fetch ConfigMap: [%s], Error: %v", dspaCaBundleCfgName, dspaCACfgErr))
return dspaCACfgErr
}
p.APICustomPemCerts = append(p.APICustomPemCerts, []byte(dspaProvidedCABundle))
}

// There are situations where global & user provided certs, or a provided ca trust configmap(s) have various trust bundles
// (for example in the case of "odh-trusted-ca-bundle") there is "odh-ca-bundle.crt" and "ca-bundle.crt".
// We create a separate configmap and concatenate all the certs into a single bundle, because passing a
// full path into the pipeline doesn't seem to work with aws cli used for artifact passing
// Ref: https://github.com/aws/aws-cli/issues/3425#issuecomment-402289636

// If user or global CABundle has been provided
// 1) create the dsp-trusted-ca configmap
// 2) populate CustomCABundle SOT var for pipeline pods and artifact script to utilize during templating
// 3) set ssl_cert_dir for api server
if len(p.APICustomPemCerts) > 0 {
p.CustomCABundle = &dspa.CABundle{
ConfigMapKey: config.CustomDSPTrustedCAConfigMapKey,
ConfigMapName: fmt.Sprintf("%s-%s", config.CustomDSPTrustedCAConfigMapNamePrefix, p.Name),
}

// Combine certs into a single configmap field
customCABundleCert := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: p.CustomCABundle.ConfigMapName,
Namespace: p.Namespace,
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: dsp.APIVersion,
Kind: dsp.Kind,
Name: dsp.Name,
UID: dsp.UID,
Controller: util.BoolPointer(true),
BlockOwnerDeletion: util.BoolPointer(true),
},
},
},

Data: map[string]string{
p.CustomCABundle.ConfigMapKey: string(bytes.Join(p.APICustomPemCerts, []byte{})),
},
}

err := client.Create(ctx, customCABundleCert)
if apierrs.IsAlreadyExists(err) {
err := client.Update(ctx, customCABundleCert)
if err != nil {
return err
}
} else if err != nil {
return err
}

// We need to update the default SSL_CERT_DIR to include
// dsp custom cert path, used by DSP Api Server
var certDirectories = []string{
config.CustomCABundleRootMountPath,
"/etc/ssl/certs", // SLES10/SLES11, https://golang.org/issue/12139
"/etc/pki/tls/certs", // Fedora/RHEL
}
// SSL_CERT_DIR accepts a colon separated list of directories
sslCertDir := strings.Join(certDirectories, ":")
p.CustomSSLCertDir = &sslCertDir
}
}

Expand Down
Loading
Loading