diff --git a/.gitignore b/.gitignore index acea8a9..62f3ac2 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ go.work.sum # Editor/IDE # .idea/ # .vscode/ + +# Claude Code cache +.claude/tsc-cache/ diff --git a/api/core/v1beta1/package_manager_config.go b/api/core/v1beta1/package_manager_config.go index b6dabf5..cd2e39c 100644 --- a/api/core/v1beta1/package_manager_config.go +++ b/api/core/v1beta1/package_manager_config.go @@ -7,17 +7,19 @@ import ( ) type PackageManagerConfig struct { - Server *PackageManagerServerConfig `json:"Server,omitempty"` - Http *PackageManagerHttpConfig `json:"Http,omitempty"` - Git *PackageManagerGitConfig `json:"Git,omitempty"` - Database *PackageManagerDatabaseConfig `json:"Database,omitempty"` - Postgres *PackageManagerPostgresConfig `json:"Postgres,omitempty"` - Storage *PackageManagerStorageConfig `json:"Storage,omitempty"` - S3Storage *PackageManagerS3StorageConfig `json:"S3Storage,omitempty"` - Metrics *PackageManagerMetricsConfig `json:"Metrics,omitempty"` - Repos *PackageManagerReposConfig `json:"Repos,omitempty"` - Cran *PackageManagerCRANConfig `json:"CRAN,omitempty"` - Debug *PackageManagerDebugConfig `json:"Debug,omitempty"` + Server *PackageManagerServerConfig `json:"Server,omitempty"` + Http *PackageManagerHttpConfig `json:"Http,omitempty"` + Git *PackageManagerGitConfig `json:"Git,omitempty"` + Database *PackageManagerDatabaseConfig `json:"Database,omitempty"` + Postgres *PackageManagerPostgresConfig `json:"Postgres,omitempty"` + Storage *PackageManagerStorageConfig `json:"Storage,omitempty"` + S3Storage *PackageManagerS3StorageConfig `json:"S3Storage,omitempty"` + Metrics *PackageManagerMetricsConfig `json:"Metrics,omitempty"` + Repos *PackageManagerReposConfig `json:"Repos,omitempty"` + Cran *PackageManagerCRANConfig `json:"CRAN,omitempty"` + Debug *PackageManagerDebugConfig `json:"Debug,omitempty"` + Authentication *PackageManagerAuthenticationConfig `json:"Authentication,omitempty"` + OpenIDConnect *PackageManagerOIDCConfig `json:"OpenIDConnect,omitempty"` // AdditionalConfig allows appending arbitrary gcfg config content not covered by typed fields. // The value is appended verbatim after the generated config. gcfg parsing naturally handles @@ -176,6 +178,37 @@ type PackageManagerDebugConfig struct { Log string `json:"Log,omitempty"` } +type PackageManagerAuthenticationConfig struct { + APITokenAuth bool `json:"APITokenAuth,omitempty"` + DeviceAuthType string `json:"DeviceAuthType,omitempty"` + NewReposAuthByDefault bool `json:"NewReposAuthByDefault,omitempty"` + Lifetime string `json:"Lifetime,omitempty"` + Inactivity string `json:"Inactivity,omitempty"` + CookieSweepDuration string `json:"CookieSweepDuration,omitempty"` +} + +type PackageManagerOIDCConfig struct { + ClientId string `json:"ClientId,omitempty"` + ClientSecret string `json:"ClientSecret,omitempty"` + ClientSecretFile string `json:"ClientSecretFile,omitempty"` + Issuer string `json:"Issuer,omitempty"` + RequireLogin bool `json:"RequireLogin,omitempty"` + Logging bool `json:"Logging,omitempty"` + Scope string `json:"Scope,omitempty"` + CustomScope string `json:"CustomScope,omitempty"` + NoAutoGroupsScope bool `json:"NoAutoGroupsScope,omitempty"` + GroupsClaim string `json:"GroupsClaim,omitempty"` + GroupsSeparator string `json:"GroupsSeparator,omitempty"` + RoleClaim string `json:"RoleClaim,omitempty"` + RolesSeparator string `json:"RolesSeparator,omitempty"` + UniqueIdClaim string `json:"UniqueIdClaim,omitempty"` + UsernameClaim string `json:"UsernameClaim,omitempty"` + TokenLifetime string `json:"TokenLifetime,omitempty"` + MaxAuthenticationAge string `json:"MaxAuthenticationAge,omitempty"` + DisablePKCE bool `json:"DisablePKCE,omitempty"` + EnableDevicePKCE bool `json:"EnableDevicePKCE,omitempty"` +} + // SSHKeyConfig defines SSH key configuration for Git authentication type SSHKeyConfig struct { // Name is a unique identifier for this SSH key configuration diff --git a/api/core/v1beta1/package_manager_config_test.go b/api/core/v1beta1/package_manager_config_test.go index ddf37a9..5970d21 100644 --- a/api/core/v1beta1/package_manager_config_test.go +++ b/api/core/v1beta1/package_manager_config_test.go @@ -62,3 +62,81 @@ func TestPackageManagerConfig_AdditionalConfigEmpty(t *testing.T) { require.Nil(t, err) require.Contains(t, str, "Address = some-address.com") } + +func TestPackageManagerConfig_OpenIDConnect(t *testing.T) { + cfg := PackageManagerConfig{ + OpenIDConnect: &PackageManagerOIDCConfig{ + ClientId: "my-client-id", + ClientSecret: "/etc/rstudio-pm/oidc-client-secret", + Issuer: "https://login.example.com", + RequireLogin: true, + }, + } + str, err := cfg.GenerateGcfg() + require.Nil(t, err) + require.Contains(t, str, "[OpenIDConnect]") + require.Contains(t, str, "ClientId = my-client-id") + require.Contains(t, str, "ClientSecret = /etc/rstudio-pm/oidc-client-secret") + require.Contains(t, str, "Issuer = https://login.example.com") + require.Contains(t, str, "RequireLogin = true") +} + +func TestPackageManagerConfig_Authentication(t *testing.T) { + cfg := PackageManagerConfig{ + Authentication: &PackageManagerAuthenticationConfig{ + APITokenAuth: true, + DeviceAuthType: "oidc", + NewReposAuthByDefault: true, + Lifetime: "30d", + Inactivity: "12h", + CookieSweepDuration: "5m", + }, + } + str, err := cfg.GenerateGcfg() + require.Nil(t, err) + require.Contains(t, str, "[Authentication]") + require.Contains(t, str, "APITokenAuth = true") + require.Contains(t, str, "DeviceAuthType = oidc") + require.Contains(t, str, "NewReposAuthByDefault = true") + require.Contains(t, str, "Lifetime = 30d") + require.Contains(t, str, "Inactivity = 12h") + require.Contains(t, str, "CookieSweepDuration = 5m") +} + +func TestPackageManagerConfig_OIDCNewFields(t *testing.T) { + cfg := PackageManagerConfig{ + OpenIDConnect: &PackageManagerOIDCConfig{ + ClientId: "my-client", + ClientSecretFile: "/etc/rstudio-pm/oidc-secret", + Issuer: "https://auth.example.com", + Logging: true, + TokenLifetime: "1h", + DisablePKCE: true, + UniqueIdClaim: "sub", + UsernameClaim: "preferred_username", + MaxAuthenticationAge: "24h", + GroupsSeparator: ",", + RolesSeparator: ";", + CustomScope: "profile email groups", + NoAutoGroupsScope: true, + EnableDevicePKCE: true, + }, + } + str, err := cfg.GenerateGcfg() + require.Nil(t, err) + require.Contains(t, str, "[OpenIDConnect]") + require.Contains(t, str, "ClientId = my-client") + require.Contains(t, str, "ClientSecretFile = /etc/rstudio-pm/oidc-secret") + require.Contains(t, str, "Issuer = https://auth.example.com") + require.Contains(t, str, "Logging = true") + require.Contains(t, str, "TokenLifetime = 1h") + require.Contains(t, str, "DisablePKCE = true") + require.Contains(t, str, "UniqueIdClaim = sub") + require.Contains(t, str, "UsernameClaim = preferred_username") + require.Contains(t, str, "MaxAuthenticationAge = 24h") + require.Contains(t, str, "GroupsSeparator = ,") + require.Contains(t, str, "RolesSeparator = ;") + require.Contains(t, str, "CustomScope = profile email groups") + require.Contains(t, str, "NoAutoGroupsScope = true") + require.Contains(t, str, "EnableDevicePKCE = true") +} diff --git a/api/core/v1beta1/packagemanager_types.go b/api/core/v1beta1/packagemanager_types.go index 0163572..9ea5a9f 100644 --- a/api/core/v1beta1/packagemanager_types.go +++ b/api/core/v1beta1/packagemanager_types.go @@ -79,6 +79,11 @@ type PackageManagerSpec struct { // AzureFiles configures Azure Files integration for persistent storage // +optional AzureFiles *AzureFilesConfig `json:"azureFiles,omitempty"` + + // OIDCClientSecretKey is the key name in the vault for the OIDC client secret. + // When set, the client secret will be mounted at /etc/rstudio-pm/oidc-client-secret + // +optional + OIDCClientSecretKey string `json:"oidcClientSecretKey,omitempty"` } // PackageManagerStatus defines the observed state of PackageManager @@ -313,6 +318,24 @@ func (pm *PackageManager) CreateSecretVolumeFactory() *product.SecretVolumeFacto } } + // Add OIDC client secret volume mount if configured + if pm.Spec.OIDCClientSecretKey != "" { + vols["client-secret-volume"] = &product.VolumeDef{ + Source: &v1.VolumeSource{ + CSI: &v1.CSIVolumeSource{ + Driver: "secrets-store.csi.k8s.io", + ReadOnly: ptr.To(true), + VolumeAttributes: map[string]string{ + "secretProviderClass": pm.SecretProviderClassName(), + }, + }, + }, + Mounts: []*product.VolumeMountDef{ + {MountPath: "/etc/rstudio-pm/oidc-client-secret", SubPath: "oidc-client-secret", ReadOnly: true}, + }, + } + } + case product.SiteSecretKubernetes: vols["key-volume"] = &product.VolumeDef{ Env: []v1.EnvVar{ @@ -350,6 +373,23 @@ func (pm *PackageManager) CreateSecretVolumeFactory() *product.SecretVolumeFacto }, } + // Add OIDC client secret volume mount if configured + if pm.Spec.OIDCClientSecretKey != "" { + vols["client-secret-volume"] = &product.VolumeDef{ + Source: &v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: pm.GetSecretVaultName(), + Items: []v1.KeyToPath{ + {Key: pm.Spec.OIDCClientSecretKey, Path: "oidc-client-secret"}, + }, + }, + }, + Mounts: []*product.VolumeMountDef{ + {MountPath: "/etc/rstudio-pm/oidc-client-secret", SubPath: "oidc-client-secret", ReadOnly: true}, + }, + } + } + default: // uh oh... some other type of secret...? } diff --git a/api/core/v1beta1/site_types.go b/api/core/v1beta1/site_types.go index 44bb426..09548fe 100644 --- a/api/core/v1beta1/site_types.go +++ b/api/core/v1beta1/site_types.go @@ -225,6 +225,14 @@ type InternalPackageManagerSpec struct { // AdditionalConfig allows appending arbitrary gcfg config content to the generated config. // +optional AdditionalConfig string `json:"additionalConfig,omitempty"` + + // Auth configures OIDC authentication for Package Manager's web UI + // +optional + Auth *AuthSpec `json:"auth,omitempty"` + + // OIDCClientSecretKey is the key in the vault for the OIDC client secret + // +optional + OIDCClientSecretKey string `json:"oidcClientSecretKey,omitempty"` } type InternalConnectSpec struct { diff --git a/api/core/v1beta1/zz_generated.deepcopy.go b/api/core/v1beta1/zz_generated.deepcopy.go index dc0775b..b437300 100644 --- a/api/core/v1beta1/zz_generated.deepcopy.go +++ b/api/core/v1beta1/zz_generated.deepcopy.go @@ -1302,6 +1302,11 @@ func (in *InternalPackageManagerSpec) DeepCopyInto(out *InternalPackageManagerSp *out = new(AzureFilesConfig) **out = **in } + if in.Auth != nil { + in, out := &in.Auth, &out.Auth + *out = new(AuthSpec) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InternalPackageManagerSpec. @@ -1542,6 +1547,21 @@ func (in *PackageManager) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackageManagerAuthenticationConfig) DeepCopyInto(out *PackageManagerAuthenticationConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageManagerAuthenticationConfig. +func (in *PackageManagerAuthenticationConfig) DeepCopy() *PackageManagerAuthenticationConfig { + if in == nil { + return nil + } + out := new(PackageManagerAuthenticationConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PackageManagerCRANConfig) DeepCopyInto(out *PackageManagerCRANConfig) { *out = *in @@ -1615,6 +1635,16 @@ func (in *PackageManagerConfig) DeepCopyInto(out *PackageManagerConfig) { *out = new(PackageManagerDebugConfig) **out = **in } + if in.Authentication != nil { + in, out := &in.Authentication, &out.Authentication + *out = new(PackageManagerAuthenticationConfig) + **out = **in + } + if in.OpenIDConnect != nil { + in, out := &in.OpenIDConnect, &out.OpenIDConnect + *out = new(PackageManagerOIDCConfig) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageManagerConfig. @@ -1734,6 +1764,21 @@ func (in *PackageManagerMetricsConfig) DeepCopy() *PackageManagerMetricsConfig { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PackageManagerOIDCConfig) DeepCopyInto(out *PackageManagerOIDCConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PackageManagerOIDCConfig. +func (in *PackageManagerOIDCConfig) DeepCopy() *PackageManagerOIDCConfig { + if in == nil { + return nil + } + out := new(PackageManagerOIDCConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PackageManagerPostgresConfig) DeepCopyInto(out *PackageManagerPostgresConfig) { *out = *in diff --git a/client-go/applyconfiguration/core/v1beta1/internalpackagemanagerspec.go b/client-go/applyconfiguration/core/v1beta1/internalpackagemanagerspec.go index c9e1297..16a5df9 100644 --- a/client-go/applyconfiguration/core/v1beta1/internalpackagemanagerspec.go +++ b/client-go/applyconfiguration/core/v1beta1/internalpackagemanagerspec.go @@ -13,19 +13,21 @@ import ( // InternalPackageManagerSpecApplyConfiguration represents a declarative configuration of the InternalPackageManagerSpec type for use // with apply. type InternalPackageManagerSpecApplyConfiguration struct { - License *product.LicenseSpec `json:"license,omitempty"` - Volume *product.VolumeSpec `json:"volume,omitempty"` - NodeSelector map[string]string `json:"nodeSelector,omitempty"` - AddEnv map[string]string `json:"addEnv,omitempty"` - Image *string `json:"image,omitempty"` - ImagePullPolicy *v1.PullPolicy `json:"imagePullPolicy,omitempty"` - S3Bucket *string `json:"s3Bucket,omitempty"` - Replicas *int `json:"replicas,omitempty"` - DomainPrefix *string `json:"domainPrefix,omitempty"` - BaseDomain *string `json:"baseDomain,omitempty"` - GitSSHKeys []SSHKeyConfigApplyConfiguration `json:"gitSSHKeys,omitempty"` - AzureFiles *AzureFilesConfigApplyConfiguration `json:"azureFiles,omitempty"` - AdditionalConfig *string `json:"additionalConfig,omitempty"` + License *product.LicenseSpec `json:"license,omitempty"` + Volume *product.VolumeSpec `json:"volume,omitempty"` + NodeSelector map[string]string `json:"nodeSelector,omitempty"` + AddEnv map[string]string `json:"addEnv,omitempty"` + Image *string `json:"image,omitempty"` + ImagePullPolicy *v1.PullPolicy `json:"imagePullPolicy,omitempty"` + S3Bucket *string `json:"s3Bucket,omitempty"` + Replicas *int `json:"replicas,omitempty"` + DomainPrefix *string `json:"domainPrefix,omitempty"` + BaseDomain *string `json:"baseDomain,omitempty"` + GitSSHKeys []SSHKeyConfigApplyConfiguration `json:"gitSSHKeys,omitempty"` + AzureFiles *AzureFilesConfigApplyConfiguration `json:"azureFiles,omitempty"` + AdditionalConfig *string `json:"additionalConfig,omitempty"` + Auth *AuthSpecApplyConfiguration `json:"auth,omitempty"` + OIDCClientSecretKey *string `json:"oidcClientSecretKey,omitempty"` } // InternalPackageManagerSpecApplyConfiguration constructs a declarative configuration of the InternalPackageManagerSpec type for use with @@ -154,3 +156,19 @@ func (b *InternalPackageManagerSpecApplyConfiguration) WithAdditionalConfig(valu b.AdditionalConfig = &value return b } + +// WithAuth sets the Auth field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Auth field is set to the value of the last call. +func (b *InternalPackageManagerSpecApplyConfiguration) WithAuth(value *AuthSpecApplyConfiguration) *InternalPackageManagerSpecApplyConfiguration { + b.Auth = value + return b +} + +// WithOIDCClientSecretKey sets the OIDCClientSecretKey field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the OIDCClientSecretKey field is set to the value of the last call. +func (b *InternalPackageManagerSpecApplyConfiguration) WithOIDCClientSecretKey(value string) *InternalPackageManagerSpecApplyConfiguration { + b.OIDCClientSecretKey = &value + return b +} diff --git a/client-go/applyconfiguration/core/v1beta1/packagemanagerauthenticationconfig.go b/client-go/applyconfiguration/core/v1beta1/packagemanagerauthenticationconfig.go new file mode 100644 index 0000000..24fca6f --- /dev/null +++ b/client-go/applyconfiguration/core/v1beta1/packagemanagerauthenticationconfig.go @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2023-2026 Posit Software, PBC + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1beta1 + +// PackageManagerAuthenticationConfigApplyConfiguration represents a declarative configuration of the PackageManagerAuthenticationConfig type for use +// with apply. +type PackageManagerAuthenticationConfigApplyConfiguration struct { + APITokenAuth *bool `json:"APITokenAuth,omitempty"` + DeviceAuthType *string `json:"DeviceAuthType,omitempty"` + NewReposAuthByDefault *bool `json:"NewReposAuthByDefault,omitempty"` + Lifetime *string `json:"Lifetime,omitempty"` + Inactivity *string `json:"Inactivity,omitempty"` + CookieSweepDuration *string `json:"CookieSweepDuration,omitempty"` +} + +// PackageManagerAuthenticationConfigApplyConfiguration constructs a declarative configuration of the PackageManagerAuthenticationConfig type for use with +// apply. +func PackageManagerAuthenticationConfig() *PackageManagerAuthenticationConfigApplyConfiguration { + return &PackageManagerAuthenticationConfigApplyConfiguration{} +} + +// WithAPITokenAuth sets the APITokenAuth field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APITokenAuth field is set to the value of the last call. +func (b *PackageManagerAuthenticationConfigApplyConfiguration) WithAPITokenAuth(value bool) *PackageManagerAuthenticationConfigApplyConfiguration { + b.APITokenAuth = &value + return b +} + +// WithDeviceAuthType sets the DeviceAuthType field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeviceAuthType field is set to the value of the last call. +func (b *PackageManagerAuthenticationConfigApplyConfiguration) WithDeviceAuthType(value string) *PackageManagerAuthenticationConfigApplyConfiguration { + b.DeviceAuthType = &value + return b +} + +// WithNewReposAuthByDefault sets the NewReposAuthByDefault field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the NewReposAuthByDefault field is set to the value of the last call. +func (b *PackageManagerAuthenticationConfigApplyConfiguration) WithNewReposAuthByDefault(value bool) *PackageManagerAuthenticationConfigApplyConfiguration { + b.NewReposAuthByDefault = &value + return b +} + +// WithLifetime sets the Lifetime field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Lifetime field is set to the value of the last call. +func (b *PackageManagerAuthenticationConfigApplyConfiguration) WithLifetime(value string) *PackageManagerAuthenticationConfigApplyConfiguration { + b.Lifetime = &value + return b +} + +// WithInactivity sets the Inactivity field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Inactivity field is set to the value of the last call. +func (b *PackageManagerAuthenticationConfigApplyConfiguration) WithInactivity(value string) *PackageManagerAuthenticationConfigApplyConfiguration { + b.Inactivity = &value + return b +} + +// WithCookieSweepDuration sets the CookieSweepDuration field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CookieSweepDuration field is set to the value of the last call. +func (b *PackageManagerAuthenticationConfigApplyConfiguration) WithCookieSweepDuration(value string) *PackageManagerAuthenticationConfigApplyConfiguration { + b.CookieSweepDuration = &value + return b +} diff --git a/client-go/applyconfiguration/core/v1beta1/packagemanagerconfig.go b/client-go/applyconfiguration/core/v1beta1/packagemanagerconfig.go index c853572..545182e 100644 --- a/client-go/applyconfiguration/core/v1beta1/packagemanagerconfig.go +++ b/client-go/applyconfiguration/core/v1beta1/packagemanagerconfig.go @@ -8,18 +8,20 @@ package v1beta1 // PackageManagerConfigApplyConfiguration represents a declarative configuration of the PackageManagerConfig type for use // with apply. type PackageManagerConfigApplyConfiguration struct { - Server *PackageManagerServerConfigApplyConfiguration `json:"Server,omitempty"` - Http *PackageManagerHttpConfigApplyConfiguration `json:"Http,omitempty"` - Git *PackageManagerGitConfigApplyConfiguration `json:"Git,omitempty"` - Database *PackageManagerDatabaseConfigApplyConfiguration `json:"Database,omitempty"` - Postgres *PackageManagerPostgresConfigApplyConfiguration `json:"Postgres,omitempty"` - Storage *PackageManagerStorageConfigApplyConfiguration `json:"Storage,omitempty"` - S3Storage *PackageManagerS3StorageConfigApplyConfiguration `json:"S3Storage,omitempty"` - Metrics *PackageManagerMetricsConfigApplyConfiguration `json:"Metrics,omitempty"` - Repos *PackageManagerReposConfigApplyConfiguration `json:"Repos,omitempty"` - Cran *PackageManagerCRANConfigApplyConfiguration `json:"CRAN,omitempty"` - Debug *PackageManagerDebugConfigApplyConfiguration `json:"Debug,omitempty"` - AdditionalConfig *string `json:"additionalConfig,omitempty"` + Server *PackageManagerServerConfigApplyConfiguration `json:"Server,omitempty"` + Http *PackageManagerHttpConfigApplyConfiguration `json:"Http,omitempty"` + Git *PackageManagerGitConfigApplyConfiguration `json:"Git,omitempty"` + Database *PackageManagerDatabaseConfigApplyConfiguration `json:"Database,omitempty"` + Postgres *PackageManagerPostgresConfigApplyConfiguration `json:"Postgres,omitempty"` + Storage *PackageManagerStorageConfigApplyConfiguration `json:"Storage,omitempty"` + S3Storage *PackageManagerS3StorageConfigApplyConfiguration `json:"S3Storage,omitempty"` + Metrics *PackageManagerMetricsConfigApplyConfiguration `json:"Metrics,omitempty"` + Repos *PackageManagerReposConfigApplyConfiguration `json:"Repos,omitempty"` + Cran *PackageManagerCRANConfigApplyConfiguration `json:"CRAN,omitempty"` + Debug *PackageManagerDebugConfigApplyConfiguration `json:"Debug,omitempty"` + Authentication *PackageManagerAuthenticationConfigApplyConfiguration `json:"Authentication,omitempty"` + OpenIDConnect *PackageManagerOIDCConfigApplyConfiguration `json:"OpenIDConnect,omitempty"` + AdditionalConfig *string `json:"additionalConfig,omitempty"` } // PackageManagerConfigApplyConfiguration constructs a declarative configuration of the PackageManagerConfig type for use with @@ -116,6 +118,22 @@ func (b *PackageManagerConfigApplyConfiguration) WithDebug(value *PackageManager return b } +// WithAuthentication sets the Authentication field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Authentication field is set to the value of the last call. +func (b *PackageManagerConfigApplyConfiguration) WithAuthentication(value *PackageManagerAuthenticationConfigApplyConfiguration) *PackageManagerConfigApplyConfiguration { + b.Authentication = value + return b +} + +// WithOpenIDConnect sets the OpenIDConnect field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the OpenIDConnect field is set to the value of the last call. +func (b *PackageManagerConfigApplyConfiguration) WithOpenIDConnect(value *PackageManagerOIDCConfigApplyConfiguration) *PackageManagerConfigApplyConfiguration { + b.OpenIDConnect = value + return b +} + // WithAdditionalConfig sets the AdditionalConfig field in the declarative configuration to the given value // and returns the receiver, so that objects can be built by chaining "With" function invocations. // If called multiple times, the AdditionalConfig field is set to the value of the last call. diff --git a/client-go/applyconfiguration/core/v1beta1/packagemanageroidcconfig.go b/client-go/applyconfiguration/core/v1beta1/packagemanageroidcconfig.go new file mode 100644 index 0000000..aac85e6 --- /dev/null +++ b/client-go/applyconfiguration/core/v1beta1/packagemanageroidcconfig.go @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2023-2026 Posit Software, PBC + +// Code generated by applyconfiguration-gen. DO NOT EDIT. + +package v1beta1 + +// PackageManagerOIDCConfigApplyConfiguration represents a declarative configuration of the PackageManagerOIDCConfig type for use +// with apply. +type PackageManagerOIDCConfigApplyConfiguration struct { + ClientId *string `json:"ClientId,omitempty"` + ClientSecret *string `json:"ClientSecret,omitempty"` + ClientSecretFile *string `json:"ClientSecretFile,omitempty"` + Issuer *string `json:"Issuer,omitempty"` + RequireLogin *bool `json:"RequireLogin,omitempty"` + Logging *bool `json:"Logging,omitempty"` + Scope *string `json:"Scope,omitempty"` + CustomScope *string `json:"CustomScope,omitempty"` + NoAutoGroupsScope *bool `json:"NoAutoGroupsScope,omitempty"` + GroupsClaim *string `json:"GroupsClaim,omitempty"` + GroupsSeparator *string `json:"GroupsSeparator,omitempty"` + RoleClaim *string `json:"RoleClaim,omitempty"` + RolesSeparator *string `json:"RolesSeparator,omitempty"` + UniqueIdClaim *string `json:"UniqueIdClaim,omitempty"` + UsernameClaim *string `json:"UsernameClaim,omitempty"` + TokenLifetime *string `json:"TokenLifetime,omitempty"` + MaxAuthenticationAge *string `json:"MaxAuthenticationAge,omitempty"` + DisablePKCE *bool `json:"DisablePKCE,omitempty"` + EnableDevicePKCE *bool `json:"EnableDevicePKCE,omitempty"` +} + +// PackageManagerOIDCConfigApplyConfiguration constructs a declarative configuration of the PackageManagerOIDCConfig type for use with +// apply. +func PackageManagerOIDCConfig() *PackageManagerOIDCConfigApplyConfiguration { + return &PackageManagerOIDCConfigApplyConfiguration{} +} + +// WithClientId sets the ClientId field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ClientId field is set to the value of the last call. +func (b *PackageManagerOIDCConfigApplyConfiguration) WithClientId(value string) *PackageManagerOIDCConfigApplyConfiguration { + b.ClientId = &value + return b +} + +// WithClientSecret sets the ClientSecret field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ClientSecret field is set to the value of the last call. +func (b *PackageManagerOIDCConfigApplyConfiguration) WithClientSecret(value string) *PackageManagerOIDCConfigApplyConfiguration { + b.ClientSecret = &value + return b +} + +// WithClientSecretFile sets the ClientSecretFile field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ClientSecretFile field is set to the value of the last call. +func (b *PackageManagerOIDCConfigApplyConfiguration) WithClientSecretFile(value string) *PackageManagerOIDCConfigApplyConfiguration { + b.ClientSecretFile = &value + return b +} + +// WithIssuer sets the Issuer field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Issuer field is set to the value of the last call. +func (b *PackageManagerOIDCConfigApplyConfiguration) WithIssuer(value string) *PackageManagerOIDCConfigApplyConfiguration { + b.Issuer = &value + return b +} + +// WithRequireLogin sets the RequireLogin field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the RequireLogin field is set to the value of the last call. +func (b *PackageManagerOIDCConfigApplyConfiguration) WithRequireLogin(value bool) *PackageManagerOIDCConfigApplyConfiguration { + b.RequireLogin = &value + return b +} + +// WithLogging sets the Logging field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Logging field is set to the value of the last call. +func (b *PackageManagerOIDCConfigApplyConfiguration) WithLogging(value bool) *PackageManagerOIDCConfigApplyConfiguration { + b.Logging = &value + return b +} + +// WithScope sets the Scope field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Scope field is set to the value of the last call. +func (b *PackageManagerOIDCConfigApplyConfiguration) WithScope(value string) *PackageManagerOIDCConfigApplyConfiguration { + b.Scope = &value + return b +} + +// WithCustomScope sets the CustomScope field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CustomScope field is set to the value of the last call. +func (b *PackageManagerOIDCConfigApplyConfiguration) WithCustomScope(value string) *PackageManagerOIDCConfigApplyConfiguration { + b.CustomScope = &value + return b +} + +// WithNoAutoGroupsScope sets the NoAutoGroupsScope field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the NoAutoGroupsScope field is set to the value of the last call. +func (b *PackageManagerOIDCConfigApplyConfiguration) WithNoAutoGroupsScope(value bool) *PackageManagerOIDCConfigApplyConfiguration { + b.NoAutoGroupsScope = &value + return b +} + +// WithGroupsClaim sets the GroupsClaim field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GroupsClaim field is set to the value of the last call. +func (b *PackageManagerOIDCConfigApplyConfiguration) WithGroupsClaim(value string) *PackageManagerOIDCConfigApplyConfiguration { + b.GroupsClaim = &value + return b +} + +// WithGroupsSeparator sets the GroupsSeparator field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GroupsSeparator field is set to the value of the last call. +func (b *PackageManagerOIDCConfigApplyConfiguration) WithGroupsSeparator(value string) *PackageManagerOIDCConfigApplyConfiguration { + b.GroupsSeparator = &value + return b +} + +// WithRoleClaim sets the RoleClaim field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the RoleClaim field is set to the value of the last call. +func (b *PackageManagerOIDCConfigApplyConfiguration) WithRoleClaim(value string) *PackageManagerOIDCConfigApplyConfiguration { + b.RoleClaim = &value + return b +} + +// WithRolesSeparator sets the RolesSeparator field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the RolesSeparator field is set to the value of the last call. +func (b *PackageManagerOIDCConfigApplyConfiguration) WithRolesSeparator(value string) *PackageManagerOIDCConfigApplyConfiguration { + b.RolesSeparator = &value + return b +} + +// WithUniqueIdClaim sets the UniqueIdClaim field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UniqueIdClaim field is set to the value of the last call. +func (b *PackageManagerOIDCConfigApplyConfiguration) WithUniqueIdClaim(value string) *PackageManagerOIDCConfigApplyConfiguration { + b.UniqueIdClaim = &value + return b +} + +// WithUsernameClaim sets the UsernameClaim field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UsernameClaim field is set to the value of the last call. +func (b *PackageManagerOIDCConfigApplyConfiguration) WithUsernameClaim(value string) *PackageManagerOIDCConfigApplyConfiguration { + b.UsernameClaim = &value + return b +} + +// WithTokenLifetime sets the TokenLifetime field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the TokenLifetime field is set to the value of the last call. +func (b *PackageManagerOIDCConfigApplyConfiguration) WithTokenLifetime(value string) *PackageManagerOIDCConfigApplyConfiguration { + b.TokenLifetime = &value + return b +} + +// WithMaxAuthenticationAge sets the MaxAuthenticationAge field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the MaxAuthenticationAge field is set to the value of the last call. +func (b *PackageManagerOIDCConfigApplyConfiguration) WithMaxAuthenticationAge(value string) *PackageManagerOIDCConfigApplyConfiguration { + b.MaxAuthenticationAge = &value + return b +} + +// WithDisablePKCE sets the DisablePKCE field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DisablePKCE field is set to the value of the last call. +func (b *PackageManagerOIDCConfigApplyConfiguration) WithDisablePKCE(value bool) *PackageManagerOIDCConfigApplyConfiguration { + b.DisablePKCE = &value + return b +} + +// WithEnableDevicePKCE sets the EnableDevicePKCE field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the EnableDevicePKCE field is set to the value of the last call. +func (b *PackageManagerOIDCConfigApplyConfiguration) WithEnableDevicePKCE(value bool) *PackageManagerOIDCConfigApplyConfiguration { + b.EnableDevicePKCE = &value + return b +} diff --git a/client-go/applyconfiguration/core/v1beta1/packagemanagerspec.go b/client-go/applyconfiguration/core/v1beta1/packagemanagerspec.go index 742eb42..4df5b1a 100644 --- a/client-go/applyconfiguration/core/v1beta1/packagemanagerspec.go +++ b/client-go/applyconfiguration/core/v1beta1/packagemanagerspec.go @@ -37,6 +37,7 @@ type PackageManagerSpecApplyConfiguration struct { Replicas *int `json:"replicas,omitempty"` GitSSHKeys []SSHKeyConfigApplyConfiguration `json:"gitSSHKeys,omitempty"` AzureFiles *AzureFilesConfigApplyConfiguration `json:"azureFiles,omitempty"` + OIDCClientSecretKey *string `json:"oidcClientSecretKey,omitempty"` } // PackageManagerSpecApplyConfiguration constructs a declarative configuration of the PackageManagerSpec type for use with @@ -261,3 +262,11 @@ func (b *PackageManagerSpecApplyConfiguration) WithAzureFiles(value *AzureFilesC b.AzureFiles = value return b } + +// WithOIDCClientSecretKey sets the OIDCClientSecretKey field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the OIDCClientSecretKey field is set to the value of the last call. +func (b *PackageManagerSpecApplyConfiguration) WithOIDCClientSecretKey(value string) *PackageManagerSpecApplyConfiguration { + b.OIDCClientSecretKey = &value + return b +} diff --git a/client-go/applyconfiguration/utils.go b/client-go/applyconfiguration/utils.go index bba2e5a..3a87ec6 100644 --- a/client-go/applyconfiguration/utils.go +++ b/client-go/applyconfiguration/utils.go @@ -125,6 +125,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &corev1beta1.InternalWorkbenchSpecApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("PackageManager"): return &corev1beta1.PackageManagerApplyConfiguration{} + case v1beta1.SchemeGroupVersion.WithKind("PackageManagerAuthenticationConfig"): + return &corev1beta1.PackageManagerAuthenticationConfigApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("PackageManagerConfig"): return &corev1beta1.PackageManagerConfigApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("PackageManagerCRANConfig"): @@ -139,6 +141,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &corev1beta1.PackageManagerHttpConfigApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("PackageManagerMetricsConfig"): return &corev1beta1.PackageManagerMetricsConfigApplyConfiguration{} + case v1beta1.SchemeGroupVersion.WithKind("PackageManagerOIDCConfig"): + return &corev1beta1.PackageManagerOIDCConfigApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("PackageManagerPostgresConfig"): return &corev1beta1.PackageManagerPostgresConfigApplyConfiguration{} case v1beta1.SchemeGroupVersion.WithKind("PackageManagerReposConfig"): diff --git a/config/crd/bases/core.posit.team_packagemanagers.yaml b/config/crd/bases/core.posit.team_packagemanagers.yaml index 72619fa..23f682d 100644 --- a/config/crd/bases/core.posit.team_packagemanagers.yaml +++ b/config/crd/bases/core.posit.team_packagemanagers.yaml @@ -75,6 +75,21 @@ spec: type: string config: properties: + Authentication: + properties: + APITokenAuth: + type: boolean + CookieSweepDuration: + type: string + DeviceAuthType: + type: string + Inactivity: + type: string + Lifetime: + type: string + NewReposAuthByDefault: + type: boolean + type: object CRAN: description: 'PackageManagerCRANConfig is deprecated TODO: deprecated! We will remove this soon!' @@ -107,6 +122,47 @@ spec: Enabled: type: boolean type: object + OpenIDConnect: + properties: + ClientId: + type: string + ClientSecret: + type: string + ClientSecretFile: + type: string + CustomScope: + type: string + DisablePKCE: + type: boolean + EnableDevicePKCE: + type: boolean + GroupsClaim: + type: string + GroupsSeparator: + type: string + Issuer: + type: string + Logging: + type: boolean + MaxAuthenticationAge: + type: string + NoAutoGroupsScope: + type: boolean + RequireLogin: + type: boolean + RoleClaim: + type: string + RolesSeparator: + type: string + Scope: + type: string + TokenLifetime: + type: string + UniqueIdClaim: + type: string + UsernameClaim: + type: string + type: object Postgres: properties: URL: @@ -304,6 +360,11 @@ spec: additionalProperties: type: string type: object + oidcClientSecretKey: + description: |- + OIDCClientSecretKey is the key name in the vault for the OIDC client secret. + When set, the client secret will be mounted at /etc/rstudio-pm/oidc-client-secret + type: string replicas: type: integer secret: diff --git a/config/crd/bases/core.posit.team_sites.yaml b/config/crd/bases/core.posit.team_sites.yaml index 647b764..40d3c7b 100644 --- a/config/crd/bases/core.posit.team_sites.yaml +++ b/config/crd/bases/core.posit.team_sites.yaml @@ -633,6 +633,59 @@ spec: description: AdditionalConfig allows appending arbitrary gcfg config content to the generated config. type: string + auth: + description: Auth configures OIDC authentication for Package Manager's + web UI + properties: + administratorRoleMapping: + items: + type: string + type: array + clientId: + type: string + disableGroupsClaim: + type: boolean + emailClaim: + type: string + groups: + type: boolean + groupsClaim: + type: string + issuer: + type: string + publisherRoleMapping: + items: + type: string + type: array + samlEmailAttribute: + type: string + samlFirstNameAttribute: + type: string + samlIdPAttributeProfile: + description: SAML-specific attribute mappings (mutually exclusive + with SamlIdPAttributeProfile) + type: string + samlLastNameAttribute: + type: string + samlMetadataUrl: + type: string + samlUsernameAttribute: + type: string + scopes: + items: + type: string + type: array + type: + type: string + uniqueIdClaim: + type: string + usernameClaim: + type: string + viewerRoleMapping: + items: + type: string + type: array + type: object azureFiles: description: AzureFiles configures Azure Files integration for persistent storage @@ -762,6 +815,10 @@ spec: additionalProperties: type: string type: object + oidcClientSecretKey: + description: OIDCClientSecretKey is the key in the vault for the + OIDC client secret + type: string replicas: type: integer s3Bucket: diff --git a/dist/chart/templates/crd/core.posit.team_packagemanagers.yaml b/dist/chart/templates/crd/core.posit.team_packagemanagers.yaml index 323d55a..fd1d978 100755 --- a/dist/chart/templates/crd/core.posit.team_packagemanagers.yaml +++ b/dist/chart/templates/crd/core.posit.team_packagemanagers.yaml @@ -96,6 +96,21 @@ spec: type: string config: properties: + Authentication: + properties: + APITokenAuth: + type: boolean + CookieSweepDuration: + type: string + DeviceAuthType: + type: string + Inactivity: + type: string + Lifetime: + type: string + NewReposAuthByDefault: + type: boolean + type: object CRAN: description: 'PackageManagerCRANConfig is deprecated TODO: deprecated! We will remove this soon!' @@ -128,6 +143,47 @@ spec: Enabled: type: boolean type: object + OpenIDConnect: + properties: + ClientId: + type: string + ClientSecret: + type: string + ClientSecretFile: + type: string + CustomScope: + type: string + DisablePKCE: + type: boolean + EnableDevicePKCE: + type: boolean + GroupsClaim: + type: string + GroupsSeparator: + type: string + Issuer: + type: string + Logging: + type: boolean + MaxAuthenticationAge: + type: string + NoAutoGroupsScope: + type: boolean + RequireLogin: + type: boolean + RoleClaim: + type: string + RolesSeparator: + type: string + Scope: + type: string + TokenLifetime: + type: string + UniqueIdClaim: + type: string + UsernameClaim: + type: string + type: object Postgres: properties: URL: @@ -325,6 +381,11 @@ spec: additionalProperties: type: string type: object + oidcClientSecretKey: + description: |- + OIDCClientSecretKey is the key name in the vault for the OIDC client secret. + When set, the client secret will be mounted at /etc/rstudio-pm/oidc-client-secret + type: string replicas: type: integer secret: diff --git a/dist/chart/templates/crd/core.posit.team_sites.yaml b/dist/chart/templates/crd/core.posit.team_sites.yaml index e0bd60e..03444f6 100755 --- a/dist/chart/templates/crd/core.posit.team_sites.yaml +++ b/dist/chart/templates/crd/core.posit.team_sites.yaml @@ -654,6 +654,59 @@ spec: description: AdditionalConfig allows appending arbitrary gcfg config content to the generated config. type: string + auth: + description: Auth configures OIDC authentication for Package Manager's + web UI + properties: + administratorRoleMapping: + items: + type: string + type: array + clientId: + type: string + disableGroupsClaim: + type: boolean + emailClaim: + type: string + groups: + type: boolean + groupsClaim: + type: string + issuer: + type: string + publisherRoleMapping: + items: + type: string + type: array + samlEmailAttribute: + type: string + samlFirstNameAttribute: + type: string + samlIdPAttributeProfile: + description: SAML-specific attribute mappings (mutually exclusive + with SamlIdPAttributeProfile) + type: string + samlLastNameAttribute: + type: string + samlMetadataUrl: + type: string + samlUsernameAttribute: + type: string + scopes: + items: + type: string + type: array + type: + type: string + uniqueIdClaim: + type: string + usernameClaim: + type: string + viewerRoleMapping: + items: + type: string + type: array + type: object azureFiles: description: AzureFiles configures Azure Files integration for persistent storage @@ -783,6 +836,10 @@ spec: additionalProperties: type: string type: object + oidcClientSecretKey: + description: OIDCClientSecretKey is the key in the vault for the + OIDC client secret + type: string replicas: type: integer s3Bucket: diff --git a/internal/controller/core/package_manager.go b/internal/controller/core/package_manager.go index af2fef6..5ff8147 100644 --- a/internal/controller/core/package_manager.go +++ b/internal/controller/core/package_manager.go @@ -272,6 +272,11 @@ func (r *PackageManagerReconciler) ensureDeployedService(ctx context.Context, re "password": "pkg-db-password", } + // Add OIDC client secret to SecretProviderClass when configured + if pm.Spec.OIDCClientSecretKey != "" { + secretRefs["oidc-client-secret"] = pm.Spec.OIDCClientSecretKey + } + if targetSpc, err := product.GetSecretProviderClassForAllSecrets( pm, pm.SecretProviderClassName(), req.Namespace, pm.Spec.Secret.VaultName, diff --git a/internal/controller/core/site_controller_package_manager.go b/internal/controller/core/site_controller_package_manager.go index bca0a64..41e3d7f 100644 --- a/internal/controller/core/site_controller_package_manager.go +++ b/internal/controller/core/site_controller_package_manager.go @@ -117,6 +117,22 @@ func (r *SiteReconciler) reconcilePackageManager( // Propagate additional config from Site to PackageManager pm.Spec.Config.AdditionalConfig = site.Spec.PackageManager.AdditionalConfig + // Propagate OIDC authentication configuration + if site.Spec.PackageManager.Auth != nil && site.Spec.PackageManager.Auth.Type == v1beta1.AuthTypeOidc { + pm.Spec.Config.OpenIDConnect = &v1beta1.PackageManagerOIDCConfig{ + ClientId: site.Spec.PackageManager.Auth.ClientId, + ClientSecretFile: "/etc/rstudio-pm/oidc-client-secret", + Issuer: site.Spec.PackageManager.Auth.Issuer, + RequireLogin: true, + Scope: "repos:read:*", + } + if site.Spec.PackageManager.Auth.GroupsClaim != "" { + pm.Spec.Config.OpenIDConnect.GroupsClaim = site.Spec.PackageManager.Auth.GroupsClaim + } + // Propagate the OIDC client secret key so the volume factory can mount it + pm.Spec.OIDCClientSecretKey = site.Spec.PackageManager.OIDCClientSecretKey + } + return nil }); err != nil { l.Error(err, "error creating package manager instance")