Skip to content

Commit 31b654f

Browse files
feat(exporter): add Cloud Foundry App resource export support (#232)
* feat(exporter): adding App resource * chore(clifford): bumping xp-clifford dependency * test(exporter): add unit tests for convertAppResource helpers * docs(exporter): add documentation comments to app export package * doc(export): Update USERGUIDE with App resource * feat(exporter): add convertProcessConfiguration helper with tests and documentation Add convertProcessConfiguration function to handle individual process conversion from CF manifest to Crossplane ProcessConfiguration. This extracts the process conversion logic into a separate, testable function. Changes: - convert.go: Add convertProcessConfiguration() helper, refactor convertProcessesField() to use the new helper, add documentation comments to convertProcessConfiguration and convertReadinessHealthCheckConfiguration - convert_test.go: Add TestConvertProcessConfiguration with test cases: * process_with_all_fields_set * process_with_only_required_fields * process_with_partial_fields * Also includes TestConvertReadinessHealthCheckConfiguration tests - manifest.go: Clean up commented code The new helper conditionally sets fields only when they have non-zero values, using nil pointers for omitted fields. * test: add edge cases for GenerateDockerCredentialSecret Add test cases covering empty username, empty secret name, and both empty to improve coverage of the docker credential secret generation. * build(xpcf): bump exporter-cli to 0.0.1-alpha2 Update version and vendorHash in config.nix. * refactor(cf): use direct struct field for app name Replace `app.GetName()` with `app.Name` in `convertAppResource`. * (build): updating nix flakes Modified-by: gergely-szabo-sap <gergely.szabo@sap.com> * build: update vendor hash for export CLI * ci: fix submodule-path of docs testing workflow * fix: invalid doc references * fix: invalid doc references
1 parent 808de31 commit 31b654f

13 files changed

Lines changed: 1214 additions & 16 deletions

File tree

.github/workflows/test-docs-build.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ jobs:
2525
with:
2626
provider-repo: ${{ github.repository }}
2727
provider-ref: ${{ github.sha }}
28-
submodule-path: docs/crossplane-provider-cloudfoundry
28+
submodule-path: docs/crossplane-provider-cloudfoundry

cmd/exporter/cf/app/app.go

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
// Package app implements Cloud Foundry App resource export functionality.
2+
// It fetches CF applications from the API, converts them to Crossplane App resources,
3+
// and handles filtering by organization and space.
4+
package app
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"log/slog"
10+
"regexp"
11+
"time"
12+
13+
"github.com/SAP/crossplane-provider-cloudfoundry/cmd/exporter/cf/guidname"
14+
"github.com/SAP/crossplane-provider-cloudfoundry/cmd/exporter/cf/org"
15+
"github.com/SAP/crossplane-provider-cloudfoundry/cmd/exporter/cf/resources"
16+
"github.com/SAP/crossplane-provider-cloudfoundry/cmd/exporter/cf/space"
17+
18+
"github.com/SAP/xp-clifford/cli/configparam"
19+
"github.com/SAP/xp-clifford/cli/export"
20+
"github.com/SAP/xp-clifford/erratt"
21+
"github.com/SAP/xp-clifford/mkcontainer"
22+
"github.com/SAP/xp-clifford/parsan"
23+
"github.com/SAP/xp-clifford/yaml"
24+
"github.com/cloudfoundry/go-cfclient/v3/client"
25+
"github.com/cloudfoundry/go-cfclient/v3/resource"
26+
)
27+
28+
var (
29+
c mkcontainer.TypedContainer[*res]
30+
param = configparam.StringSlice("app", "Filter for Cloud Foundry apps").
31+
WithFlagName("app")
32+
)
33+
34+
func init() {
35+
resources.RegisterKind(app{})
36+
}
37+
38+
// res wraps a Cloud Foundry App with comment metadata for YAML export.
39+
type res struct {
40+
*resource.App
41+
*yaml.ResourceWithComment
42+
}
43+
44+
// GetGUID returns the Cloud Foundry GUID of the application.
45+
func (r *res) GetGUID() string {
46+
return r.GUID
47+
}
48+
49+
// GetName returns the sanitized name of the application suitable for Kubernetes resources.
50+
// If sanitization fails, a warning comment is added to the resource.
51+
func (r *res) GetName() string {
52+
name := r.Name
53+
names := parsan.ParseAndSanitize(name, parsan.RFC1035LowerSubdomain)
54+
if len(names) == 0 {
55+
r.AddComment(fmt.Sprintf("error sanitizing name: %s", name))
56+
} else {
57+
name = names[0]
58+
}
59+
return name
60+
}
61+
62+
// app implements the resources.Kind interface for CF App export.
63+
type app struct{}
64+
65+
var _ resources.Kind = app{}
66+
67+
// Param returns the configuration parameter for filtering apps during export.
68+
func (a app) Param() configparam.ConfigParam {
69+
return param
70+
}
71+
72+
// KindName returns the name of this resource kind ("app").
73+
func (a app) KindName() string {
74+
return param.GetName()
75+
}
76+
77+
// Export fetches CF applications, converts them to Crossplane resources, and emits them via the event handler.
78+
// It filters apps by organization/space and processes each matching application.
79+
func (a app) Export(ctx context.Context, cfClient *client.Client, evHandler export.EventHandler, resolveReferences bool) error {
80+
apps, err := Get(ctx, cfClient)
81+
if err != nil {
82+
return err
83+
}
84+
if apps.IsEmpty() {
85+
evHandler.Warn(erratt.New("no apps found", "apps", param.Value()))
86+
return nil
87+
}
88+
ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
89+
defer cancel()
90+
for _, app := range apps.AllByGUIDs() {
91+
slog.Debug("exporting app", "name", app.Name)
92+
evHandler.Resource(convertAppResource(ctx, cfClient, app, evHandler, resolveReferences))
93+
}
94+
95+
return nil
96+
}
97+
98+
// getAllNamesFn returns a function that fetches all app names for the given org/space GUIDs.
99+
func getAllNamesFn(ctx context.Context, cfClient *client.Client, orgGuids, spaceGuids []string) func() ([]string, error) {
100+
return func() ([]string, error) {
101+
resources, err := getAll(ctx, cfClient, orgGuids, spaceGuids, []string{})
102+
if err != nil {
103+
return nil, err
104+
}
105+
names := make([]string, len(resources))
106+
for i, res := range resources {
107+
names[i] = guidname.NewName(res).String()
108+
}
109+
return names, nil
110+
}
111+
}
112+
113+
// Get returns all CF applications matching the configured filter criteria.
114+
// Results are cached for subsequent calls. It prompts for app selection if none are specified.
115+
func Get(ctx context.Context, cfClient *client.Client) (mkcontainer.TypedContainer[*res], error) {
116+
if c != nil {
117+
return c, nil
118+
}
119+
orgs, err := org.Get(ctx, cfClient)
120+
if err != nil {
121+
return nil, err
122+
}
123+
spaces, err := space.Get(ctx, cfClient)
124+
if err != nil {
125+
return nil, err
126+
}
127+
128+
param.WithPossibleValuesFn(getAllNamesFn(ctx, cfClient, orgs.GetGUIDs(), spaces.GetGUIDs()))
129+
130+
selectedApps, err := param.ValueOrAsk(ctx)
131+
if err != nil {
132+
return nil, err
133+
}
134+
135+
appNames := make([]string, len(selectedApps))
136+
for i, appName := range selectedApps {
137+
name, err := guidname.ParseName(appName)
138+
if err != nil {
139+
return nil, err
140+
}
141+
appNames[i] = name.Name
142+
}
143+
slog.Debug("apps selected", "apps", selectedApps, "appNames", appNames)
144+
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
145+
defer cancel()
146+
apps, err := getAll(ctx,
147+
cfClient,
148+
orgs.GetGUIDs(),
149+
spaces.GetGUIDs(),
150+
appNames,
151+
)
152+
if err != nil {
153+
return nil, err
154+
}
155+
c = mkcontainer.NewTyped[*res]()
156+
c.Store(apps...)
157+
slog.Debug("apps collected", "apps", c.GetNames())
158+
return c, nil
159+
}
160+
161+
// getAll retrieves applications from the CF API and filters them by the provided name patterns.
162+
// Supports regex matching for app names. If appNames is empty, matches all applications.
163+
func getAll(ctx context.Context, cfClient *client.Client, orgGuids, spaceGuids, appNames []string) ([]*res, error) {
164+
var nameRxs []*regexp.Regexp
165+
166+
if len(appNames) > 0 {
167+
for _, appName := range appNames {
168+
slog.Debug("processing app", "name", appName)
169+
rx, err := regexp.Compile(appName)
170+
if err != nil {
171+
return nil, erratt.Errorf("cannot compile name to regexp: %w", err).With("appName", appName)
172+
}
173+
nameRxs = append(nameRxs, rx)
174+
}
175+
} else {
176+
nameRxs = []*regexp.Regexp{
177+
regexp.MustCompile(`.*`),
178+
}
179+
}
180+
181+
listOptions := client.NewAppListOptions()
182+
if len(orgGuids) > 0 {
183+
listOptions.OrganizationGUIDs.Values = orgGuids
184+
listOptions.SpaceGUIDs.Values = spaceGuids
185+
}
186+
apps, err := cfClient.Applications.ListAll(ctx, listOptions)
187+
if err != nil {
188+
return nil, err
189+
}
190+
191+
var results []*res
192+
for _, app := range apps {
193+
for _, nameRx := range nameRxs {
194+
if nameRx.MatchString(app.Name) {
195+
slog.Debug("matching app found", "rx", nameRx.String(), "found", app.Name)
196+
results = append(results, &res{
197+
ResourceWithComment: yaml.NewResourceWithComment(nil),
198+
App: app,
199+
})
200+
}
201+
}
202+
}
203+
return results, nil
204+
}

0 commit comments

Comments
 (0)