@@ -35,11 +35,27 @@ pub struct RegistryProxiesConfig {
3535 pub max_size : Option < size:: Size > ,
3636}
3737
38+ fn normalize_path_prefix < ' de , D > ( deserializer : D ) -> Result < Option < String > , D :: Error >
39+ where
40+ D : serde:: Deserializer < ' de > ,
41+ {
42+ let opt = Option :: < String > :: deserialize ( deserializer) ?;
43+ Ok ( opt
44+ . map ( |s| s. trim_matches ( '/' ) . to_string ( ) )
45+ . filter ( |s| !s. is_empty ( ) ) )
46+ }
47+
3848#[ derive( Serialize , Deserialize , Clone , Debug , Default , PartialEq ) ]
3949pub struct SingleRegistryProxyConfig {
4050 /// What containerd calls "namespace" (ghcr.io, docker.io, ...)
4151 /// This can be empty !!
4252 pub host : String ,
53+ /// Optional path prefix for scoped credential matching.
54+ /// Allows different credentials for different projects on the same registry host.
55+ /// Example: "system" matches repos like "system/app", "system/worker".
56+ /// When multiple entries match the same host, the longest matching prefix wins.
57+ #[ serde( default , deserialize_with = "normalize_path_prefix" ) ]
58+ pub path_prefix : Option < String > ,
4359 /// TODO: insecure currently means "use HTTP", we should also support self-signed TLS
4460 #[ serde( default ) ]
4561 pub insecure : bool ,
@@ -58,11 +74,30 @@ impl Default for RegistryProxiesConfig {
5874}
5975
6076impl RegistryProxyConfigs {
61- pub fn get_for < ' a > ( & ' a self , registry_host : & str ) -> Option < & ' a SingleRegistryProxyConfig > {
62- self . 0
63- . iter ( )
64- . find ( |& registry| registry. host == registry_host)
65- . map ( |v| v as _ )
77+ pub fn get_for < ' a > (
78+ & ' a self ,
79+ registry : & str ,
80+ repo : & str ,
81+ ) -> Option < & ' a SingleRegistryProxyConfig > {
82+ let matches = self . 0 . iter ( ) . filter_map ( |proxy| {
83+ if proxy. host == registry {
84+ if let Some ( proxy_prefix) = proxy. path_prefix . as_deref ( ) {
85+ // for prefix "org" match org/toto, not org_b/toto
86+ if repo == proxy_prefix
87+ || ( repo. starts_with ( proxy_prefix)
88+ && repo. as_bytes ( ) . get ( proxy_prefix. len ( ) ) == Some ( & b'/' ) )
89+ {
90+ return Some ( ( proxy_prefix. len ( ) , proxy) ) ;
91+ }
92+ } else {
93+ return Some ( ( 0 , proxy) ) ;
94+ }
95+ }
96+ None
97+ } ) ;
98+ matches
99+ . max_by_key ( |( prefix_len, _) | * prefix_len)
100+ . map ( |( _, registry) | registry)
66101 }
67102}
68103
@@ -71,3 +106,71 @@ impl From<Vec<SingleRegistryProxyConfig>> for RegistryProxyConfigs {
71106 RegistryProxyConfigs ( vec)
72107 }
73108}
109+
110+ #[ cfg( test) ]
111+ mod tests {
112+ use super :: * ;
113+
114+ #[ test]
115+ fn test_registry_proxy_deserialize_path_prefix ( ) {
116+ let proxy_config: SingleRegistryProxyConfig =
117+ serde_json:: from_str ( r#"{"host": "registry.example.com", "path_prefix": "/org/sub/"}"# )
118+ . unwrap ( ) ;
119+ assert_eq ! ( proxy_config. path_prefix. unwrap( ) , "org/sub" ) ;
120+
121+ let proxy_config: SingleRegistryProxyConfig =
122+ serde_json:: from_str ( r#"{"host": "registry.example.com", "path_prefix": "/"}"# )
123+ . unwrap ( ) ;
124+ assert_eq ! ( proxy_config. path_prefix, None ) ;
125+ }
126+
127+ #[ test]
128+ fn test_registry_proxy_configs_path_prefix_longest_match_wins ( ) {
129+ let config = RegistryProxiesConfig {
130+ registries : vec ! [
131+ SingleRegistryProxyConfig {
132+ host: "registry.example.com" . to_string( ) ,
133+ username: Some ( "default" . to_string( ) ) ,
134+ ..Default :: default ( )
135+ } ,
136+ SingleRegistryProxyConfig {
137+ host: "registry.example.com" . to_string( ) ,
138+ path_prefix: Some ( "org" . to_string( ) ) ,
139+ username: Some ( "org-token" . to_string( ) ) ,
140+ ..Default :: default ( )
141+ } ,
142+ SingleRegistryProxyConfig {
143+ host: "registry.example.com" . to_string( ) ,
144+ path_prefix: Some ( "org/sub" . to_string( ) ) ,
145+ username: Some ( "org-sub-token" . to_string( ) ) ,
146+ ..Default :: default ( )
147+ } ,
148+ ]
149+ . into ( ) ,
150+ ..Default :: default ( )
151+ } ;
152+ // "org/sub/app" matches both, but "org/sub" is longer
153+ let proxy = config
154+ . registries
155+ . get_for ( "registry.example.com" , "org/sub/app" ) ;
156+ assert_eq ! ( proxy. unwrap( ) . username, Some ( "org-sub-token" . to_string( ) ) ) ;
157+
158+ // "org/other" matches only "org"
159+ let proxy = config
160+ . registries
161+ . get_for ( "registry.example.com" , "org/other" ) ;
162+ assert_eq ! ( proxy. unwrap( ) . username, Some ( "org-token" . to_string( ) ) ) ;
163+
164+ // no path_prefix match
165+ let proxy = config
166+ . registries
167+ . get_for ( "registry.example.com" , "outta-this-world" ) ;
168+ assert_eq ! ( proxy. unwrap( ) . username, Some ( "default" . to_string( ) ) ) ;
169+
170+ // doesn't match path prefix across '/' boundary
171+ let proxy = config
172+ . registries
173+ . get_for ( "registry.example.com" , "org_b/app" ) ;
174+ assert_eq ! ( proxy. unwrap( ) . username, Some ( "default" . to_string( ) ) ) ;
175+ }
176+ }
0 commit comments