Skip to content

Commit 7041360

Browse files
feat/export-tool: adding export CLI
Feat: Export tool
2 parents affa1ce + b2d935b commit 7041360

30 files changed

Lines changed: 2430 additions & 16 deletions

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ NPROCS ?= 1
3333
GO_TEST_PARALLEL := $(shell echo $$(( $(NPROCS) / 2 )))
3434

3535
GO_REQUIRED_VERSION ?= 1.23
36-
GO_STATIC_PACKAGES = $(GO_PROJECT)/cmd/provider
36+
GO_STATIC_PACKAGES = $(GO_PROJECT)/cmd/provider $(GO_PROJECT)/cmd/exporter
3737
GO_LDFLAGS += -X $(GO_PROJECT)/internal/version.Version=$(VERSION)
3838
GO_SUBDIRS += cmd internal apis
3939
GO111MODULE = on

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ kind delete cluster <cluster-name>
6969

7070
### Upgrade Tests
7171

72-
The provider comes with a set of upgrade tests that can be run locally. To learn more about it : [Upgrade Tests README](./test/upgrade/README.md).
72+
The provider comes with a set of upgrade tests that can be run locally. To learn more about it : [Upgrade Tests README](./test/upgrade/README.md).
7373

7474
#### Required Configuration
7575
In order for the tests to perform successfully some configuration need to be present as environment variables:
@@ -92,9 +92,15 @@ Contains the CF server URL, for example:
9292
https://api.cf.eu12.hana.ondemand.com
9393
```
9494

95+
## Export CLI
96+
97+
The provider includes an export CLI tool that generates managed resource definitions from the existing resource configurations of a Cloud Foundry cluster.
98+
99+
For more details, refer to the [user guide](cmd/exporter/docs/USERGUIDE.md).
100+
95101
## 👐 Support, Feedback, Contributing
96102

97-
If you have a question always feel free to reach out on our official crossplane slack channel:
103+
If you have a question always feel free to reach out on our official crossplane slack channel:
98104

99105
:rocket: [**#provider-sap-cloudfoundry**](https://crossplane.slack.com/archives/C08NBTJ1J05).
100106

cmd/exporter/cf/cache/guid.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package cache
2+
3+
import (
4+
"iter"
5+
"maps"
6+
"slices"
7+
)
8+
9+
type CacheWithGUID[T ResourceWithGuid] interface {
10+
GetByGUID(guid string) T
11+
GetGUIDs() []string
12+
Len() int
13+
StoreWithGUID(resources ...T)
14+
AllByGUIDs() iter.Seq2[string, T]
15+
}
16+
17+
type cacheWithGUID[T ResourceWithGuid] struct {
18+
guidIndex map[string]T
19+
}
20+
21+
var _ CacheWithGUID[dummyResourceWithGuid] = &cacheWithGUID[dummyResourceWithGuid]{}
22+
23+
func NewWithGUID[T ResourceWithGuid]() CacheWithGUID[T] {
24+
return &cacheWithGUID[T]{
25+
guidIndex: make(map[string]T),
26+
}
27+
}
28+
29+
func (c *cacheWithGUID[T]) StoreWithGUID(resources ...T) {
30+
for _, resource := range resources {
31+
c.guidIndex[resource.GetGUID()] = resource
32+
}
33+
}
34+
35+
func (c *cacheWithGUID[T]) GetByGUID(guid string) T {
36+
return c.guidIndex[guid]
37+
}
38+
39+
func (c *cacheWithGUID[T]) GetGUIDs() []string {
40+
return slices.Sorted(maps.Keys(c.guidIndex))
41+
}
42+
43+
func (c *cacheWithGUID[T]) Len() int {
44+
return len(c.guidIndex)
45+
}
46+
47+
func (c *cacheWithGUID[T]) AllByGUIDs() iter.Seq2[string, T] {
48+
return maps.All(c.guidIndex)
49+
}

cmd/exporter/cf/cache/guidname.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package cache
2+
3+
type CacheWithGUIDAndName[T ResourceWithGUIDAndName] interface {
4+
CacheWithGUID[T]
5+
CacheWithName[T]
6+
StoreWithGUIDAndName(resources ...T)
7+
}
8+
9+
type cacheWithGUIDAndName[T ResourceWithGUIDAndName] struct {
10+
CacheWithGUID[T]
11+
CacheWithName[T]
12+
}
13+
14+
var _ CacheWithGUIDAndName[dummyResourceWithGUIDAndName] = &cacheWithGUIDAndName[dummyResourceWithGUIDAndName]{}
15+
16+
func NewWithGUIDAndName[T ResourceWithGUIDAndName]() CacheWithGUIDAndName[T] {
17+
return &cacheWithGUIDAndName[T]{
18+
CacheWithGUID: NewWithGUID[T](),
19+
CacheWithName: NewWithName[T](),
20+
}
21+
}
22+
23+
func (c *cacheWithGUIDAndName[T]) Len() int {
24+
return c.CacheWithGUID.Len()
25+
}
26+
27+
func (c *cacheWithGUIDAndName[T]) StoreWithGUIDAndName(resources ...T) {
28+
c.CacheWithGUID.StoreWithGUID(resources...)
29+
c.CacheWithName.StoreWithName(resources...)
30+
}
31+
32+
func (c *cacheWithGUIDAndName[T]) StoreWithName(_ ...T) {
33+
panic("StoreWithName shall not be used here")
34+
}
35+
36+
func (c *cacheWithGUIDAndName[T]) StoreWithGUID(_ ...T) {
37+
panic("StoreWithGUID shall not be used here")
38+
}

cmd/exporter/cf/cache/name.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package cache
2+
3+
import (
4+
"iter"
5+
"maps"
6+
"slices"
7+
)
8+
9+
type CacheWithName[T ResourceWithName] interface {
10+
GetByName(name string) []T
11+
GetNames() []string
12+
StoreWithName(resources ...T)
13+
Len() int
14+
AllByNames() iter.Seq2[string, []T]
15+
}
16+
17+
type cacheWithName[T ResourceWithName] struct {
18+
nameIndex map[string][]T
19+
}
20+
21+
var _ CacheWithName[dummyResourceWithName] = &cacheWithName[dummyResourceWithName]{}
22+
23+
func NewWithName[T ResourceWithName]() CacheWithName[T] {
24+
return &cacheWithName[T]{
25+
nameIndex: make(map[string][]T),
26+
}
27+
}
28+
29+
func (c *cacheWithName[T]) StoreWithName(resources ...T) {
30+
for _, resource := range resources {
31+
name := resource.GetName()
32+
c.nameIndex[name] = append(c.nameIndex[name], resource)
33+
}
34+
}
35+
36+
func (c *cacheWithName[T]) GetByName(name string) []T {
37+
return c.nameIndex[name]
38+
}
39+
40+
func (c *cacheWithName[T]) GetNames() []string {
41+
return slices.Sorted(maps.Keys(c.nameIndex))
42+
}
43+
44+
func (c *cacheWithName[T]) Len() int {
45+
return len(c.nameIndex)
46+
}
47+
48+
func (c *cacheWithName[T]) AllByNames() iter.Seq2[string, []T] {
49+
return maps.All(c.nameIndex)
50+
}

cmd/exporter/cf/cache/resource.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package cache
2+
3+
import (
4+
"github.com/SAP/xp-clifford/yaml"
5+
)
6+
7+
type ResourceWithGuid interface {
8+
GetGUID() string
9+
}
10+
11+
type dummyResourceWithGuid struct{}
12+
13+
var _ ResourceWithGuid = dummyResourceWithGuid{}
14+
15+
func (r dummyResourceWithGuid) GetGUID() string {
16+
return "dummyGUID"
17+
}
18+
19+
type ResourceWithName interface {
20+
GetName() string
21+
}
22+
23+
type dummyResourceWithName struct{}
24+
25+
var _ ResourceWithName = dummyResourceWithName{}
26+
27+
func (r dummyResourceWithName) GetName() string {
28+
return "dummyName"
29+
}
30+
31+
type ResourceWithGUIDAndName interface {
32+
ResourceWithGuid
33+
ResourceWithName
34+
yaml.CommentedYAML
35+
}
36+
37+
type dummyResourceWithGUIDAndName struct {
38+
dummyResourceWithGuid
39+
dummyResourceWithName
40+
*yaml.ResourceWithComment
41+
}
42+
43+
var _ ResourceWithGUIDAndName = dummyResourceWithGUIDAndName{}

cmd/exporter/cf/config/config.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package config
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
7+
"github.com/SAP/xp-clifford/cli/configparam"
8+
9+
"github.com/cloudfoundry/go-cfclient/v3/config"
10+
)
11+
12+
func Get(ctx context.Context, useCfLoginMethod *configparam.BoolParam, apiUrlParam, usernameParam, passwordParam *configparam.StringParam) (*config.Config, error) {
13+
if useCfLoginMethod.Value() {
14+
slog.Debug("log in to CF using the CF login method")
15+
return config.NewFromCFHome()
16+
} else {
17+
apiUrl, err := apiUrlParam.ValueOrAsk(ctx)
18+
if err != nil {
19+
return nil, err
20+
}
21+
username, err := usernameParam.ValueOrAsk(ctx)
22+
if err != nil {
23+
return nil, err
24+
}
25+
password, err := passwordParam.ValueOrAsk(ctx)
26+
if err != nil {
27+
return nil, err
28+
}
29+
slog.Debug("log in to CF using credentials", "url", apiUrl, "username", username)
30+
return config.New(apiUrl, config.UserPassword(username, password))
31+
}
32+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package guidname
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
7+
"github.com/SAP/xp-clifford/erratt"
8+
)
9+
10+
type Name struct {
11+
Name string
12+
GUID string
13+
}
14+
15+
func NewName(guid, name string) *Name {
16+
return &Name{
17+
Name: name,
18+
GUID: guid,
19+
}
20+
}
21+
22+
func (n *Name) String() string {
23+
return fmt.Sprintf("%s - [%s]", n.Name, n.GUID)
24+
}
25+
26+
var nameRx = regexp.MustCompile(`(.*) - \[(.*)\]`)
27+
28+
func ParseName(s string) (*Name, error) {
29+
parsed := nameRx.FindStringSubmatch(s)
30+
switch len(parsed) {
31+
case 0:
32+
return &Name{
33+
Name: fmt.Sprintf("^%s$", s),
34+
GUID: s,
35+
}, nil
36+
case 3:
37+
return &Name{
38+
Name: fmt.Sprintf("^%s$", parsed[1]),
39+
GUID: parsed[2],
40+
}, nil
41+
default:
42+
return nil, erratt.New("guidname cannot be be parsed", "name", s, "len", len(parsed))
43+
}
44+
}
45+
46+
func CollectNames(guidNames []string) ([]string, error) {
47+
names := make([]string, len(guidNames))
48+
for i, guidName := range guidNames {
49+
name, err := ParseName(guidName)
50+
if err != nil {
51+
return nil, err
52+
}
53+
names[i] = name.Name
54+
}
55+
return names, nil
56+
}

cmd/exporter/cf/org/convert.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org
2+
3+
import (
4+
"log/slog"
5+
6+
"github.com/SAP/crossplane-provider-cloudfoundry/apis/resources/v1alpha1"
7+
8+
"github.com/SAP/xp-clifford/yaml"
9+
v1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
)
12+
13+
func convertOrgResource(org *res) *yaml.ResourceWithComment {
14+
slog.Debug("converting org", "name", org.GetName())
15+
o := yaml.NewResourceWithComment(
16+
&v1alpha1.Organization{
17+
TypeMeta: metav1.TypeMeta{
18+
Kind: v1alpha1.Org_Kind,
19+
APIVersion: v1alpha1.CRDGroupVersion.String(),
20+
},
21+
ObjectMeta: metav1.ObjectMeta{
22+
Name: org.GetName(),
23+
Annotations: map[string]string{
24+
"crossplane.io/external-name": org.GetGUID(),
25+
},
26+
},
27+
Spec: v1alpha1.OrgSpec{
28+
ResourceSpec: v1.ResourceSpec{
29+
ManagementPolicies: []v1.ManagementAction{
30+
v1.ManagementActionObserve,
31+
},
32+
},
33+
ForProvider: v1alpha1.OrgParameters{
34+
Annotations: org.Metadata.Annotations,
35+
Labels: org.Metadata.Labels,
36+
Name: org.Name,
37+
Suspended: &org.Suspended,
38+
},
39+
},
40+
})
41+
o.CloneComment(org)
42+
return o
43+
}

0 commit comments

Comments
 (0)