strfmt.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. /*
  2. Copyright 2025 The Kubernetes Authors.
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. http://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package validate
  14. import (
  15. "context"
  16. "fmt"
  17. "strings"
  18. "k8s.io/apimachinery/pkg/api/operation"
  19. "k8s.io/apimachinery/pkg/api/validate/content"
  20. "k8s.io/apimachinery/pkg/util/validation/field"
  21. )
  22. const (
  23. uuidErrorMessage = "must be a lowercase UUID in 8-4-4-4-12 format"
  24. defaultResourceRequestsPrefix = "requests."
  25. // Default namespace prefix.
  26. resourceDefaultNamespacePrefix = "kubernetes.io/"
  27. resourceDeviceMaxLength = 32
  28. )
  29. // ShortName verifies that the specified value is a valid "short name"
  30. // (sometimes known as a "DNS label").
  31. // - must not be empty
  32. // - must be less than 64 characters long
  33. // - must start and end with lower-case alphanumeric characters
  34. // - must contain only lower-case alphanumeric characters or dashes
  35. //
  36. // All errors returned by this function will be "invalid" type errors. If the
  37. // caller wants better errors, it must take responsibility for checking things
  38. // like required/optional and max-length.
  39. func ShortName[T ~string](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
  40. if value == nil {
  41. return nil
  42. }
  43. var allErrs field.ErrorList
  44. for _, msg := range content.IsDNS1123Label((string)(*value)) {
  45. allErrs = append(allErrs, field.Invalid(fldPath, *value, msg).WithOrigin("format=k8s-short-name"))
  46. }
  47. return allErrs
  48. }
  49. // LongName verifies that the specified value is a valid "long name"
  50. // (sometimes known as a "DNS subdomain").
  51. // - must not be empty
  52. // - must be less than 254 characters long
  53. // - each element must start and end with lower-case alphanumeric characters
  54. // - each element must contain only lower-case alphanumeric characters or dashes
  55. //
  56. // All errors returned by this function will be "invalid" type errors. If the
  57. // caller wants better errors, it must take responsibility for checking things
  58. // like required/optional and max-length.
  59. func LongName[T ~string](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
  60. if value == nil {
  61. return nil
  62. }
  63. var allErrs field.ErrorList
  64. for _, msg := range content.IsDNS1123Subdomain((string)(*value)) {
  65. allErrs = append(allErrs, field.Invalid(fldPath, *value, msg).WithOrigin("format=k8s-long-name"))
  66. }
  67. return allErrs
  68. }
  69. // LabelKey verifies that the specified value is a valid label key.
  70. // A label key is composed of an optional prefix and a name, separated by a '/'.
  71. // The name part is required and must:
  72. // - be 63 characters or less
  73. // - begin and end with an alphanumeric character ([a-z0-9A-Z])
  74. // - contain only alphanumeric characters, dashes (-), underscores (_), or dots (.)
  75. //
  76. // The prefix is optional and must:
  77. // - be a DNS subdomain
  78. // - be no more than 253 characters
  79. func LabelKey[T ~string](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
  80. if value == nil {
  81. return nil
  82. }
  83. var allErrs field.ErrorList
  84. for _, msg := range content.IsLabelKey((string)(*value)) {
  85. allErrs = append(allErrs, field.Invalid(fldPath, *value, msg).WithOrigin("format=k8s-label-key"))
  86. }
  87. return allErrs
  88. }
  89. // LongNameCaseless verifies that the specified value is a valid "long name"
  90. // (sometimes known as a "DNS subdomain"), but is case-insensitive.
  91. // - must not be empty
  92. // - must be less than 254 characters long
  93. // - each element must start and end with alphanumeric characters
  94. // - each element must contain only alphanumeric characters or dashes
  95. //
  96. // Deprecated: Case-insensitive names are not recommended as they can lead to ambiguity
  97. // (e.g., 'Foo', 'FOO', and 'foo' would be allowed names for foo). Use LongName for strict, lowercase validation.
  98. func LongNameCaseless[T ~string](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
  99. if value == nil {
  100. return nil
  101. }
  102. var allErrs field.ErrorList
  103. for _, msg := range content.IsDNS1123SubdomainCaseless((string)(*value)) {
  104. allErrs = append(allErrs, field.Invalid(fldPath, *value, msg).WithOrigin("format=k8s-long-name-caseless"))
  105. }
  106. return allErrs
  107. }
  108. // LabelValue verifies that the specified value is a valid label value.
  109. // - can be empty
  110. // - must be no more than 63 characters
  111. // - must start and end with alphanumeric characters
  112. // - must contain only alphanumeric characters, dashes, underscores, or dots
  113. func LabelValue[T ~string](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
  114. if value == nil {
  115. return nil
  116. }
  117. var allErrs field.ErrorList
  118. for _, msg := range content.IsLabelValue((string)(*value)) {
  119. allErrs = append(allErrs, field.Invalid(fldPath, *value, msg).WithOrigin("format=k8s-label-value"))
  120. }
  121. return allErrs
  122. }
  123. // UUID verifies that the specified value is a valid UUID (RFC 4122).
  124. // - must be 36 characters long
  125. // - must be in the normalized form `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`
  126. // - must use only lowercase hexadecimal characters
  127. func UUID[T ~string](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
  128. if value == nil {
  129. return nil
  130. }
  131. val := (string)(*value)
  132. if len(val) != 36 {
  133. return field.ErrorList{field.Invalid(fldPath, val, uuidErrorMessage).WithOrigin("format=k8s-uuid")}
  134. }
  135. for idx := 0; idx < len(val); idx++ {
  136. character := val[idx]
  137. switch idx {
  138. case 8, 13, 18, 23:
  139. if character != '-' {
  140. return field.ErrorList{field.Invalid(fldPath, val, uuidErrorMessage).WithOrigin("format=k8s-uuid")}
  141. }
  142. default:
  143. // should be lower case hexadecimal.
  144. if (character < '0' || character > '9') && (character < 'a' || character > 'f') {
  145. return field.ErrorList{field.Invalid(fldPath, val, uuidErrorMessage).WithOrigin("format=k8s-uuid")}
  146. }
  147. }
  148. }
  149. return nil
  150. }
  151. // ResourcePoolName verifies that the specified value is one or more valid "long name"
  152. // parts separated by a '/' and no longer than 253 characters.
  153. func ResourcePoolName[T ~string](ctx context.Context, op operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
  154. if value == nil {
  155. return nil
  156. }
  157. val := (string)(*value)
  158. var allErrs field.ErrorList
  159. if len(val) > 253 {
  160. allErrs = append(allErrs, field.TooLong(fldPath, val, 253))
  161. }
  162. parts := strings.Split(val, "/")
  163. for i, part := range parts {
  164. if len(part) == 0 {
  165. allErrs = append(allErrs, field.Invalid(fldPath, val, fmt.Sprintf("segment %d: must not be empty", i)))
  166. continue
  167. }
  168. // Note that we are overwriting the origin from the underlying LongName validation.
  169. allErrs = append(allErrs, LongName(ctx, op, fldPath, &part, nil).PrefixDetail(fmt.Sprintf("segment %d: ", i))...)
  170. }
  171. return allErrs.WithOrigin("format=k8s-resource-pool-name")
  172. }
  173. // ExtendedResourceName verifies that the specified value is a valid extended resource name.
  174. // An extended resource name is a domain-prefixed name that does not use the "kubernetes.io"
  175. // or "requests." prefixes. Must be a valid label key when appended to "requests.", as in quota.
  176. //
  177. // - must have slash domain and name.
  178. // - must not have the "kubernetes.io" domain
  179. // - must not have the "requests." prefix
  180. // - name must be 63 characters or less
  181. // - must be a valid label key when appended to "requests.", as in quota
  182. // -- must contain only alphanumeric characters, dashes, underscores, or dots
  183. // -- must end with an alphanumeric character
  184. func ExtendedResourceName[T ~string](_ context.Context, op operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
  185. if value == nil {
  186. return nil
  187. }
  188. val := string(*value)
  189. allErrs := field.ErrorList{}
  190. if !strings.Contains(val, "/") {
  191. allErrs = append(allErrs, field.Invalid(fldPath, val, "a name must be a domain-prefixed path, such as 'example.com/my-prop'"))
  192. } else if strings.Contains(val, resourceDefaultNamespacePrefix) {
  193. allErrs = append(allErrs, field.Invalid(fldPath, val, fmt.Sprintf("must not have %q domain", resourceDefaultNamespacePrefix)))
  194. }
  195. // Ensure extended resource is not type of quota.
  196. if strings.HasPrefix(val, defaultResourceRequestsPrefix) {
  197. allErrs = append(allErrs, field.Invalid(fldPath, val, fmt.Sprintf("must not have %q prefix", defaultResourceRequestsPrefix)))
  198. }
  199. // Ensure it satisfies the rules in IsLabelKey() after converted into quota resource name
  200. nameForQuota := fmt.Sprintf("%s%s", defaultResourceRequestsPrefix, val)
  201. for _, msg := range content.IsLabelKey(nameForQuota) {
  202. allErrs = append(allErrs, field.Invalid(fldPath, val, msg))
  203. }
  204. return allErrs.WithOrigin("format=k8s-extended-resource-name")
  205. }
  206. // resourcesQualifiedName verifies that the specified value is a valid Kubernetes resources
  207. // qualified name.
  208. // - must not be empty
  209. // - must be composed of an optional prefix and a name, separated by a slash (e.g., "prefix/name")
  210. // - the prefix, if specified, must be a DNS subdomain
  211. // - the name part must be a C identifier
  212. // - the name part must be no more than 32 characters
  213. func resourcesQualifiedName[T ~string](ctx context.Context, op operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
  214. if value == nil {
  215. return nil
  216. }
  217. var allErrs field.ErrorList
  218. s := string(*value)
  219. parts := strings.Split(s, "/")
  220. // TODO: This validation and the corresponding handwritten validation validateQualifiedName in
  221. // pkg/apis/resource/validation/validation.go are not validating whether there are more than 1
  222. // slash. This should be fixed in both places.
  223. switch len(parts) {
  224. case 1:
  225. allErrs = append(allErrs, validateCIdentifier(parts[0], resourceDeviceMaxLength, fldPath)...)
  226. case 2:
  227. if len(parts[0]) == 0 {
  228. allErrs = append(allErrs, field.Invalid(fldPath, "", "prefix must not be empty"))
  229. } else {
  230. if len(parts[0]) > 63 {
  231. allErrs = append(allErrs, field.TooLong(fldPath, parts[0], 63))
  232. }
  233. allErrs = append(allErrs, LongName(ctx, op, fldPath, &parts[0], nil).PrefixDetail("prefix: ")...)
  234. }
  235. if len(parts[1]) == 0 {
  236. allErrs = append(allErrs, field.Invalid(fldPath, "", "name must not be empty"))
  237. } else {
  238. allErrs = append(allErrs, validateCIdentifier(parts[1], resourceDeviceMaxLength, fldPath)...)
  239. }
  240. }
  241. return allErrs
  242. }
  243. // ResourceFullyQualifiedName verifies that the specified value is a valid Kubernetes
  244. // fully qualified name.
  245. // - must not be empty
  246. // - must be composed of a prefix and a name, separated by a slash (e.g., "prefix/name")
  247. // - the prefix must be a DNS subdomain
  248. // - the name part must be a C identifier
  249. // - the name part must be no more than 32 characters
  250. func ResourceFullyQualifiedName[T ~string](ctx context.Context, op operation.Operation, fldPath *field.Path, value, _ *T) field.ErrorList {
  251. if value == nil {
  252. return nil
  253. }
  254. var allErrs field.ErrorList
  255. s := string(*value)
  256. allErrs = append(allErrs, resourcesQualifiedName(ctx, op, fldPath, &s, nil)...)
  257. if !strings.Contains(s, "/") {
  258. allErrs = append(allErrs, field.Invalid(fldPath, s, "a fully qualified name must be a domain and a name separated by a slash"))
  259. }
  260. return allErrs.WithOrigin("format=k8s-resource-fully-qualified-name")
  261. }
  262. func validateCIdentifier(id string, length int, fldPath *field.Path) field.ErrorList {
  263. var allErrs field.ErrorList
  264. if len(id) > length {
  265. allErrs = append(allErrs, field.TooLong(fldPath, id, length))
  266. }
  267. for _, msg := range content.IsCIdentifier(id) {
  268. allErrs = append(allErrs, field.Invalid(fldPath, id, msg))
  269. }
  270. return allErrs
  271. }