From ad5b332e7700e4dc6a413ee633ee7cff3100cd5e Mon Sep 17 00:00:00 2001 From: Blaine Gardner Date: Tue, 20 Aug 2024 16:55:46 -0600 Subject: [PATCH] rgw: allow users to add custom volume mounts Allow RGW users to mount arbitrary volumes to RGW pods. This follows the pattern that was established for NFS to support SSSD and LDAP, and reuses much of the same code. This opens the door wider for advanced users to take advantage of some Ceph RGW features that Rook doesn't have first class support for. Signed-off-by: Blaine Gardner (cherry picked from commit eea43b7b79ae3dbcd0d6c8f8eacc8cba5c2e7ada) --- .../Object-Storage/ceph-object-store-crd.md | 36 ++- Documentation/CRDs/specification.md | 129 ++++---- .../charts/rook-ceph/templates/resources.yaml | 279 +++++++++++++++++- deploy/examples/crds.yaml | 279 +++++++++++++++++- deploy/examples/object.yaml | 8 + pkg/apis/ceph.rook.io/v1/labels.go | 78 +++++ pkg/apis/ceph.rook.io/v1/labels_test.go | 80 +++++ pkg/apis/ceph.rook.io/v1/nfs_test.go | 10 +- pkg/apis/ceph.rook.io/v1/types.go | 31 +- pkg/apis/ceph.rook.io/v1/volume.go | 24 ++ .../ceph.rook.io/v1/zz_generated.deepcopy.go | 73 +++-- pkg/operator/ceph/nfs/security.go | 24 +- pkg/operator/ceph/object/spec.go | 4 + pkg/operator/ceph/object/spec_test.go | 28 ++ pkg/operator/k8sutil/k8sutil.go | 75 ----- pkg/operator/k8sutil/k8sutil_test.go | 80 ----- 16 files changed, 945 insertions(+), 293 deletions(-) diff --git a/Documentation/CRDs/Object-Storage/ceph-object-store-crd.md b/Documentation/CRDs/Object-Storage/ceph-object-store-crd.md index 9ef7ca235..4b4efb8f0 100644 --- a/Documentation/CRDs/Object-Storage/ceph-object-store-crd.md +++ b/Documentation/CRDs/Object-Storage/ceph-object-store-crd.md @@ -190,29 +190,35 @@ The gateway settings correspond to the RGW daemon settings. for more details. Multiple endpoints can be given, but for stability of ObjectBucketClaims, we highly recommend that users give only a single external RGW endpoint that is a load balancer that sends requests to the multiple RGWs. + + Example of external rgw endpoints to connect to: + + ```yaml + gateway: + port: 80 + externalRgwEndpoints: + - ip: 192.168.39.182 + # hostname: example.com + ``` + * `annotations`: Key value pair list of annotations to add. * `labels`: Key value pair list of labels to add. * `placement`: The Kubernetes placement settings to determine where the RGW pods should be started in the cluster. * `resources`: Set resource requests/limits for the Gateway Pod(s), see [Resource Requirements/Limits](../Cluster/ceph-cluster-crd.md#resource-requirementslimits). * `priorityClassName`: Set priority class name for the Gateway Pod(s) +* `additionalVolumeMounts`: additional volumes to be mounted to the RGW pod. The root directory for + each additional volume mount is `/var/rgw`. Each volume mount has a `subPath` that defines the + subdirectory where that volumes files will be mounted. Rook supports several standard Kubernetes + volume types. Example: for an additional mount at subPath `ldap`, mounted from a secret that has + key `bindpass.secret`, the file would reside at `/var/rgw/ldap/bindpass.secret`. * `service`: The annotations to set on to the Kubernetes Service of RGW. The [service serving cert](https://docs.openshift.com/container-platform/4.6/security/certificates/service-serving-certificate.html) feature supported in Openshift is enabled by the following example: -```yaml -gateway: - service: - annotations: + ```yaml + gateway: + service: + annotations: service.beta.openshift.io/serving-cert-secret-name: -``` - -Example of external rgw endpoints to connect to: - -```yaml -gateway: - port: 80 - externalRgwEndpoints: - - ip: 192.168.39.182 - # hostname: example.com -``` + ``` ## Zone Settings diff --git a/Documentation/CRDs/specification.md b/Documentation/CRDs/specification.md index 301b3c969..4e7960824 100644 --- a/Documentation/CRDs/specification.md +++ b/Documentation/CRDs/specification.md @@ -2578,6 +2578,61 @@ string +

AdditionalVolumeMount +

+
+

AdditionalVolumeMount represents the source from where additional files in pod containers +should come from and what subdirectory they are made available in.

+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+subPath
+ +string + +
+

SubPath defines the sub-path (subdirectory) of the directory root where the volumeSource will +be mounted. All files/keys in the volume source’s volume will be mounted to the subdirectory. +This is not the same as the Kubernetes subPath volume mount option. +Each subPath definition must be unique and must not contain ‘:’.

+
+volumeSource
+ + +ConfigFileVolumeSource + + +
+

VolumeSource accepts a pared down version of the standard Kubernetes VolumeSource for the +additional file(s) like what is normally used to configure Volumes for a Pod. Fore example, a +ConfigMap, Secret, or HostPath. Each VolumeSource adds one or more additional files to the +container <directory-root>/<subPath> directory. +Be aware that some files may need to have a specific file mode like 0600 due to application +requirements. For example, CA or TLS certificates.

+
+

AdditionalVolumeMounts +([]github.com/rook/rook/pkg/apis/ceph.rook.io/v1.AdditionalVolumeMount alias)

+

+(Appears on:GatewaySpec, SSSDSidecar) +

+
+

AddressRangesSpec

@@ -5089,7 +5144,7 @@ blocking deletion.

ConfigFileVolumeSource

-(Appears on:KerberosConfigFiles, KerberosKeytabFile, SSSDSidecarAdditionalFile, SSSDSidecarConfigFile) +(Appears on:AdditionalVolumeMount, KerberosConfigFiles, KerberosKeytabFile, SSSDSidecarConfigFile)

Represents the source of a volume to mount. @@ -6958,6 +7013,22 @@ bool

Whether rgw dashboard is enabled for the rgw daemon. If not set, the rgw dashboard will be enabled.

+ + +additionalVolumeMounts
+ + +AdditionalVolumeMounts + + + + +

AdditionalVolumeMounts allows additional volumes to be mounted to the RGW pod. +The root directory for each additional volume mount is /var/rgw. +Example: for an additional mount at subPath ldap, mounted from a secret that has key +bindpass.secret, the file would reside at /var/rgw/ldap/bindpass.secret.

+ +

HTTPEndpointSpec @@ -11452,15 +11523,16 @@ securely add the file via annotations on the CephNFS spec (passed to the NFS ser additionalFiles
- -[]SSSDSidecarAdditionalFile + +AdditionalVolumeMounts (Optional)

AdditionalFiles defines any number of additional files that should be mounted into the SSSD -sidecar. These files may be referenced by the sssd.conf config file.

+sidecar with a directory root of /etc/sssd/rook-additional/. +These files may be referenced by the sssd.conf config file.

@@ -11493,55 +11565,6 @@ this may be a value between 1 and 10. See SSSD docs for more info: -

SSSDSidecarAdditionalFile -

-

-(Appears on:SSSDSidecar) -

-
-

SSSDSidecarAdditionalFile represents the source from where additional files for the the SSSD -configuration should come from and are made available.

-
- - - - - - - - - - - - - - - - - -
FieldDescription
-subPath
- -string - -
-

SubPath defines the sub-path in /etc/sssd/rook-additional/ where the additional file(s) -will be placed. Each subPath definition must be unique and must not contain ‘:’.

-
-volumeSource
- - -ConfigFileVolumeSource - - -
-

VolumeSource accepts a pared down version of the standard Kubernetes VolumeSource for the -additional file(s) like what is normally used to configure Volumes for a Pod. Fore example, a -ConfigMap, Secret, or HostPath. Each VolumeSource adds one or more additional files to the -SSSD sidecar container in the /etc/sssd/rook-additional/<subPath> directory. -Be aware that some files may need to have a specific file mode like 0600 due to requirements -by SSSD for some files. For example, CA or TLS certificates.

-

SSSDSidecarConfigFile

diff --git a/deploy/charts/rook-ceph/templates/resources.yaml b/deploy/charts/rook-ceph/templates/resources.yaml index 568c97643..e7954efe7 100644 --- a/deploy/charts/rook-ceph/templates/resources.yaml +++ b/deploy/charts/rook-ceph/templates/resources.yaml @@ -8997,16 +8997,19 @@ spec: additionalFiles: description: |- AdditionalFiles defines any number of additional files that should be mounted into the SSSD - sidecar. These files may be referenced by the sssd.conf config file. + sidecar with a directory root of `/etc/sssd/rook-additional/`. + These files may be referenced by the sssd.conf config file. items: description: |- - SSSDSidecarAdditionalFile represents the source from where additional files for the the SSSD - configuration should come from and are made available. + AdditionalVolumeMount represents the source from where additional files in pod containers + should come from and what subdirectory they are made available in. properties: subPath: description: |- - SubPath defines the sub-path in `/etc/sssd/rook-additional/` where the additional file(s) - will be placed. Each subPath definition must be unique and must not contain ':'. + SubPath defines the sub-path (subdirectory) of the directory root where the volumeSource will + be mounted. All files/keys in the volume source's volume will be mounted to the subdirectory. + This is not the same as the Kubernetes `subPath` volume mount option. + Each subPath definition must be unique and must not contain ':'. minLength: 1 pattern: ^[^:]+$ type: string @@ -10753,6 +10756,272 @@ spec: description: The rgw pod info nullable: true properties: + additionalVolumeMounts: + description: |- + AdditionalVolumeMounts allows additional volumes to be mounted to the RGW pod. + The root directory for each additional volume mount is `/var/rgw`. + Example: for an additional mount at subPath `ldap`, mounted from a secret that has key + `bindpass.secret`, the file would reside at `/var/rgw/ldap/bindpass.secret`. + items: + description: |- + AdditionalVolumeMount represents the source from where additional files in pod containers + should come from and what subdirectory they are made available in. + properties: + subPath: + description: |- + SubPath defines the sub-path (subdirectory) of the directory root where the volumeSource will + be mounted. All files/keys in the volume source's volume will be mounted to the subdirectory. + This is not the same as the Kubernetes `subPath` volume mount option. + Each subPath definition must be unique and must not contain ':'. + minLength: 1 + pattern: ^[^:]+$ + type: string + volumeSource: + properties: + configMap: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + projected: + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + secret: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + type: object + required: + - subPath + - volumeSource + type: object + type: array annotations: additionalProperties: type: string diff --git a/deploy/examples/crds.yaml b/deploy/examples/crds.yaml index 03d4288cd..597826229 100644 --- a/deploy/examples/crds.yaml +++ b/deploy/examples/crds.yaml @@ -8990,16 +8990,19 @@ spec: additionalFiles: description: |- AdditionalFiles defines any number of additional files that should be mounted into the SSSD - sidecar. These files may be referenced by the sssd.conf config file. + sidecar with a directory root of `/etc/sssd/rook-additional/`. + These files may be referenced by the sssd.conf config file. items: description: |- - SSSDSidecarAdditionalFile represents the source from where additional files for the the SSSD - configuration should come from and are made available. + AdditionalVolumeMount represents the source from where additional files in pod containers + should come from and what subdirectory they are made available in. properties: subPath: description: |- - SubPath defines the sub-path in `/etc/sssd/rook-additional/` where the additional file(s) - will be placed. Each subPath definition must be unique and must not contain ':'. + SubPath defines the sub-path (subdirectory) of the directory root where the volumeSource will + be mounted. All files/keys in the volume source's volume will be mounted to the subdirectory. + This is not the same as the Kubernetes `subPath` volume mount option. + Each subPath definition must be unique and must not contain ':'. minLength: 1 pattern: ^[^:]+$ type: string @@ -10744,6 +10747,272 @@ spec: description: The rgw pod info nullable: true properties: + additionalVolumeMounts: + description: |- + AdditionalVolumeMounts allows additional volumes to be mounted to the RGW pod. + The root directory for each additional volume mount is `/var/rgw`. + Example: for an additional mount at subPath `ldap`, mounted from a secret that has key + `bindpass.secret`, the file would reside at `/var/rgw/ldap/bindpass.secret`. + items: + description: |- + AdditionalVolumeMount represents the source from where additional files in pod containers + should come from and what subdirectory they are made available in. + properties: + subPath: + description: |- + SubPath defines the sub-path (subdirectory) of the directory root where the volumeSource will + be mounted. All files/keys in the volume source's volume will be mounted to the subdirectory. + This is not the same as the Kubernetes `subPath` volume mount option. + Each subPath definition must be unique and must not contain ':'. + minLength: 1 + pattern: ^[^:]+$ + type: string + volumeSource: + properties: + configMap: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + emptyDir: + properties: + medium: + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + hostPath: + properties: + path: + type: string + type: + type: string + required: + - path + type: object + persistentVolumeClaim: + properties: + claimName: + type: string + readOnly: + type: boolean + required: + - claimName + type: object + projected: + properties: + defaultMode: + format: int32 + type: integer + sources: + items: + properties: + clusterTrustBundle: + properties: + labelSelector: + properties: + matchExpressions: + items: + properties: + key: + type: string + operator: + type: string + values: + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + type: object + type: object + x-kubernetes-map-type: atomic + name: + type: string + optional: + type: boolean + path: + type: string + signerName: + type: string + required: + - path + type: object + configMap: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + downwardAPI: + properties: + items: + items: + properties: + fieldRef: + properties: + apiVersion: + type: string + fieldPath: + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + mode: + format: int32 + type: integer + path: + type: string + resourceFieldRef: + properties: + containerName: + type: string + divisor: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + required: + - path + type: object + type: array + x-kubernetes-list-type: atomic + type: object + secret: + properties: + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + name: + default: "" + type: string + optional: + type: boolean + type: object + x-kubernetes-map-type: atomic + serviceAccountToken: + properties: + audience: + type: string + expirationSeconds: + format: int64 + type: integer + path: + type: string + required: + - path + type: object + type: object + type: array + x-kubernetes-list-type: atomic + type: object + secret: + properties: + defaultMode: + format: int32 + type: integer + items: + items: + properties: + key: + type: string + mode: + format: int32 + type: integer + path: + type: string + required: + - key + - path + type: object + type: array + x-kubernetes-list-type: atomic + optional: + type: boolean + secretName: + type: string + type: object + type: object + required: + - subPath + - volumeSource + type: object + type: array annotations: additionalProperties: type: string diff --git a/deploy/examples/object.yaml b/deploy/examples/object.yaml index ba22f3a94..bee795abb 100644 --- a/deploy/examples/object.yaml +++ b/deploy/examples/object.yaml @@ -99,6 +99,14 @@ spec: # cpu: "500m" # memory: "1024Mi" priorityClassName: system-cluster-critical + # # Add arbitrary volume mounts to RGW pods + # additionalVolumeMounts: + # # 'rgw-ldap' secret keys will show up as files in RGW container in dir /var/rgw/ldap + # - subPath: ldap + # volumeSource: + # secret: + # secretName: rgw-ldap + # defaultMode: 0600 #zone: #name: zone-a # service endpoint healthcheck diff --git a/pkg/apis/ceph.rook.io/v1/labels.go b/pkg/apis/ceph.rook.io/v1/labels.go index 6825838f8..d019aba26 100644 --- a/pkg/apis/ceph.rook.io/v1/labels.go +++ b/pkg/apis/ceph.rook.io/v1/labels.go @@ -17,7 +17,10 @@ limitations under the License. package v1 import ( + "strings" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation" ) const ( @@ -138,3 +141,78 @@ func (a Labels) Merge(with Labels) Labels { } return ret } + +// ToValidDNSLabel converts a given string to a valid DNS-1035 spec label. The DNS-1035 spec +// follows the regex '[a-z]([-a-z0-9]*[a-z0-9])?' and is at most 63 chars long. DNS-1035 is used +// over DNS-1123 because it is more strict. Kubernetes docs are not always clear when a DNS_LABEL is +// supposed to be 1035 or 1123 compliant, so we use the more strict version for ease of use. +// - Any input symbol that is not valid is converted to a dash ('-'). +// - Multiple resultant dashes in a row are compressed to a single dash. +// - If the starting character is a number, a 'd' is prepended to preserve the number. +// - Any non-alphanumeric starting or ending characters are removed. +// - If the resultant string is longer than the maximum-allowed 63 characters], characters are +// removed from the middle and replaced with a double dash ('--') to reduce the string to 63 +// characters. +func ToValidDNSLabel(input string) string { + maxl := validation.DNS1035LabelMaxLength + + if input == "" { + return "" + } + + outbuf := make([]byte, len(input)+1) + j := 0 // position in output buffer + last := byte('-') + for _, c := range []byte(input) { + switch { + case c >= 'a' && c <= 'z': + outbuf[j] = c + case c >= '0' && c <= '9': + // if the first char is a number, add a 'd' (for decimal) in front + if j == 0 { + outbuf[j] = 'd' // for decimal + j++ + } + outbuf[j] = c + case c >= 'A' && c <= 'Z': + // convert to lower case + outbuf[j] = c - 'A' + 'a' // convert to lower case + default: + if last == '-' { + // don't write two dashes in a row + continue + } + outbuf[j] = byte('-') + } + last = outbuf[j] + j++ + } + + // set the length of the output buffer to the number of chars we copied to it so there aren't + // \0x00 chars at the end + outbuf = outbuf[:j] + + // trim any leading or trailing dashes + out := strings.Trim(string(outbuf), "-") + + // if string is longer than max length, cut content from the middle to get it to length + if len(out) > maxl { + out = cutMiddle(out, maxl) + } + + return out +} + +// don't use this function for anything less than toSize=4 chars long +func cutMiddle(input string, toSize int) string { + if len(input) <= toSize { + return input + } + + lenLeft := toSize / 2 // truncation rounds down the left side + lenRight := toSize/2 + (toSize % 2) // modulo rounds up the right side + + buf := []byte(input) + + return string(buf[:lenLeft-1]) + "--" + string(buf[len(input)-lenRight+1:]) +} diff --git a/pkg/apis/ceph.rook.io/v1/labels_test.go b/pkg/apis/ceph.rook.io/v1/labels_test.go index 86668fb25..a4163e24d 100644 --- a/pkg/apis/ceph.rook.io/v1/labels_test.go +++ b/pkg/apis/ceph.rook.io/v1/labels_test.go @@ -236,3 +236,83 @@ func TestLabelsMerge(t *testing.T) { var empty Labels assert.Equal(t, map[string]string(testLabelsPart3), map[string]string(empty.Merge(testLabelsPart3))) } + +func TestToValidDNSLabel(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + {"empty string", "", ""}, + {"single dash", "-", ""}, + {"multiple dashes", "----", ""}, + {"lc a", "a", "a"}, + {"lc z", "z", "z"}, + {"lc alphabet", "abcdefghijklmnopqrstuvwxyz", "abcdefghijklmnopqrstuvwxyz"}, + {"UC A", "A", "a"}, + {"UC Z", "Z", "z"}, + {"UC ALPHABET", "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"}, + {"mixed case AlPhAbEt", "AbCdEfGhIjKlMnOpQrStUvWxYz", "abcdefghijklmnopqrstuvwxyz"}, + {"single 0", "0", "d0"}, + {"single 9", "9", "d9"}, + {"single 1", "1", "d1"}, + {"numbers", "01234567890", "d01234567890"}, + {"letters with numbers", "1a0b1c2d3e4f5g6h7i8j9k0", "d1a0b1c2d3e4f5g6h7i8j9k0"}, + {"single / symbol", "/", ""}, + {"single : symbol", ":", ""}, + {"single . symbol", ".", ""}, + {"bunch of symbols", "`~!@#$%^&*()_+-={}[]\\|;':\",.<>/?", ""}, + {"alphabet with symbols", + "a~b!c@d#e$f^g&h*i(j)k_l-m+n+o[p]q{r}s|t:u;v'wz", "a-b-c-d-e-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z"}, + {"multiple symbols between letters", "a//b//c", "a-b-c"}, + {"symbol before", "/a/b/c", "a-b-c"}, + {"symbol after", "a/b/c/", "a-b-c"}, + {"symbols before and after", "/a/b/c/", "a-b-c"}, + {"multiple symbols before after between", "//a//b//c//", "a-b-c"}, + {"mix of all tests except length", "//1a//B-c/d_f/../00-thing.ini/", "d1a-b-c-d-f-00-thing-ini"}, + {"too long input -> middle trim", + "qwertyuiopqwertyuiopqwertyuiopaaqwertyuiopqwertyuiopqwertyuiopaa", + "qwertyuiopqwertyuiopqwertyuiop--wertyuiopqwertyuiopqwertyuiopaa"}, + {"too long input but symbols allow for no middle trim", + "/qwertyuiopqwerty/uiopqwertyuiop//qwertyuiopqwerty/uiopqwertyuiop/", + "qwertyuiopqwerty-uiopqwertyuiop-qwertyuiopqwerty-uiopqwertyuiop"}, + {"max allowed length but starts with number -> middle trim", + "123qwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiop", + "d123qwertyuiopqwertyuiopqwerty--pqwertyuiopqwertyuiopqwertyuiop"}, + {"max allowed length ok", + "qwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiop123", + "qwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiop123"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, ToValidDNSLabel(tt.input)) + }) + } +} + +func Test_cutMiddle(t *testing.T) { + // not an exported function, so don't bother with extreme cases like 0, 1, 2, or 3 len inputs + t.Run("len 8 -> 6", func(t *testing.T) { + assert.Equal(t, "ab--gh", cutMiddle("abcdefgh", 6)) + }) + t.Run("len 9 -> 6", func(t *testing.T) { + assert.Equal(t, "ab--hi", cutMiddle("abcdefghi", 6)) + }) + t.Run("len 9 -> 7", func(t *testing.T) { + assert.Equal(t, "ab--ghi", cutMiddle("abcdefghi", 7)) + }) + t.Run("len 10 -> 10", func(t *testing.T) { + assert.Equal(t, "qwertyuiop", cutMiddle("qwertyuiop", 10)) + }) + // below is what we really want to test + t.Run("len 63 -> 63", func(t *testing.T) { + assert.Equal(t, + "qwertyuiopqwertyuiopqwertyuiop12qwertyuiopqwertyuiopqwertyuiop1", + cutMiddle("qwertyuiopqwertyuiopqwertyuiop12qwertyuiopqwertyuiopqwertyuiop1", 63)) + }) + t.Run("len 64 -> 63", func(t *testing.T) { + assert.Equal(t, + "qwertyuiopqwertyuiopqwertyuiop--wertyuiopqwertyuiopqwertyuiop12", + cutMiddle("qwertyuiopqwertyuiopqwertyuiop12qwertyuiopqwertyuiopqwertyuiop12", 63)) + }) +} diff --git a/pkg/apis/ceph.rook.io/v1/nfs_test.go b/pkg/apis/ceph.rook.io/v1/nfs_test.go index b2ba10c8f..7591760e2 100644 --- a/pkg/apis/ceph.rook.io/v1/nfs_test.go +++ b/pkg/apis/ceph.rook.io/v1/nfs_test.go @@ -92,7 +92,7 @@ func TestNFSSecuritySpec_Validate(t *testing.T) { withSSSD(&SSSDSpec{ Sidecar: &SSSDSidecar{ Image: "myimage", - AdditionalFiles: []SSSDSidecarAdditionalFile{}, + AdditionalFiles: AdditionalVolumeMounts{}, }, }), isOkay}, @@ -100,7 +100,7 @@ func TestNFSSecuritySpec_Validate(t *testing.T) { withSSSD(&SSSDSpec{ Sidecar: &SSSDSidecar{ Image: "myimage", - AdditionalFiles: []SSSDSidecarAdditionalFile{ + AdditionalFiles: AdditionalVolumeMounts{ {SubPath: "one", VolumeSource: configMapVolumeSource}, {SubPath: "two", VolumeSource: configMapVolumeSource}, {SubPath: "three", VolumeSource: configMapVolumeSource}, @@ -112,7 +112,7 @@ func TestNFSSecuritySpec_Validate(t *testing.T) { withSSSD(&SSSDSpec{ Sidecar: &SSSDSidecar{ Image: "myimage", - AdditionalFiles: []SSSDSidecarAdditionalFile{ + AdditionalFiles: AdditionalVolumeMounts{ {SubPath: "one", VolumeSource: configMapVolumeSource}, {SubPath: "", VolumeSource: configMapVolumeSource}, {SubPath: "three", VolumeSource: configMapVolumeSource}, @@ -124,7 +124,7 @@ func TestNFSSecuritySpec_Validate(t *testing.T) { withSSSD(&SSSDSpec{ Sidecar: &SSSDSidecar{ Image: "myimage", - AdditionalFiles: []SSSDSidecarAdditionalFile{ + AdditionalFiles: AdditionalVolumeMounts{ {SubPath: "one", VolumeSource: configMapVolumeSource}, {SubPath: "two", VolumeSource: configMapVolumeSource}, {SubPath: "one", VolumeSource: configMapVolumeSource}, @@ -136,7 +136,7 @@ func TestNFSSecuritySpec_Validate(t *testing.T) { withSSSD(&SSSDSpec{ Sidecar: &SSSDSidecar{ Image: "myimage", - AdditionalFiles: []SSSDSidecarAdditionalFile{ + AdditionalFiles: AdditionalVolumeMounts{ {SubPath: "one", VolumeSource: configMapVolumeSource}, {SubPath: "", VolumeSource: &ConfigFileVolumeSource{}}, {SubPath: "three", VolumeSource: configMapVolumeSource}, diff --git a/pkg/apis/ceph.rook.io/v1/types.go b/pkg/apis/ceph.rook.io/v1/types.go index 12a756231..380c1f3cd 100755 --- a/pkg/apis/ceph.rook.io/v1/types.go +++ b/pkg/apis/ceph.rook.io/v1/types.go @@ -1637,6 +1637,12 @@ type GatewaySpec struct { // +nullable // +optional DashboardEnabled *bool `json:"dashboardEnabled,omitempty"` + + // AdditionalVolumeMounts allows additional volumes to be mounted to the RGW pod. + // The root directory for each additional volume mount is `/var/rgw`. + // Example: for an additional mount at subPath `ldap`, mounted from a secret that has key + // `bindpass.secret`, the file would reside at `/var/rgw/ldap/bindpass.secret`. + AdditionalVolumeMounts AdditionalVolumeMounts `json:"additionalVolumeMounts,omitempty"` } // EndpointAddress is a tuple that describes a single IP address or host name. This is a subset of @@ -2477,9 +2483,10 @@ type SSSDSidecar struct { SSSDConfigFile SSSDSidecarConfigFile `json:"sssdConfigFile"` // AdditionalFiles defines any number of additional files that should be mounted into the SSSD - // sidecar. These files may be referenced by the sssd.conf config file. + // sidecar with a directory root of `/etc/sssd/rook-additional/`. + // These files may be referenced by the sssd.conf config file. // +optional - AdditionalFiles []SSSDSidecarAdditionalFile `json:"additionalFiles,omitempty"` + AdditionalFiles AdditionalVolumeMounts `json:"additionalFiles,omitempty"` // Resources allow specifying resource requests/limits on the SSSD sidecar container. // +optional @@ -2507,11 +2514,13 @@ type SSSDSidecarConfigFile struct { VolumeSource *ConfigFileVolumeSource `json:"volumeSource,omitempty"` } -// SSSDSidecarAdditionalFile represents the source from where additional files for the the SSSD -// configuration should come from and are made available. -type SSSDSidecarAdditionalFile struct { - // SubPath defines the sub-path in `/etc/sssd/rook-additional/` where the additional file(s) - // will be placed. Each subPath definition must be unique and must not contain ':'. +// AdditionalVolumeMount represents the source from where additional files in pod containers +// should come from and what subdirectory they are made available in. +type AdditionalVolumeMount struct { + // SubPath defines the sub-path (subdirectory) of the directory root where the volumeSource will + // be mounted. All files/keys in the volume source's volume will be mounted to the subdirectory. + // This is not the same as the Kubernetes `subPath` volume mount option. + // Each subPath definition must be unique and must not contain ':'. // +kubebuilder:validation:Required // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:Pattern=`^[^:]+$` @@ -2520,12 +2529,14 @@ type SSSDSidecarAdditionalFile struct { // VolumeSource accepts a pared down version of the standard Kubernetes VolumeSource for the // additional file(s) like what is normally used to configure Volumes for a Pod. Fore example, a // ConfigMap, Secret, or HostPath. Each VolumeSource adds one or more additional files to the - // SSSD sidecar container in the `/etc/sssd/rook-additional/` directory. - // Be aware that some files may need to have a specific file mode like 0600 due to requirements - // by SSSD for some files. For example, CA or TLS certificates. + // container `/` directory. + // Be aware that some files may need to have a specific file mode like 0600 due to application + // requirements. For example, CA or TLS certificates. VolumeSource *ConfigFileVolumeSource `json:"volumeSource"` } +type AdditionalVolumeMounts []AdditionalVolumeMount + // NetworkSpec for Ceph includes backward compatibility code // +kubebuilder:validation:XValidation:message="at least one network selector must be specified when using multus",rule="!has(self.provider) || (self.provider != 'multus' || (self.provider == 'multus' && size(self.selectors) > 0))" // +kubebuilder:validation:XValidation:message=`the legacy hostNetwork setting can only be set if the network.provider is set to the empty string`,rule=`!has(self.hostNetwork) || self.hostNetwork == false || !has(self.provider) || self.provider == ""` diff --git a/pkg/apis/ceph.rook.io/v1/volume.go b/pkg/apis/ceph.rook.io/v1/volume.go index 9acb4d8e1..d713d976b 100644 --- a/pkg/apis/ceph.rook.io/v1/volume.go +++ b/pkg/apis/ceph.rook.io/v1/volume.go @@ -17,9 +17,11 @@ limitations under the License. package v1 import ( + "path/filepath" "reflect" corev1 "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" ) func (src *ConfigFileVolumeSource) ToKubernetesVolumeSource() *corev1.VolumeSource { @@ -49,6 +51,28 @@ func (src *ConfigFileVolumeSource) ToKubernetesVolumeSource() *corev1.VolumeSour return dst } +// GenerateVolumesAndMounts converts Rook's AdditionalVolumeMounts type to a list of volumes and +// corresponding mounts that can be added to Kubernetes pod specs. +func (v *AdditionalVolumeMounts) GenerateVolumesAndMounts(rootDir string) ([]v1.Volume, []v1.VolumeMount) { + vols := []v1.Volume{} + mounts := []v1.VolumeMount{} + + for _, addVolMnt := range *v { + mountPath := filepath.Join(rootDir, addVolMnt.SubPath) + volName := ToValidDNSLabel(mountPath) + vols = append(vols, v1.Volume{ + Name: volName, + VolumeSource: *addVolMnt.VolumeSource.ToKubernetesVolumeSource(), + }) + mounts = append(mounts, v1.VolumeMount{ + Name: volName, + MountPath: mountPath, + }) + } + + return vols, mounts +} + func (t *VolumeClaimTemplate) ToPVC() *corev1.PersistentVolumeClaim { if t == nil { return nil diff --git a/pkg/apis/ceph.rook.io/v1/zz_generated.deepcopy.go b/pkg/apis/ceph.rook.io/v1/zz_generated.deepcopy.go index fc927f636..bd6bf6821 100644 --- a/pkg/apis/ceph.rook.io/v1/zz_generated.deepcopy.go +++ b/pkg/apis/ceph.rook.io/v1/zz_generated.deepcopy.go @@ -43,6 +43,49 @@ func (in *AMQPEndpointSpec) DeepCopy() *AMQPEndpointSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AdditionalVolumeMount) DeepCopyInto(out *AdditionalVolumeMount) { + *out = *in + if in.VolumeSource != nil { + in, out := &in.VolumeSource, &out.VolumeSource + *out = new(ConfigFileVolumeSource) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalVolumeMount. +func (in *AdditionalVolumeMount) DeepCopy() *AdditionalVolumeMount { + if in == nil { + return nil + } + out := new(AdditionalVolumeMount) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in AdditionalVolumeMounts) DeepCopyInto(out *AdditionalVolumeMounts) { + { + in := &in + *out = make(AdditionalVolumeMounts, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdditionalVolumeMounts. +func (in AdditionalVolumeMounts) DeepCopy() AdditionalVolumeMounts { + if in == nil { + return nil + } + out := new(AdditionalVolumeMounts) + in.DeepCopyInto(out) + return *out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AddressRangesSpec) DeepCopyInto(out *AddressRangesSpec) { *out = *in @@ -2645,6 +2688,13 @@ func (in *GatewaySpec) DeepCopyInto(out *GatewaySpec) { *out = new(bool) **out = **in } + if in.AdditionalVolumeMounts != nil { + in, out := &in.AdditionalVolumeMounts, &out.AdditionalVolumeMounts + *out = make(AdditionalVolumeMounts, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -4315,7 +4365,7 @@ func (in *SSSDSidecar) DeepCopyInto(out *SSSDSidecar) { in.SSSDConfigFile.DeepCopyInto(&out.SSSDConfigFile) if in.AdditionalFiles != nil { in, out := &in.AdditionalFiles, &out.AdditionalFiles - *out = make([]SSSDSidecarAdditionalFile, len(*in)) + *out = make(AdditionalVolumeMounts, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -4334,27 +4384,6 @@ func (in *SSSDSidecar) DeepCopy() *SSSDSidecar { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SSSDSidecarAdditionalFile) DeepCopyInto(out *SSSDSidecarAdditionalFile) { - *out = *in - if in.VolumeSource != nil { - in, out := &in.VolumeSource, &out.VolumeSource - *out = new(ConfigFileVolumeSource) - (*in).DeepCopyInto(*out) - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SSSDSidecarAdditionalFile. -func (in *SSSDSidecarAdditionalFile) DeepCopy() *SSSDSidecarAdditionalFile { - if in == nil { - return nil - } - out := new(SSSDSidecarAdditionalFile) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SSSDSidecarConfigFile) DeepCopyInto(out *SSSDSidecarConfigFile) { *out = *in diff --git a/pkg/operator/ceph/nfs/security.go b/pkg/operator/ceph/nfs/security.go index 7d859858c..96a1e3dc2 100644 --- a/pkg/operator/ceph/nfs/security.go +++ b/pkg/operator/ceph/nfs/security.go @@ -18,10 +18,8 @@ package nfs import ( "fmt" - "path/filepath" cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1" - "github.com/rook/rook/pkg/operator/k8sutil" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" ) @@ -144,7 +142,7 @@ func generateSssdSidecarResources(nfs *cephv1.CephNFS, sidecarCfg *cephv1.SSSDSi sssdMounts = append(sssdMounts, mount) } - genericVols, genericMounts := generateGenericFileVolsAndMounts(sidecarCfg.AdditionalFiles) + genericVols, genericMounts := sidecarCfg.AdditionalFiles.GenerateVolumesAndMounts("/etc/sssd/rook-additional/") volumes = append(volumes, genericVols...) sssdMounts = append(sssdMounts, genericMounts...) @@ -293,26 +291,6 @@ func sssdConfigVolAndMount(volSource v1.VolumeSource) (v1.Volume, v1.VolumeMount return vol, mount } -func generateGenericFileVolsAndMounts(additionalFiles []cephv1.SSSDSidecarAdditionalFile) ([]v1.Volume, []v1.VolumeMount) { - vols := []v1.Volume{} - mounts := []v1.VolumeMount{} - - for _, additionalFile := range additionalFiles { - mountPath := filepath.Join("/etc/sssd/rook-additional/", additionalFile.SubPath) - volName := k8sutil.ToValidDNSLabel(mountPath) - vols = append(vols, v1.Volume{ - Name: volName, - VolumeSource: *additionalFile.VolumeSource.ToKubernetesVolumeSource(), - }) - mounts = append(mounts, v1.VolumeMount{ - Name: volName, - MountPath: mountPath, - }) - } - - return vols, mounts -} - func generateSssdNsswitchConfResources(r *ReconcileCephNFS, nfs *cephv1.CephNFS) (*v1.Container, *v1.Volume, *v1.VolumeMount) { volName := "nsswitch-conf" diff --git a/pkg/operator/ceph/object/spec.go b/pkg/operator/ceph/object/spec.go index 701042d95..cd34dab8d 100644 --- a/pkg/operator/ceph/object/spec.go +++ b/pkg/operator/ceph/object/spec.go @@ -254,6 +254,10 @@ func (c *clusterConfig) makeRGWPodSpec(rgwConfig *rgwConfig) (v1.PodTemplateSpec } } + addVols, addMounts := c.store.Spec.Gateway.AdditionalVolumeMounts.GenerateVolumesAndMounts("/var/rgw/") + podTemplateSpec.Spec.Volumes = append(podTemplateSpec.Spec.Volumes, addVols...) + podTemplateSpec.Spec.Containers[0].VolumeMounts = append(podTemplateSpec.Spec.Containers[0].VolumeMounts, addMounts...) + return podTemplateSpec, nil } diff --git a/pkg/operator/ceph/object/spec_test.go b/pkg/operator/ceph/object/spec_test.go index e13df8265..aeeaa3681 100644 --- a/pkg/operator/ceph/object/spec_test.go +++ b/pkg/operator/ceph/object/spec_test.go @@ -260,6 +260,34 @@ func TestSSLPodSpec(t *testing.T) { assert.True(t, s.Spec.HostNetwork) assert.Equal(t, v1.DNSClusterFirstWithHostNet, s.Spec.DNSPolicy) + + // add user-defined volume mount + c.store.Spec.Gateway.AdditionalVolumeMounts = cephv1.AdditionalVolumeMounts{ + { + SubPath: "ldap", + VolumeSource: &cephv1.ConfigFileVolumeSource{ + Secret: &v1.SecretVolumeSource{ + SecretName: "my-rgw-ldap-secret", + }, + }, + }, + } + s, err = c.makeRGWPodSpec(rgwConfig) + assert.NoError(t, err) + podTemplate = cephtest.NewPodTemplateSpecTester(t, &s) // checks that vols have corresponding mounts + podTemplate.RunFullSuite(cephconfig.RgwType, "default", "rook-ceph-rgw", "mycluster", "quay.io/ceph/ceph:myversion", + "200", "100", "1337", "500", /* resources */ + "my-priority-class", "default", "cephobjectstores.ceph.rook.io", "ceph-rgw") + assert.True(t, hasSecretVolWithName(s.Spec.Volumes, "my-rgw-ldap-secret")) +} + +func hasSecretVolWithName(vols []v1.Volume, secretName string) bool { + for _, v := range vols { + if v.Secret != nil && v.Secret.SecretName == secretName { + return true + } + } + return false } func TestValidateSpec(t *testing.T) { diff --git a/pkg/operator/k8sutil/k8sutil.go b/pkg/operator/k8sutil/k8sutil.go index bc0ac15b2..79aa66dba 100644 --- a/pkg/operator/k8sutil/k8sutil.go +++ b/pkg/operator/k8sutil/k8sutil.go @@ -192,78 +192,3 @@ func validateLabelValue(value string) string { } return sanitized } - -// ToValidDNSLabel converts a given string to a valid DNS-1035 spec label. The DNS-1035 spec -// follows the regex '[a-z]([-a-z0-9]*[a-z0-9])?' and is at most 63 chars long. DNS-1035 is used -// over DNS-1123 because it is more strict. Kubernetes docs are not always clear when a DNS_LABEL is -// supposed to be 1035 or 1123 compliant, so we use the more strict version for ease of use. -// - Any input symbol that is not valid is converted to a dash ('-'). -// - Multiple resultant dashes in a row are compressed to a single dash. -// - If the starting character is a number, a 'd' is prepended to preserve the number. -// - Any non-alphanumeric starting or ending characters are removed. -// - If the resultant string is longer than the maximum-allowed 63 characters], characters are -// removed from the middle and replaced with a double dash ('--') to reduce the string to 63 -// characters. -func ToValidDNSLabel(input string) string { - maxl := validation.DNS1035LabelMaxLength - - if input == "" { - return "" - } - - outbuf := make([]byte, len(input)+1) - j := 0 // position in output buffer - last := byte('-') - for _, c := range []byte(input) { - switch { - case c >= 'a' && c <= 'z': - outbuf[j] = c - case c >= '0' && c <= '9': - // if the first char is a number, add a 'd' (for decimal) in front - if j == 0 { - outbuf[j] = 'd' // for decimal - j++ - } - outbuf[j] = c - case c >= 'A' && c <= 'Z': - // convert to lower case - outbuf[j] = c - 'A' + 'a' // convert to lower case - default: - if last == '-' { - // don't write two dashes in a row - continue - } - outbuf[j] = byte('-') - } - last = outbuf[j] - j++ - } - - // set the length of the output buffer to the number of chars we copied to it so there aren't - // \0x00 chars at the end - outbuf = outbuf[:j] - - // trim any leading or trailing dashes - out := strings.Trim(string(outbuf), "-") - - // if string is longer than max length, cut content from the middle to get it to length - if len(out) > maxl { - out = cutMiddle(out, maxl) - } - - return out -} - -// don't use this function for anything less than toSize=4 chars long -func cutMiddle(input string, toSize int) string { - if len(input) <= toSize { - return input - } - - lenLeft := toSize / 2 // truncation rounds down the left side - lenRight := toSize/2 + (toSize % 2) // modulo rounds up the right side - - buf := []byte(input) - - return string(buf[:lenLeft-1]) + "--" + string(buf[len(input)-lenRight+1:]) -} diff --git a/pkg/operator/k8sutil/k8sutil_test.go b/pkg/operator/k8sutil/k8sutil_test.go index a4d2a0eff..173d83f44 100644 --- a/pkg/operator/k8sutil/k8sutil_test.go +++ b/pkg/operator/k8sutil/k8sutil_test.go @@ -88,83 +88,3 @@ func TestValidateLabelValue(t *testing.T) { assert.Equal(t, result, validateLabelValue(input)) } } - -func TestToValidDNSLabel(t *testing.T) { - tests := []struct { - name string - input string - want string - }{ - {"empty string", "", ""}, - {"single dash", "-", ""}, - {"multiple dashes", "----", ""}, - {"lc a", "a", "a"}, - {"lc z", "z", "z"}, - {"lc alphabet", "abcdefghijklmnopqrstuvwxyz", "abcdefghijklmnopqrstuvwxyz"}, - {"UC A", "A", "a"}, - {"UC Z", "Z", "z"}, - {"UC ALPHABET", "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"}, - {"mixed case AlPhAbEt", "AbCdEfGhIjKlMnOpQrStUvWxYz", "abcdefghijklmnopqrstuvwxyz"}, - {"single 0", "0", "d0"}, - {"single 9", "9", "d9"}, - {"single 1", "1", "d1"}, - {"numbers", "01234567890", "d01234567890"}, - {"letters with numbers", "1a0b1c2d3e4f5g6h7i8j9k0", "d1a0b1c2d3e4f5g6h7i8j9k0"}, - {"single / symbol", "/", ""}, - {"single : symbol", ":", ""}, - {"single . symbol", ".", ""}, - {"bunch of symbols", "`~!@#$%^&*()_+-={}[]\\|;':\",.<>/?", ""}, - {"alphabet with symbols", - "a~b!c@d#e$f^g&h*i(j)k_l-m+n+o[p]q{r}s|t:u;v'wz", "a-b-c-d-e-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z"}, - {"multiple symbols between letters", "a//b//c", "a-b-c"}, - {"symbol before", "/a/b/c", "a-b-c"}, - {"symbol after", "a/b/c/", "a-b-c"}, - {"symbols before and after", "/a/b/c/", "a-b-c"}, - {"multiple symbols before after between", "//a//b//c//", "a-b-c"}, - {"mix of all tests except length", "//1a//B-c/d_f/../00-thing.ini/", "d1a-b-c-d-f-00-thing-ini"}, - {"too long input -> middle trim", - "qwertyuiopqwertyuiopqwertyuiopaaqwertyuiopqwertyuiopqwertyuiopaa", - "qwertyuiopqwertyuiopqwertyuiop--wertyuiopqwertyuiopqwertyuiopaa"}, - {"too long input but symbols allow for no middle trim", - "/qwertyuiopqwerty/uiopqwertyuiop//qwertyuiopqwerty/uiopqwertyuiop/", - "qwertyuiopqwerty-uiopqwertyuiop-qwertyuiopqwerty-uiopqwertyuiop"}, - {"max allowed length but starts with number -> middle trim", - "123qwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiop", - "d123qwertyuiopqwertyuiopqwerty--pqwertyuiopqwertyuiopqwertyuiop"}, - {"max allowed length ok", - "qwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiop123", - "qwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiopqwertyuiop123"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - assert.Equal(t, tt.want, ToValidDNSLabel(tt.input)) - }) - } -} - -func Test_cutMiddle(t *testing.T) { - // not an exported function, so don't bother with extreme cases like 0, 1, 2, or 3 len inputs - t.Run("len 8 -> 6", func(t *testing.T) { - assert.Equal(t, "ab--gh", cutMiddle("abcdefgh", 6)) - }) - t.Run("len 9 -> 6", func(t *testing.T) { - assert.Equal(t, "ab--hi", cutMiddle("abcdefghi", 6)) - }) - t.Run("len 9 -> 7", func(t *testing.T) { - assert.Equal(t, "ab--ghi", cutMiddle("abcdefghi", 7)) - }) - t.Run("len 10 -> 10", func(t *testing.T) { - assert.Equal(t, "qwertyuiop", cutMiddle("qwertyuiop", 10)) - }) - // below is what we really want to test - t.Run("len 63 -> 63", func(t *testing.T) { - assert.Equal(t, - "qwertyuiopqwertyuiopqwertyuiop12qwertyuiopqwertyuiopqwertyuiop1", - cutMiddle("qwertyuiopqwertyuiopqwertyuiop12qwertyuiopqwertyuiopqwertyuiop1", 63)) - }) - t.Run("len 64 -> 63", func(t *testing.T) { - assert.Equal(t, - "qwertyuiopqwertyuiopqwertyuiop--wertyuiopqwertyuiopqwertyuiop12", - cutMiddle("qwertyuiopqwertyuiopqwertyuiop12qwertyuiopqwertyuiopqwertyuiop12", 63)) - }) -}