Skip to content

Commit

Permalink
Merge pull request #179 from vshn/fix/invalid_object_names
Browse files Browse the repository at this point in the history
Escape Object names before applying
  • Loading branch information
Kidswiss authored Jun 17, 2024
2 parents 7bf809b + a831d23 commit 5fb5bb7
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 1 deletion.
14 changes: 13 additions & 1 deletion pkg/comp-functions/runtime/function_mgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,9 @@ func (s *ServiceRuntime) SetDesiredComposedResourceWithName(obj xpresource.Manag

s.addOwnerReferenceAnnotation(obj, true)

escapeK8sNames(obj)
name = escapeDNS1123(name, false)

for _, opt := range opts {
opt(obj)
}
Expand Down Expand Up @@ -409,9 +412,11 @@ func KubeOptionAddRefs(refs ...xkube.Reference) KubeObjectOption {
// The associated secret will have the UID of the parent object as the name.
func KubeOptionAddConnectionDetails(destNamespace string, cd ...xkube.ConnectionDetail) KubeObjectOption {
return func(obj *xkube.Object) {
objName := escapeDNS1123(obj.GetName()+"-cd", false)

obj.Spec.ConnectionDetails = cd
obj.Spec.WriteConnectionSecretToReference = &xpv1.SecretReference{
Name: obj.GetName() + "-cd",
Name: objName,
Namespace: destNamespace,
}
}
Expand Down Expand Up @@ -486,6 +491,8 @@ func (s *ServiceRuntime) putIntoObject(observeOnly bool, o client.Object, kon, r

s.addOwnerReferenceAnnotation(o, false)

escapeK8sNames(o)

kind, _, err := composed.Scheme.ObjectKinds(o)
if err != nil {
return nil, fmt.Errorf("cannot determine object kind, have you registered it in the scheme: %w", err)
Expand Down Expand Up @@ -614,6 +621,7 @@ func (s *ServiceRuntime) GetConnectionDetails() map[string][]byte {
// composed resource.
// Returns an empty map if not found.
func (s *ServiceRuntime) GetObservedComposedResourceConnectionDetails(objectName string) (map[string][]byte, error) {
objectName = escapeDNS1123(objectName, false)
object, ok := s.req.Observed.Resources[objectName]
if !ok {
return map[string][]byte{}, ErrNotFound
Expand All @@ -624,6 +632,7 @@ func (s *ServiceRuntime) GetObservedComposedResourceConnectionDetails(objectName

// GetObservedComposedResource returns and unmarshalls the observed object into the given managed resource.
func (s *ServiceRuntime) GetObservedComposedResource(obj xpresource.Managed, name string) error {
name = escapeDNS1123(name, false)
resources, err := request.GetObservedComposedResources(s.req)
if err != nil {
return err
Expand All @@ -646,6 +655,7 @@ func (s *ServiceRuntime) GetObservedComposedResource(obj xpresource.Managed, nam
// GetDesiredComposedResourceByName will return a desired composed resource from the request.
// Use this, if you want anything from a previous function in the pipeline.
func (s *ServiceRuntime) GetDesiredComposedResourceByName(obj xpresource.Managed, name string) error {
name = escapeDNS1123(name, false)
if res, ok := s.desiredResources[resource.Name(name)]; ok {
jsonString, err := res.Resource.Unstructured.MarshalJSON()
if err != nil {
Expand Down Expand Up @@ -708,6 +718,7 @@ func (s *ServiceRuntime) AddObservedConnectionDetails(name string) error {

// GetObservedKubeObject returns the object as is on the cluster.
func (s *ServiceRuntime) GetObservedKubeObject(obj client.Object, name string) error {
name = escapeDNS1123(name, false)
resources, err := request.GetObservedComposedResources(s.req)
if err != nil {
return err
Expand Down Expand Up @@ -739,6 +750,7 @@ func (s *ServiceRuntime) GetObservedKubeObject(obj client.Object, name string) e

// GetDesiredKubeObject returns the object as is on the cluster.
func (s *ServiceRuntime) GetDesiredKubeObject(obj client.Object, name string) error {
name = escapeDNS1123(name, false)
res, ok := s.desiredResources[resource.Name(name)]
if !ok {
return ErrNotFound
Expand Down
93 changes: 93 additions & 0 deletions pkg/comp-functions/runtime/naming.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package runtime

import (
"crypto/sha1"
"fmt"
"regexp"
"strings"

"github.com/crossplane/function-sdk-go/resource/composed"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// escapeK8sNames will figure out what naming scheme applies to the given object
// and tries to find the correct naming scheme for it.
// Then it will escape the name accordingly.
func escapeK8sNames(obj client.Object) {
kind, _, err := composed.Scheme.ObjectKinds(obj)
if err != nil {
obj.SetName(escapeDNS1123(obj.GetName(), false))
}

switch kind[0].Kind {
case "Pod":
obj.SetName(escapeDNS1123Label(obj.GetName()))
case "Namespace":
obj.SetName(escapeDNS1123Label(obj.GetName()))
case "Service":
obj.SetName(escapeDNS1123Label(obj.GetName()))
case "Role":
obj.SetName(escapeDNS1123(obj.GetName(), true))
case "ClusterRole":
obj.SetName(escapeDNS1123(obj.GetName(), true))
case "RoleBinding":
obj.SetName(escapeDNS1123(obj.GetName(), true))
case "ClusterRoleBinding":
obj.SetName(escapeDNS1123(obj.GetName(), true))
default:
obj.SetName(escapeDNS1123(obj.GetName(), false))
}
}

// escapeDNS1123Label does the same as escapeDNS1123 but also limit to 63 chars
func escapeDNS1123Label(name string) string {
name = escapeDNS1123(name, false)
if len(name) > 63 {
suffix := hashString(name)
name = name[:58] + suffix
}
return name
}

// escapeDNS1123 will always return a string that conforms to K8s' DNS subdomain naming scheme
// contain no more than 253 characters
// contain only lowercase alphanumeric characters, '-' or '.'
// start with an alphanumeric character
// end with an alphanumeric character
// We also remove any '.' here, so that it can be used as a base function for escaping
// label names as well.
func escapeDNS1123(name string, allowColons bool) string {
escapeNameStart := regexp.MustCompile("^[^a-zA-Z0-9]+")
escapeNameEnd := regexp.MustCompile("[^a-zA-Z0-9]+$")
escapeExpression := regexp.MustCompile("[^a-zA-Z0-9]+")
dns1123LabelRegexColons := "^[a-z0-9]([-a-z0-9:]*[a-z0-9])?$"
dns1123LabelRegexp := regexp.MustCompile("^[a-z0-9]([-a-z0-9]*[a-z0-9])?$")

if allowColons {
dns1123LabelRegexp = regexp.MustCompile(dns1123LabelRegexColons)
}

if dns1123LabelRegexp.MatchString(name) && len(name) <= 253 {
return name
}

escapedName := strings.ToLower(name)

escapedName = escapeExpression.ReplaceAllString(escapedName, "-")
escapedName = escapeNameStart.ReplaceAllString(escapedName, "")
escapedName = escapeNameEnd.ReplaceAllString(escapedName, "")

if len(escapedName) > 253 {
suffix := hashString(escapedName)
escapedName = escapedName[:248] + suffix
}

return escapedName
}

// hashString returns the first 4 symbols of the sha1 hash, prefixed with a `-`
func hashString(name string) string {
hasher := sha1.New()
hasher.Write([]byte(name))
return "-" + fmt.Sprintf("%x", hasher.Sum(nil))[:4]
}
70 changes: 70 additions & 0 deletions pkg/comp-functions/runtime/naming_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package runtime

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestServiceRuntime_escapeK8sNames(t *testing.T) {

type args struct {
name string
allowColon bool
}
tests := []struct {
name string
args args
want string
}{
{
name: "GivenInvalidString_ThenExpectValidString",
args: args{
name: "Hello_World",
},
want: "hello-world",
},
{
name: "GivenValidString_ThenExpectValidString",
args: args{
name: "hello-world",
},
want: "hello-world",
},
{
name: "GivenStringStartingWithUnderscore_ThenExpectValidString",
args: args{
name: "_Hello_World",
},
want: "hello-world",
},
{
name: "GivenMoreInvalidCharacters_ThenExpectValidString",
args: args{
name: "+*ç%&/()=?b+*ç%&/()=?",
},
want: "b",
},
{
name: "GivenVerylongName_ThenExpectValidString",
args: args{
name: "AVeryLongStringWhichShouldBetrimmedToItaSensiblesize1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012",
},
want: "averylongstringwhichshouldbetrimmedtoitasensiblesize1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456-fdbe",
},
{
name: "GivenNameWithColon_ThenExpectValisString",
args: args{
name: "test:colon",
allowColon: true,
},
want: "test:colon",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := escapeDNS1123(tt.args.name, tt.args.allowColon)
assert.Equal(t, tt.want, got)
})
}
}

0 comments on commit 5fb5bb7

Please sign in to comment.