1414// limitations under the License.
1515//===----------------------------------------------------------------------===//
1616
17+ import ContainerizationError
18+ import ContainerizationExtras
19+
1720/// Configuration parameters for network creation.
1821public struct NetworkConfiguration : Codable , Sendable , Identifiable {
1922 /// A unique identifier for the network
@@ -25,14 +28,89 @@ public struct NetworkConfiguration: Codable, Sendable, Identifiable {
2528 /// The preferred CIDR address for the subnet, if specified
2629 public let subnet : String ?
2730
31+ /// Key-value labels for the network.
32+ public var labels : [ String : String ] = [ : ]
33+
2834 /// Creates a network configuration
2935 public init (
3036 id: String ,
3137 mode: NetworkMode ,
32- subnet: String ? = nil
33- ) {
38+ subnet: String ? = nil ,
39+ labels: [ String : String ] = [ : ]
40+ ) throws {
3441 self . id = id
3542 self . mode = mode
3643 self . subnet = subnet
44+ self . labels = labels
45+ try validate ( )
46+ }
47+
48+ enum CodingKeys : String , CodingKey {
49+ case id
50+ case mode
51+ case subnet
52+ case labels
53+ }
54+
55+ /// Create a configuration from the supplied Decoder, initializing missing
56+ /// values where possible to reasonable defaults.
57+ public init ( from decoder: Decoder ) throws {
58+ let container = try decoder. container ( keyedBy: CodingKeys . self)
59+
60+ id = try container. decode ( String . self, forKey: . id)
61+ mode = try container. decode ( NetworkMode . self, forKey: . mode)
62+ subnet = try container. decodeIfPresent ( String . self, forKey: . subnet)
63+ labels = try container. decodeIfPresent ( [ String : String ] . self, forKey: . labels) ?? [ : ]
64+ try validate ( )
65+ }
66+
67+ private func validate( ) throws {
68+ guard id. isValidNetworkID ( ) else {
69+ throw ContainerizationError ( . invalidArgument, message: " invalid network ID: \( id) " )
70+ }
71+
72+ if let subnet {
73+ _ = try CIDRAddress ( subnet)
74+ }
75+
76+ for (key, value) in labels {
77+ try validateLabel ( key: key, value: value)
78+ }
79+ }
80+
81+ /// TODO: Extract when we clean up client dependencies.
82+ private func validateLabel( key: String , value: String ) throws {
83+ let keyLengthMax = 128
84+ let labelLengthMax = 4096
85+ guard key. count <= keyLengthMax else {
86+ throw ContainerizationError ( . invalidArgument, message: " invalid label, key length is greater than \( keyLengthMax) : \( key) " )
87+ }
88+
89+ guard key. isValidLabelKey ( ) else {
90+ throw ContainerizationError ( . invalidArgument, message: " invalid label key: \( key) " )
91+ }
92+
93+ let fullLabel = " \( key) = \( value) "
94+ guard fullLabel. count <= labelLengthMax else {
95+ throw ContainerizationError ( . invalidArgument, message: " invalid label, key length is greater than \( labelLengthMax) : \( fullLabel) " )
96+ }
97+ }
98+ }
99+
100+ extension String {
101+ /// Ensure that the network ID has the correct syntax.
102+ fileprivate func isValidNetworkID( ) -> Bool {
103+ let pattern = #"^[a-z0-9](?:[a-z0-9._-]{0,61}[a-z0-9])?$"#
104+ return self . range ( of: pattern, options: . regularExpression) != nil
105+ }
106+
107+ /// Ensure label key conforms to OCI or Docker label guidelines.
108+ /// TODO: Extract when we clean up client dependencies.
109+ fileprivate func isValidLabelKey( ) -> Bool {
110+ let dockerPattern = #/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*$/#
111+ let ociPattern = #/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*(?:/(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*))*$/#
112+ let dockerMatch = !self . ranges ( of: dockerPattern) . isEmpty
113+ let ociMatch = !self . ranges ( of: ociPattern) . isEmpty
114+ return dockerMatch || ociMatch
37115 }
38116}
0 commit comments