Skip to content

Commit b8dd018

Browse files
authored
Support organization-based CF discovery and filtering (#587)
1 parent 0893619 commit b8dd018

File tree

5 files changed

+772
-145
lines changed

5 files changed

+772
-145
lines changed

cmd/asset_generation/discover/cloud_foundry/cmd.go

Lines changed: 101 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var (
2323
outputDir string
2424
pType string
2525
spaces []string
26+
orgs []string
2627
appName string
2728
skipSslValidation bool
2829
cfConfigPath string
@@ -45,16 +46,23 @@ func NewDiscoverCloudFoundryCommand(log logr.Logger) (string, *cobra.Command) {
4546
if err := cmd.ParseFlags(args); err != nil {
4647
return err
4748
}
49+
50+
// Validate that --orgs is only used with --use-live-connection
51+
if !useLive && len(orgs) > 0 {
52+
return fmt.Errorf("--orgs flag can only be used with --use-live-connection. For local manifest discovery (--input), organization name defaults to 'local'")
53+
}
54+
4855
if useLive {
49-
if len(spaces) == 0 {
50-
return fmt.Errorf("at least one space is required")
51-
}
5256
if cfConfigPath != "" {
5357
_, err := os.Stat(cfConfigPath)
5458
if err != nil {
5559
return fmt.Errorf("failed to retrieve Cloud Foundry configuration file at %s:%s", cfConfigPath, err)
5660
}
5761
}
62+
// Orgs are required for all live discovery operations
63+
if len(orgs) == 0 {
64+
return fmt.Errorf("--orgs flag is required when using --use-live-connection")
65+
}
5866
return nil
5967
}
6068
if input == "" {
@@ -87,15 +95,15 @@ func NewDiscoverCloudFoundryCommand(log logr.Logger) (string, *cobra.Command) {
8795
cmd.Flags().BoolVar(&useLive, "use-live-connection", false, "Enable real-time discovery using live platform connections.")
8896
cmd.Flags().StringVar(&pType, "platformType", "cloud-foundry", "Platform type for discovery. Allowed value is: \"cloud-foundry\" (default).")
8997
cmd.Flags().StringVar(&cfConfigPath, "cf-config", "~/.cf/config", "Path to the Cloud Foundry CLI configuration file (default: ~/.cf/config).")
90-
cmd.Flags().StringSliceVar(&spaces, "spaces", []string{}, "Comma-separated list of Cloud Foundry spaces to analyze (e.g., --spaces=\"space1,space2\"). At least one space is required when using live discovery.")
98+
cmd.Flags().StringSliceVar(&spaces, "spaces", []string{}, "Comma-separated list of Cloud Foundry spaces to analyze (e.g., --spaces=\"space1,space2\"). If not provided, discovers all spaces in the specified organizations.")
99+
cmd.Flags().StringSliceVar(&orgs, "orgs", []string{}, "Comma-separated list of Cloud Foundry organizations (e.g., --orgs=\"org1,org2\"). Required for live discovery.")
91100
cmd.Flags().StringVar(&appName, "app-name", "", "Name of the Cloud Foundry application to discover.")
92101
cmd.Flags().BoolVar(&skipSslValidation, "skip-ssl-validation", false, "Skip SSL certificate validation for API connections (default: false).")
93102

94103
cmd.Flags().BoolVar(&listApps, "list-apps", false, "List applications available for each space.")
95104

96105
cmd.MarkFlagsMutuallyExclusive("use-live-connection", "input")
97106
cmd.MarkFlagsOneRequired("use-live-connection", "input")
98-
cmd.MarkFlagsRequiredTogether("use-live-connection", "spaces")
99107
cmd.MarkFlagsMutuallyExclusive("list-apps", "app-name")
100108
cmd.MarkFlagsMutuallyExclusive("list-apps", "output-dir")
101109
return "Cloud Foundry V3 (local manifest)", cmd
@@ -127,7 +135,17 @@ func listApplicationsLive(p providerInterface.Provider, out io.Writer) error {
127135
if err != nil {
128136
return fmt.Errorf("failed to list apps by space: %w", err)
129137
}
130-
printApps(appListPerSpace, out)
138+
139+
// Log orgs that have no spaces/apps and will be skipped
140+
for _, org := range orgs {
141+
if appList, exists := appListPerSpace[org]; !exists || len(appList) == 0 {
142+
logger.Info("Skipping organization: no spaces matching the filter or no applications found", "org_name", org)
143+
}
144+
}
145+
146+
if err := printApps(appListPerSpace, out); err != nil {
147+
return err
148+
}
131149

132150
return nil
133151
}
@@ -147,21 +165,51 @@ func listApplicationsLocal(inputPath string, out io.Writer) error {
147165
if err != nil {
148166
return err
149167
}
150-
printApps(appListPerSpace, out)
168+
if err := printApps(appListPerSpace, out); err != nil {
169+
return err
170+
}
151171
}
152172
return nil
153173
}
154174

155-
func printApps(appListPerSpace map[string][]any, out io.Writer) error {
175+
func printApps(appListByOrg map[string][]any, out io.Writer) error {
176+
// Group apps by org -> space -> apps for hierarchical display
177+
type orgSpaceApps struct {
178+
orgName string
179+
spaceName string
180+
apps []string
181+
}
182+
183+
hierarchy := make(map[string]map[string][]string) // map[org]map[space][]appNames
184+
185+
for orgName, appsAny := range appListByOrg {
186+
if _, exists := hierarchy[orgName]; !exists {
187+
hierarchy[orgName] = make(map[string][]string)
188+
}
156189

157-
for space, appsAny := range appListPerSpace {
158-
fmt.Fprintf(out, "Space: %s\n", space)
159190
for _, appAny := range appsAny {
160191
appRef, ok := appAny.(cfProvider.AppReference)
161192
if !ok {
162193
return fmt.Errorf("unexpected type for app list: %T", appAny)
163194
}
164-
fmt.Fprintf(out, " - %s\n", appRef.AppName)
195+
196+
spaceName := appRef.SpaceName
197+
if spaceName == "" {
198+
spaceName = "unknown"
199+
}
200+
201+
hierarchy[orgName][spaceName] = append(hierarchy[orgName][spaceName], appRef.AppName)
202+
}
203+
}
204+
205+
// Print in org -> space -> apps hierarchy
206+
for orgName, spaces := range hierarchy {
207+
fmt.Fprintf(out, "Organization: %s\n", orgName)
208+
for spaceName, apps := range spaces {
209+
fmt.Fprintf(out, " Space: %s\n", spaceName)
210+
for _, appName := range apps {
211+
fmt.Fprintf(out, " - %s\n", appName)
212+
}
165213
}
166214
}
167215
return nil
@@ -223,9 +271,22 @@ func createLiveProvider() (providerInterface.Provider, error) {
223271
return nil, err
224272
}
225273

274+
// Log auto-discovery intentions
275+
if len(orgs) == 0 {
276+
logger.Info("No organizations specified, will discover all organizations")
277+
}
278+
if len(spaces) == 0 {
279+
if len(orgs) == 0 {
280+
logger.Info("No spaces specified, will discover all spaces across all organizations")
281+
} else {
282+
logger.Info("No spaces specified, will discover all spaces in organizations", "orgs", orgs)
283+
}
284+
}
285+
226286
cfg := &cfProvider.Config{
227287
CloudFoundryConfig: cfCfg,
228-
SpaceNames: spaces,
288+
SpaceNames: spaces, // Empty means all spaces for the orgs
289+
OrgNames: orgs,
229290
}
230291

231292
p, err := cfProvider.New(cfg, &logger, concealSensitiveData)
@@ -236,31 +297,48 @@ func createLiveProvider() (providerInterface.Provider, error) {
236297
}
237298

238299
func discoverLive(p providerInterface.Provider, out io.Writer) error {
239-
appListPerSpace, err := p.ListApps()
300+
appListByOrg, err := p.ListApps()
240301
if err != nil {
241-
return fmt.Errorf("failed to list apps by space: %w", err)
302+
return fmt.Errorf("failed to list apps by org: %w", err)
303+
}
304+
305+
// Log orgs that have no spaces/apps and will be skipped
306+
for _, org := range orgs {
307+
if appList, exists := appListByOrg[org]; !exists || len(appList) == 0 {
308+
logger.Info("Skipping organization: no spaces matching the filter or no applications found", "org_name", org)
309+
}
242310
}
243311

244-
for _, appList := range appListPerSpace {
312+
// Iterate through orgs -> apps
313+
for orgName, appList := range appListByOrg {
314+
if len(appList) == 0 {
315+
continue
316+
}
317+
318+
logger.Info("Processing organization", "org_name", orgName, "app_count", len(appList))
319+
245320
for _, appReferences := range appList {
246321
appRef, ok := appReferences.(cfProvider.AppReference)
247322
if !ok {
248323
return fmt.Errorf("unexpected type for app list: %T", appReferences)
249324
}
325+
250326
if appName != "" && appRef.AppName != appName {
251-
logger.Info("Skipping application: app name does not match target app name", "app name", appRef.AppName, "target app name", appName)
327+
logger.Info("Skipping application: app name does not match target app name",
328+
"app_name", appRef.AppName, "target_app_name", appName)
252329
continue
253330
}
254331

332+
logger.Info("Processing application", "org_name", appRef.OrgName, "space_name", appRef.SpaceName, "app_name", appRef.AppName)
333+
255334
discoverResult, err := p.Discover(appRef)
256335
if err != nil {
257336
return err
258337
}
259-
err = OutputAppManifestsYAML(out, discoverResult, appRef.SpaceName, appRef.AppName)
338+
err = OutputAppManifestsYAML(out, discoverResult, appRef.OrgName, appRef.SpaceName, appRef.AppName)
260339
if err != nil {
261340
return err
262341
}
263-
264342
}
265343
}
266344
return nil
@@ -316,19 +394,22 @@ func processAppList(p providerInterface.Provider, appList []any, out io.Writer)
316394
return err
317395
}
318396

319-
err = OutputAppManifestsYAML(out, app, appRef.SpaceName, appRef.AppName)
397+
err = OutputAppManifestsYAML(out, app, appRef.OrgName, appRef.SpaceName, appRef.AppName)
320398
if err != nil {
321399
return err
322400
}
323401
}
324402
return nil
325403
}
326404

327-
func OutputAppManifestsYAML(out io.Writer, discoverResult *providerTypes.DiscoverResult, spaceName string, appName string) error {
405+
func OutputAppManifestsYAML(out io.Writer, discoverResult *providerTypes.DiscoverResult, orgName string, spaceName string, appName string) error {
328406
suffix := "_" + appName
329407
if spaceName != "" {
330408
suffix = "_" + spaceName + suffix
331409
}
410+
if orgName != "" {
411+
suffix = "_" + orgName + suffix
412+
}
332413
printer := printers.NewOutput(out)
333414
printFunc := printer.ToStdout
334415

0 commit comments

Comments
 (0)