Skip to content

Commit 2e0e058

Browse files
committed
Add new ServiceClusterProperties CRUD
This allows controllers to coordinate values
1 parent ba0ad58 commit 2e0e058

File tree

23 files changed

+378
-4
lines changed

23 files changed

+378
-4
lines changed

internal/api/registry.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ const (
7878
var (
7979
OperationStatusResourceType = azcorearm.NewResourceType(ProviderNamespace, OperationStatusResourceTypeName)
8080
ClusterResourceType = azcorearm.NewResourceType(ProviderNamespace, ClusterResourceTypeName)
81+
ServiceProviderClusterResourceType = azcorearm.NewResourceType(ProviderNamespace, ClusterResourceTypeName+"/serviceProviderCluster")
8182
NodePoolResourceType = azcorearm.NewResourceType(ProviderNamespace, ClusterResourceTypeName+"/"+NodePoolResourceTypeName)
8283
ExternalAuthResourceType = azcorearm.NewResourceType(ProviderNamespace, ClusterResourceTypeName+"/"+ExternalAuthResourceTypeName)
8384
PreflightResourceType = azcorearm.NewResourceType(ProviderNamespace, "deployments/preflight")

internal/api/types_cosmosdata.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,55 @@
1515
package api
1616

1717
import (
18+
"errors"
19+
"strings"
20+
21+
azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
22+
1823
"github.com/Azure/ARO-HCP/internal/api/arm"
1924
)
2025

26+
// CosmosMetadata is metadata required for all data we store in cosmos
27+
type CosmosMetadata struct {
28+
// resourceID is used as the cosmosUID after replacing all '/' with '|'
29+
ResourceID azcorearm.ResourceID `json:"resourceID"`
30+
31+
// TODO add an etag that is not serialized to cosmos, but is set on read.
32+
// When non-empty it will cause a conditional replace to be used
33+
// When empty it will cause an unconditional replace
34+
}
35+
36+
func (c *CosmosMetadata) GetResourceID() azcorearm.ResourceID {
37+
return c.ResourceID
38+
}
39+
40+
func (c *CosmosMetadata) SetResourceID(resourceID azcorearm.ResourceID) {
41+
c.ResourceID = resourceID
42+
}
43+
44+
type CosmosMetadataAccessor interface {
45+
GetResourceID() azcorearm.ResourceID
46+
SetResourceID(azcorearm.ResourceID)
47+
}
48+
49+
var _ CosmosPersistable = &CosmosMetadata{}
50+
51+
func (o *CosmosMetadata) GetCosmosData() CosmosData {
52+
return CosmosData{
53+
CosmosUID: Must(ResourceIDToCosmosID(&o.ResourceID)),
54+
// partitionkeys are case-sensitive in cosmos, so we need all of our cases to be the same
55+
// and we have no guarantee that prior to this the case is consistent.
56+
PartitionKey: strings.ToLower(o.ResourceID.SubscriptionID),
57+
ItemID: &o.ResourceID,
58+
}
59+
}
60+
61+
func (o *CosmosMetadata) SetCosmosDocumentData(cosmosUID string) {
62+
panic("not supported")
63+
}
64+
65+
var _ CosmosMetadataAccessor = &CosmosMetadata{}
66+
2167
type CosmosPersistable interface {
2268
GetCosmosData() CosmosData
2369
SetCosmosDocumentData(cosmosUID string)
@@ -26,3 +72,24 @@ type CosmosPersistable interface {
2672
// CosmosData contains the information that persisted resources must have for us to support CRUD against them.
2773
// These are not (currently) all stored in the same place in our various types.
2874
type CosmosData = arm.CosmosData
75+
76+
func ResourceIDToCosmosID(resourceID *azcorearm.ResourceID) (string, error) {
77+
if resourceID == nil {
78+
return "", errors.New("resource ID is nil")
79+
}
80+
return ResourceIDStringToCosmosID(resourceID.String())
81+
}
82+
83+
func ResourceIDStringToCosmosID(resourceID string) (string, error) {
84+
if len(resourceID) == 0 {
85+
return "", errors.New("resource ID is empty")
86+
}
87+
// cosmos uses a REST API, which means that IDs that contain slashes cause problems with URL handling.
88+
// We chose | because that is a delimiter that is not allowed inside of an ARM resource ID because it is a separator
89+
// for multiple resource IDs.
90+
return strings.ReplaceAll(strings.ToLower(resourceID), "/", "|"), nil
91+
}
92+
93+
func CosmosIDToResourceID(resourceID string) (*azcorearm.ResourceID, error) {
94+
return azcorearm.ParseResourceID(strings.ReplaceAll(resourceID, "|", "/"))
95+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2025 Microsoft Corporation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package api
16+
17+
import (
18+
azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
19+
)
20+
21+
// ServiceProviderCluster is used internally by controllers to track and pass information between them.
22+
type ServiceProviderCluster struct {
23+
// CosmosMetadata ResourceID is nested under the cluster so that association and cleanup work as expected
24+
// it will be the ServiceProviderCluster type and the name default
25+
CosmosMetadata `json:"cosmosMetadata"`
26+
27+
// resourceID exists to match cosmosMetadata.resourceID until we're able to transition all types to use cosmosMetadata,
28+
// at which point we will stop using properties.resourceId in our queries. That will be about a month from now.
29+
ResourceID azcorearm.ResourceID `json:"resourceId"`
30+
31+
LoadBalancerResourceID *azcorearm.ResourceID `json:"loadBalancerResourceID,omitempty"`
32+
}

internal/database/convert_any.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ func CosmosToInternal[InternalAPIType, CosmosAPIType any](obj *CosmosAPIType) (*
5353
return nil, fmt.Errorf("unexpected return type: %T", castObj)
5454
}
5555

56+
case *GenericDocument[InternalAPIType]:
57+
internalObj, err = CosmosGenericToInternal[InternalAPIType](cosmosObj)
58+
5659
default:
5760
return nil, fmt.Errorf("unknown type %T", cosmosObj)
5861
}
@@ -100,7 +103,7 @@ func InternalToCosmos[InternalAPIType, CosmosAPIType any](obj *InternalAPIType)
100103
}
101104

102105
default:
103-
return nil, fmt.Errorf("unknown type %T", internalObj)
106+
cosmosObj, err = InternalToCosmosGeneric[InternalAPIType](obj)
104107
}
105108

106109
if err != nil {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Copyright 2025 Microsoft Corporation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package database
16+
17+
import (
18+
"fmt"
19+
"strings"
20+
21+
"k8s.io/utils/ptr"
22+
23+
"github.com/Azure/ARO-HCP/internal/api"
24+
)
25+
26+
func InternalToCosmosGeneric[InternalAPIType any](internalObj *InternalAPIType) (*GenericDocument[InternalAPIType], error) {
27+
if internalObj == nil {
28+
return nil, nil
29+
}
30+
31+
metadata, ok := any(internalObj).(api.CosmosMetadataAccessor)
32+
if !ok {
33+
return nil, fmt.Errorf("internalObj must be an api.CosmosMetadataAccessor: %T", internalObj)
34+
}
35+
36+
cosmosID, err := api.ResourceIDToCosmosID(ptr.To(metadata.GetResourceID()))
37+
if err != nil {
38+
return nil, err
39+
}
40+
41+
cosmosObj := &GenericDocument[InternalAPIType]{
42+
TypedDocument: TypedDocument{
43+
BaseDocument: BaseDocument{
44+
ID: cosmosID,
45+
},
46+
PartitionKey: strings.ToLower(metadata.GetResourceID().SubscriptionID),
47+
ResourceType: metadata.GetResourceID().ResourceType.String(),
48+
},
49+
Content: *internalObj,
50+
}
51+
52+
return cosmosObj, nil
53+
}
54+
55+
func CosmosGenericToInternal[InternalAPIType any](cosmosObj *GenericDocument[InternalAPIType]) (*InternalAPIType, error) {
56+
if cosmosObj == nil {
57+
return nil, nil
58+
}
59+
60+
return &cosmosObj.Content, nil
61+
}

internal/database/crud_helpers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func getByItemID[InternalAPIType, CosmosAPIType any](ctx context.Context, contai
6363

6464
func get[InternalAPIType, CosmosAPIType any](ctx context.Context, containerClient *azcosmos.ContainerClient, partitionKeyString string, completeResourceID *azcorearm.ResourceID) (*InternalAPIType, error) {
6565
// try to see if the cosmosID we've passed is also the exact resource ID. If so, then return the value we got.
66-
if exactCosmosID, err := resourceIDToCosmosID(completeResourceID); err == nil {
66+
if exactCosmosID, err := api.ResourceIDToCosmosID(completeResourceID); err == nil {
6767
ret, err := getByItemID[InternalAPIType, CosmosAPIType](ctx, containerClient, partitionKeyString, exactCosmosID)
6868
if err == nil {
6969
return ret, nil
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Copyright 2025 Microsoft Corporation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package database
16+
17+
import (
18+
"path"
19+
"strings"
20+
21+
azcorearm "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
22+
23+
"github.com/Azure/ARO-HCP/internal/api"
24+
)
25+
26+
type ServiceProviderClusterCRUD interface {
27+
ResourceCRUD[api.ServiceProviderCluster]
28+
}
29+
30+
func NewClusterResourceID(subscriptionID, resourceGroupName, clusterName string) *azcorearm.ResourceID {
31+
parts := []string{
32+
"/subscriptions",
33+
strings.ToLower(subscriptionID),
34+
"resourceGroups",
35+
resourceGroupName,
36+
"providers",
37+
api.ClusterResourceType.Namespace,
38+
api.ClusterResourceType.Type,
39+
clusterName,
40+
}
41+
return api.Must(azcorearm.ParseResourceID(strings.ToLower(path.Join(parts...))))
42+
}

internal/database/database.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
2929
"github.com/Azure/azure-sdk-for-go/sdk/data/azcosmos"
3030

31+
"github.com/Azure/ARO-HCP/internal/api"
3132
"github.com/Azure/ARO-HCP/internal/utils"
3233
)
3334

@@ -118,6 +119,8 @@ type DBClient interface {
118119
Operations(subscriptionID string) OperationCRUD
119120

120121
Subscriptions() SubscriptionCRUD
122+
123+
ServiceProviderClusters(subscriptionID, resourceGroupName, clusterName string) ServiceProviderClusterCRUD
121124
}
122125

123126
var _ DBClient = &cosmosDBClient{}
@@ -270,6 +273,12 @@ func (d *cosmosDBClient) Subscriptions() SubscriptionCRUD {
270273
return NewSubscriptionCRUD(d.resources)
271274
}
272275

276+
func (d *cosmosDBClient) ServiceProviderClusters(subscriptionID, resourceGroupName, clusterName string) ServiceProviderClusterCRUD {
277+
clusterResourceID := NewClusterResourceID(subscriptionID, resourceGroupName, clusterName)
278+
return NewCosmosResourceCRUD[api.ServiceProviderCluster, GenericDocument[api.ServiceProviderCluster]](
279+
d.resources, clusterResourceID, api.ServiceProviderClusterResourceType)
280+
}
281+
273282
func (d *cosmosDBClient) UntypedCRUD(parentResourceID azcorearm.ResourceID) (UntypedResourceCRUD, error) {
274283
return NewUntypedCRUD(d.resources, parentResourceID), nil
275284
}

internal/database/types_generic.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Copyright 2025 Microsoft Corporation
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package database
16+
17+
// pointer of InternalAPIType needs to be api.CosmosMetadataAccessor
18+
type GenericDocument[InternalAPIType any] struct {
19+
TypedDocument `json:",inline"`
20+
21+
Content InternalAPIType `json:"properties"`
22+
}

internal/mocks/dbclient.go

Lines changed: 38 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)