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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
285 changes: 279 additions & 6 deletions openshift/openshift.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,18 @@ const (
buildConfigSourceImagesPullSecret = "/spec/source/images/%v/pullSecret"
buildConfigSourceImagesFrom = "/spec/source/images/%v/from/name"
roleBindingSubject = "/subjects/%d/namespace"

// SCCNamespaceUIDMin is the minimum UID value for OpenShift SCC-injected namespace UID ranges.
// UIDs >= this value are considered SCC-injected and should be stripped during migration.
SCCNamespaceUIDMin int64 = 1000000000
)

var defaultPullSecrets = []string{"builder-dockercfg-", "default-dockercfg-", "deployer-dockercfg-"}

func updateBuildConfigImageReference(
imgRef v1.ObjectReference,
imgPath string,
fields openshiftOptionalFields,
fields OpenshiftOptionalFields,
) (jsonpatch.Patch, error) {
patch := jsonpatch.Patch{}
var err error
Expand All @@ -67,15 +71,15 @@ func updateBuildConfigImageReference(
return patch, nil
}

func UpdateDefaultPullSecrets(u unstructured.Unstructured, fields openshiftOptionalFields) (jsonpatch.Patch, error) {
func UpdateDefaultPullSecrets(u unstructured.Unstructured, fields OpenshiftOptionalFields) (jsonpatch.Patch, error) {
return updateSecretsForSlice(getPullSecrets(u), podReplaceImagePullSecret, podRemoveImagePullSecret, fields)
}

func updateSecretsForSlice(
pullSecrets []v1.LocalObjectReference,
replaceOp string,
removeOp string,
fields openshiftOptionalFields) (jsonpatch.Patch, error) {
fields OpenshiftOptionalFields) (jsonpatch.Patch, error) {
var err error

replacePatch := jsonpatch.Patch{}
Expand Down Expand Up @@ -155,7 +159,7 @@ func UpdateRoleBinding(u unstructured.Unstructured) (jsonpatch.Patch, error) {
func updateSecret(
pullSecret *v1.LocalObjectReference,
secretPath string,
fields openshiftOptionalFields) (jsonpatch.Patch, error) {
fields OpenshiftOptionalFields) (jsonpatch.Patch, error) {
var err error

patch := jsonpatch.Patch{}
Expand Down Expand Up @@ -271,7 +275,7 @@ func isDefault(name string) bool {
return false
}

func UpdateBuildConfig(u unstructured.Unstructured, fields openshiftOptionalFields) (jsonpatch.Patch, error) {
func UpdateBuildConfig(u unstructured.Unstructured, fields OpenshiftOptionalFields) (jsonpatch.Patch, error) {
jsonPatch := jsonpatch.Patch{}
js, err := u.MarshalJSON()
if err != nil {
Expand Down Expand Up @@ -355,7 +359,7 @@ func UpdateBuildConfig(u unstructured.Unstructured, fields openshiftOptionalFiel
return jsonPatch, nil
}

func UpdateDeploymentConfig(u unstructured.Unstructured, fields openshiftOptionalFields) (jsonpatch.Patch, error) {
func UpdateDeploymentConfig(u unstructured.Unstructured, fields OpenshiftOptionalFields) (jsonpatch.Patch, error) {
js, err := u.MarshalJSON()
if err != nil {
return nil, err
Expand Down Expand Up @@ -417,3 +421,272 @@ func getSecretReferencesServiceAccount(u unstructured.Unstructured) []v1.ObjectR

return sa.Secrets
}

// sccUID safely extracts a UID value from an interface{}, handling both int64 and float64
// types. JSON unmarshaling of unstructured.Unstructured produces float64 for numeric values,
// so this helper ensures we can check SCC UID ranges regardless of the source type.
// Returns the int64 value and true if successful, or 0 and false if the type is neither.
func sccUID(v interface{}) (int64, bool) {
switch val := v.(type) {
case int64:
return val, true
case float64:
return int64(val), true
default:
return 0, false
}
}

// StripSecurityContext removes SCC-injected security context values while preserving
Comment thread
nachandr marked this conversation as resolved.
// user-configured values.
//
// Only strips:
// - runAsUser when >= SCCNamespaceUIDMin (SCC-injected namespace UID range)
// - fsGroup when >= SCCNamespaceUIDMin (SCC-injected namespace UID range)
// - seLinuxOptions.level (always SCC-injected)
//
// Preserves all other security context values (capabilities, readOnlyRootFilesystem, etc.)
func StripSecurityContext(u unstructured.Unstructured) (jsonpatch.Patch, error) {
kind := u.GetKind()

// Only process workload resources
if kind != "Pod" && kind != "Deployment" && kind != "StatefulSet" &&
kind != "DaemonSet" && kind != "Job" && kind != "CronJob" &&
kind != "ReplicaSet" && kind != "ReplicationController" {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return jsonpatch.Patch{}, nil
}

// Create a copy to modify
modified := u.DeepCopy()

// Determine base path for pod spec
var basePath []string
if kind == "Pod" {
basePath = []string{"spec"}
} else if kind == "CronJob" {
basePath = []string{"spec", "jobTemplate", "spec", "template", "spec"}
} else {
basePath = []string{"spec", "template", "spec"}
}

// Strip SCC-injected values from pod-level securityContext
stripPodSecurityContext := func(scPath ...string) error {
sc, found, _ := unstructured.NestedMap(modified.Object, scPath...)
if !found || sc == nil {
return nil
}

// Strip runAsUser if >= 1000000000
if runAsUser, ok := sccUID(sc["runAsUser"]); ok && runAsUser >= SCCNamespaceUIDMin {
delete(sc, "runAsUser")
}

// Strip fsGroup if >= 1000000000
if fsGroup, ok := sccUID(sc["fsGroup"]); ok && fsGroup >= SCCNamespaceUIDMin {
delete(sc, "fsGroup")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Strip seLinuxOptions.level (always SCC-injected)
if seLinuxOpts, ok := sc["seLinuxOptions"].(map[string]interface{}); ok {
delete(seLinuxOpts, "level")
// If seLinuxOptions is now empty, remove it entirely
if len(seLinuxOpts) == 0 {
delete(sc, "seLinuxOptions")
} else {
sc["seLinuxOptions"] = seLinuxOpts
}
}

// If securityContext is now empty, remove it entirely
if len(sc) == 0 {
unstructured.RemoveNestedField(modified.Object, scPath...)
} else {
if err := unstructured.SetNestedMap(modified.Object, sc, scPath...); err != nil {
return err
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return nil
}

// Strip pod-level security context
podSecurityContextPath := append(basePath, "securityContext")
if err := stripPodSecurityContext(podSecurityContextPath...); err != nil {
return nil, err
}

// Helper function to strip container securityContext
stripContainerSecurityContext := func(containersPath ...string) error {
containers, found, _ := unstructured.NestedSlice(modified.Object, containersPath...)
if !found {
return nil
}

for i, c := range containers {
container, ok := c.(map[string]interface{})
if !ok {
continue
}

sc, ok := container["securityContext"].(map[string]interface{})
if !ok || sc == nil {
continue
}

// Strip runAsUser if >= 1000000000
if runAsUser, ok := sccUID(sc["runAsUser"]); ok && runAsUser >= SCCNamespaceUIDMin {
delete(sc, "runAsUser")
}

// Strip seLinuxOptions.level (always SCC-injected)
if seLinuxOpts, ok := sc["seLinuxOptions"].(map[string]interface{}); ok {
delete(seLinuxOpts, "level")
// If seLinuxOptions is now empty, remove it entirely
if len(seLinuxOpts) == 0 {
delete(sc, "seLinuxOptions")
} else {
sc["seLinuxOptions"] = seLinuxOpts
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// If securityContext is now empty, remove it entirely
if len(sc) == 0 {
delete(container, "securityContext")
} else {
container["securityContext"] = sc
}

containers[i] = container
}
if err := unstructured.SetNestedSlice(modified.Object, containers, containersPath...); err != nil {
return err
}
return nil
}

// Strip container-level securityContext
containersPath := append(basePath, "containers")
if err := stripContainerSecurityContext(containersPath...); err != nil {
return nil, err
}

// Strip initContainers securityContext
initContainersPath := append(basePath, "initContainers")
if err := stripContainerSecurityContext(initContainersPath...); err != nil {
return nil, err
}

// Strip ephemeralContainers securityContext (if present)
ephemeralContainersPath := append(basePath, "ephemeralContainers")
if err := stripContainerSecurityContext(ephemeralContainersPath...); err != nil {
return nil, err
}

// Generate patch by comparing original and modified
// Build JSON patch operations for fields that were removed
var patchOps []string

// Helper to build path string
buildPath := func(parts ...string) string {
result := ""
for _, part := range parts {
if part != "" {
result += "/" + part
}
}
return result
}

// Check what changed in pod-level securityContext
origSC, _, _ := unstructured.NestedMap(u.Object, append(basePath, "securityContext")...)
modSC, _, _ := unstructured.NestedMap(modified.Object, append(basePath, "securityContext")...)

if origSC != nil && modSC != nil {
scPath := buildPath(basePath...) + "/securityContext"
// Check each field
if _, origHas := origSC["runAsUser"]; origHas {
if _, modHas := modSC["runAsUser"]; !modHas {
patchOps = append(patchOps, fmt.Sprintf(`{"op":"remove","path":"%s/runAsUser"}`, scPath))
}
}
if _, origHas := origSC["fsGroup"]; origHas {
if _, modHas := modSC["fsGroup"]; !modHas {
patchOps = append(patchOps, fmt.Sprintf(`{"op":"remove","path":"%s/fsGroup"}`, scPath))
}
}
if origOpts, ok := origSC["seLinuxOptions"].(map[string]interface{}); ok {
if modOpts, ok := modSC["seLinuxOptions"].(map[string]interface{}); ok {
if _, origHas := origOpts["level"]; origHas {
if _, modHas := modOpts["level"]; !modHas {
patchOps = append(patchOps, fmt.Sprintf(`{"op":"remove","path":"%s/seLinuxOptions/level"}`, scPath))
}
}
} else if len(origOpts) > 0 {
// seLinuxOptions was removed entirely
if _, hasLevel := origOpts["level"]; hasLevel && len(origOpts) == 1 {
patchOps = append(patchOps, fmt.Sprintf(`{"op":"remove","path":"%s/seLinuxOptions"}`, scPath))
}
}
}
} else if origSC != nil && modSC == nil {
// Entire securityContext was removed
patchOps = append(patchOps, fmt.Sprintf(`{"op":"remove","path":"%s"}`, buildPath(basePath...)+"/securityContext"))
}

// Check container changes
checkContainerChanges := func(containerType string) {
origContainers, _, _ := unstructured.NestedSlice(u.Object, append(basePath, containerType)...)
modContainers, _, _ := unstructured.NestedSlice(modified.Object, append(basePath, containerType)...)

for i := 0; i < len(origContainers) && i < len(modContainers); i++ {
origC, ok1 := origContainers[i].(map[string]interface{})
modC, ok2 := modContainers[i].(map[string]interface{})
if !ok1 || !ok2 {
continue
}

origSC, _ := origC["securityContext"].(map[string]interface{})
modSC, ok2 := modC["securityContext"].(map[string]interface{})

containerPath := buildPath(basePath...) + "/" + containerType + "/" + fmt.Sprintf("%d", i) + "/securityContext"

if origSC != nil && modSC != nil {
if _, origHas := origSC["runAsUser"]; origHas {
if _, modHas := modSC["runAsUser"]; !modHas {
patchOps = append(patchOps, fmt.Sprintf(`{"op":"remove","path":"%s/runAsUser"}`, containerPath))
}
}
if origOpts, ok := origSC["seLinuxOptions"].(map[string]interface{}); ok {
if modOpts, ok := modSC["seLinuxOptions"].(map[string]interface{}); ok {
if _, origHas := origOpts["level"]; origHas {
if _, modHas := modOpts["level"]; !modHas {
patchOps = append(patchOps, fmt.Sprintf(`{"op":"remove","path":"%s/seLinuxOptions/level"}`, containerPath))
}
}
} else if len(origOpts) > 0 {
if _, hasLevel := origOpts["level"]; hasLevel && len(origOpts) == 1 {
patchOps = append(patchOps, fmt.Sprintf(`{"op":"remove","path":"%s/seLinuxOptions"}`, containerPath))
}
}
}
} else if origSC != nil && !ok2 {
patchOps = append(patchOps, fmt.Sprintf(`{"op":"remove","path":"%s"}`, containerPath))
}
}
}

checkContainerChanges("containers")
checkContainerChanges("initContainers")
checkContainerChanges("ephemeralContainers")

if len(patchOps) == 0 {
return jsonpatch.Patch{}, nil
}

patchJSON := "[" + strings.Join(patchOps, ",") + "]"
patch, err := jsonpatch.DecodePatch([]byte(patchJSON))
if err != nil {
return nil, err
}

return patch, nil
}
23 changes: 18 additions & 5 deletions openshift/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func (o *OpenShiftTransformPlugin) Run(request transform.PluginRequest) (transfo
u := request.Unstructured
var patch jsonpatch.Patch
whiteOut := false
inputFields, err := parseOptionalFields(request.Extras)
inputFields, err := ParseOptionalFields(request.Extras)
if err != nil {
return transform.PluginResponse{}, err
}
Expand Down Expand Up @@ -110,7 +110,18 @@ func (o *OpenShiftTransformPlugin) Run(request transform.PluginRequest) (transfo
patch, err = UpdateDeploymentConfig(u, inputFields)
case "Pod":
o.log().Info("found pod, processing update default pull secret")
patch, err = UpdateDefaultPullSecrets(u, inputFields)
pullSecretPatch, err := UpdateDefaultPullSecrets(u, inputFields)
if err != nil {
break
}
securityContextPatch, err := StripSecurityContext(u)
if err != nil {
break
}
patch = append(pullSecretPatch, securityContextPatch...)
case "Deployment", "StatefulSet", "DaemonSet", "Job", "CronJob", "ReplicaSet", "ReplicationController":
o.log().Infof("found %s, stripping SCC-injected security context", u.GetKind())
patch, err = StripSecurityContext(u)
case "Route":
o.log().Info("found route, processing")
patch, err = UpdateRoute(u)
Expand Down Expand Up @@ -165,7 +176,8 @@ func (o *OpenShiftTransformPlugin) log() logrus.FieldLogger {
return logrus.New()
}

type openshiftOptionalFields struct {
// OpenshiftOptionalFields contains the optional configuration fields for OpenShift transformations
type OpenshiftOptionalFields struct {
StripDefaultRBAC bool
StripDefaultCABundle bool
StripDefaultPullSecrets bool
Expand All @@ -174,8 +186,9 @@ type openshiftOptionalFields struct {
RegistryReplacement map[string]string
}

func parseOptionalFields(extras map[string]string) (openshiftOptionalFields, error) {
fields := openshiftOptionalFields{
// ParseOptionalFields parses the extras map into OpenshiftOptionalFields
func ParseOptionalFields(extras map[string]string) (OpenshiftOptionalFields, error) {
fields := OpenshiftOptionalFields{
StripDefaultRBAC: true,
StripDefaultCABundle: true,
StripDefaultPullSecrets: true,
Expand Down
Loading
Loading