@@ -1964,6 +1964,287 @@ deployments:
19641964 }
19651965}
19661966
1967+ // TestImportsDeepMerge validates that imports are deep merged correctly.
1968+ // Maps should be recursively merged while arrays and scalars are replaced.
1969+ func TestImportsDeepMerge (t * testing.T ) {
1970+ dir := t .TempDir ()
1971+
1972+ wdBackup , err := os .Getwd ()
1973+ if err != nil {
1974+ t .Fatalf ("Error getting current working directory: %v" , err )
1975+ }
1976+ err = os .Chdir (dir )
1977+ if err != nil {
1978+ t .Fatalf ("Error changing working directory: %v" , err )
1979+ }
1980+ defer func () {
1981+ err = os .Chdir (wdBackup )
1982+ if err != nil {
1983+ t .Fatalf ("Error changing dir back: %v" , err )
1984+ }
1985+ }()
1986+
1987+ testCases := []struct {
1988+ name string
1989+ catalogYaml string
1990+ mainYaml string
1991+ verify func (t * testing.T , dev map [string ]* latest.DevPod )
1992+ }{
1993+ {
1994+ name : "Catalog provides base config, project adds minimal customization" ,
1995+ catalogYaml : `
1996+ version: v2beta1
1997+ name: catalog
1998+ dev:
1999+ api:
2000+ command: ["/bin/bash"]
2001+ ports:
2002+ - port: "8080:8080"
2003+ sync:
2004+ - path: ./src:/app/src
2005+ excludePaths:
2006+ - "**/__pycache__/"
2007+ - "**/*.pyc"
2008+ onUpload:
2009+ restartContainer: true
2010+ logs:
2011+ enabled: true
2012+ ` ,
2013+ mainYaml : `
2014+ version: v2beta1
2015+ name: my-project
2016+ imports:
2017+ - path: catalog.yaml
2018+ dev:
2019+ api:
2020+ labelSelector:
2021+ app.kubernetes.io/name: my-app
2022+ ` ,
2023+ verify : func (t * testing.T , dev map [string ]* latest.DevPod ) {
2024+ assert .Assert (t , dev ["api" ] != nil , "api dev config should exist" )
2025+
2026+ // Project customization
2027+ assert .Equal (t , dev ["api" ].LabelSelector ["app.kubernetes.io/name" ], "my-app" , "labelSelector from project" )
2028+
2029+ // Catalog base config preserved
2030+ assert .Equal (t , len (dev ["api" ].Command ), 1 , "command from catalog" )
2031+ assert .Equal (t , dev ["api" ].Command [0 ], "/bin/bash" , "command value from catalog" )
2032+ assert .Assert (t , len (dev ["api" ].Ports ) >= 1 , "ports from catalog" )
2033+ assert .Assert (t , len (dev ["api" ].Sync ) >= 1 , "sync from catalog" )
2034+ },
2035+ },
2036+ {
2037+ name : "Project replaces arrays but deep merges maps" ,
2038+ catalogYaml : `
2039+ version: v2beta1
2040+ name: catalog
2041+ dev:
2042+ api:
2043+ labelSelector:
2044+ app: catalog-app
2045+ version: v1
2046+ command: ["/bin/bash"]
2047+ ports:
2048+ - port: "8080:8080"
2049+ - port: "8443:8443"
2050+ ` ,
2051+ mainYaml : `
2052+ version: v2beta1
2053+ name: my-project
2054+ imports:
2055+ - path: catalog.yaml
2056+ dev:
2057+ api:
2058+ labelSelector:
2059+ app: project-app
2060+ tier: frontend
2061+ ports:
2062+ - port: "3000:3000"
2063+ ` ,
2064+ verify : func (t * testing.T , dev map [string ]* latest.DevPod ) {
2065+ assert .Assert (t , dev ["api" ] != nil , "api dev config should exist" )
2066+
2067+ // Maps are deep merged
2068+ assert .Equal (t , dev ["api" ].LabelSelector ["app" ], "project-app" , "project overrides 'app' in map" )
2069+ assert .Equal (t , dev ["api" ].LabelSelector ["version" ], "v1" , "catalog 'version' preserved in map" )
2070+ assert .Equal (t , dev ["api" ].LabelSelector ["tier" ], "frontend" , "project adds 'tier' in map" )
2071+
2072+ // Arrays are replaced entirely
2073+ assert .Equal (t , len (dev ["api" ].Ports ), 1 , "project ports replace catalog ports (array replaced)" )
2074+
2075+ // Catalog scalar values preserved
2076+ assert .Equal (t , len (dev ["api" ].Command ), 1 , "command from catalog" )
2077+ assert .Equal (t , dev ["api" ].Command [0 ], "/bin/bash" , "command value" )
2078+ },
2079+ },
2080+ {
2081+ name : "Catalog with nested labelSelector, project adds and overrides" ,
2082+ catalogYaml : `
2083+ version: v2beta1
2084+ name: catalog
2085+ dev:
2086+ api:
2087+ labelSelector:
2088+ app: base-app
2089+ version: v1
2090+ environment: production
2091+ command: ["/bin/bash"]
2092+ ports:
2093+ - port: "8080:8080"
2094+ ` ,
2095+ mainYaml : `
2096+ version: v2beta1
2097+ name: my-project
2098+ imports:
2099+ - path: catalog.yaml
2100+ dev:
2101+ api:
2102+ labelSelector:
2103+ app: my-custom-app
2104+ tier: frontend
2105+ ` ,
2106+ verify : func (t * testing.T , dev map [string ]* latest.DevPod ) {
2107+ assert .Assert (t , dev ["api" ] != nil , "api dev config should exist" )
2108+
2109+ // Project overrides specific label
2110+ assert .Equal (t , dev ["api" ].LabelSelector ["app" ], "my-custom-app" , "project overrides 'app' label" )
2111+
2112+ // Project adds new label
2113+ assert .Equal (t , dev ["api" ].LabelSelector ["tier" ], "frontend" , "project adds 'tier' label" )
2114+
2115+ // Catalog labels preserved when not overridden
2116+ assert .Equal (t , dev ["api" ].LabelSelector ["version" ], "v1" , "catalog 'version' label preserved" )
2117+ assert .Equal (t , dev ["api" ].LabelSelector ["environment" ], "production" , "catalog 'environment' label preserved" )
2118+
2119+ // Catalog command/ports preserved
2120+ assert .Equal (t , len (dev ["api" ].Command ), 1 , "command from catalog" )
2121+ assert .Assert (t , len (dev ["api" ].Ports ) >= 1 , "ports from catalog" )
2122+ },
2123+ },
2124+ {
2125+ name : "Multiple services with independent merges" ,
2126+ catalogYaml : `
2127+ version: v2beta1
2128+ name: catalog
2129+ dev:
2130+ api:
2131+ command: ["/bin/bash"]
2132+ ports:
2133+ - port: "8080:8080"
2134+ sync:
2135+ - path: ./api:/app
2136+ worker:
2137+ command: ["/bin/sh"]
2138+ ports:
2139+ - port: "9090:9090"
2140+ sync:
2141+ - path: ./worker:/app
2142+ ` ,
2143+ mainYaml : `
2144+ version: v2beta1
2145+ name: my-project
2146+ imports:
2147+ - path: catalog.yaml
2148+ dev:
2149+ api:
2150+ labelSelector:
2151+ app.kubernetes.io/name: my-api
2152+ tier: backend
2153+ worker:
2154+ labelSelector:
2155+ app.kubernetes.io/name: my-worker
2156+ tier: jobs
2157+ ` ,
2158+ verify : func (t * testing.T , dev map [string ]* latest.DevPod ) {
2159+ // Verify api service
2160+ assert .Assert (t , dev ["api" ] != nil , "api dev config should exist" )
2161+ assert .Equal (t , dev ["api" ].LabelSelector ["app.kubernetes.io/name" ], "my-api" , "api labelSelector" )
2162+ assert .Equal (t , dev ["api" ].LabelSelector ["tier" ], "backend" , "api tier label" )
2163+ assert .Equal (t , len (dev ["api" ].Command ), 1 , "api command from catalog" )
2164+ assert .Equal (t , dev ["api" ].Command [0 ], "/bin/bash" , "api command value" )
2165+ assert .Assert (t , len (dev ["api" ].Ports ) >= 1 , "api ports from catalog" )
2166+ assert .Assert (t , len (dev ["api" ].Sync ) >= 1 , "api sync from catalog" )
2167+
2168+ // Verify worker service
2169+ assert .Assert (t , dev ["worker" ] != nil , "worker dev config should exist" )
2170+ assert .Equal (t , dev ["worker" ].LabelSelector ["app.kubernetes.io/name" ], "my-worker" , "worker labelSelector" )
2171+ assert .Equal (t , dev ["worker" ].LabelSelector ["tier" ], "jobs" , "worker tier label" )
2172+ assert .Equal (t , len (dev ["worker" ].Command ), 1 , "worker command from catalog" )
2173+ assert .Equal (t , dev ["worker" ].Command [0 ], "/bin/sh" , "worker command value" )
2174+ assert .Assert (t , len (dev ["worker" ].Ports ) >= 1 , "worker ports from catalog" )
2175+ assert .Assert (t , len (dev ["worker" ].Sync ) >= 1 , "worker sync from catalog" )
2176+ },
2177+ },
2178+ {
2179+ name : "Project only defines labelSelector, everything else from catalog" ,
2180+ catalogYaml : `
2181+ version: v2beta1
2182+ name: catalog
2183+ dev:
2184+ api:
2185+ command: ["/bin/bash"]
2186+ ports:
2187+ - port: "8080:8080"
2188+ - port: "8443:8443"
2189+ sync:
2190+ - path: ./src:/usr/src/app/src
2191+ excludePaths:
2192+ - "**/__pycache__/"
2193+ - "**/*.pyc"
2194+ - "**/node_modules/"
2195+ onUpload:
2196+ restartContainer: true
2197+ logs:
2198+ enabled: true
2199+ ` ,
2200+ mainYaml : `
2201+ version: v2beta1
2202+ name: my-project
2203+ imports:
2204+ - path: catalog.yaml
2205+ dev:
2206+ api:
2207+ labelSelector:
2208+ app.kubernetes.io/name: my-minimal-app
2209+ ` ,
2210+ verify : func (t * testing.T , dev map [string ]* latest.DevPod ) {
2211+ assert .Assert (t , dev ["api" ] != nil , "api dev config should exist" )
2212+
2213+ // Only labelSelector from project
2214+ assert .Equal (t , dev ["api" ].LabelSelector ["app.kubernetes.io/name" ], "my-minimal-app" , "labelSelector from project" )
2215+
2216+ // Everything else from catalog
2217+ assert .Equal (t , len (dev ["api" ].Command ), 1 , "command from catalog" )
2218+ assert .Equal (t , dev ["api" ].Command [0 ], "/bin/bash" , "command value" )
2219+ assert .Equal (t , len (dev ["api" ].Ports ), 2 , "all ports from catalog" )
2220+ assert .Assert (t , len (dev ["api" ].Sync ) >= 1 , "sync config from catalog" )
2221+ },
2222+ },
2223+ }
2224+
2225+ for _ , tc := range testCases {
2226+ t .Run (tc .name , func (t * testing.T ) {
2227+ // Create catalog file
2228+ err := fsutil .WriteToFile ([]byte (tc .catalogYaml ), filepath .Join (dir , "catalog.yaml" ))
2229+ assert .NilError (t , err , "Error writing catalog.yaml" )
2230+
2231+ // Create main config file
2232+ err = fsutil .WriteToFile ([]byte (tc .mainYaml ), filepath .Join (dir , "devspace.yaml" ))
2233+ assert .NilError (t , err , "Error writing devspace.yaml" )
2234+
2235+ // Load config
2236+ loader , err := NewConfigLoader (filepath .Join (dir , "devspace.yaml" ))
2237+ assert .NilError (t , err , "Error creating config loader" )
2238+
2239+ config , err := loader .Load (context .TODO (), nil , & ConfigOptions {Dry : true }, log .Discard )
2240+ assert .NilError (t , err , "Error loading config in test case %s" , tc .name )
2241+
2242+ // Verify using custom verification function
2243+ tc .verify (t , config .Config ().Dev )
2244+ })
2245+ }
2246+ }
2247+
19672248func stripNames (config * latest.Config ) {
19682249 for k := range config .Images {
19692250 config .Images [k ].Name = ""
0 commit comments