Skip to content

Commit 86cdb96

Browse files
makdenissvertex451renovate[bot]
authored
Fix stale definitions files on ClusterAccess update/delete (#82)
* ensured that we have ClusterAccess CRD installed * wait for CRD to be fully registered * addressed comments * removed TOCTOU race * fix(deps): update module sigs.k8s.io/controller-runtime to v0.22.2 (#67) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix(deps): update module golang.org/x/text to v0.30.0 (#70) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Fix: relations nil pointer check (#68) * added nil checks in relation resovling On-behalf-of: @SAP [email protected] Signed-off-by: Artem Shcherbatiuk <[email protected]> * fix(deps): update golang.org/x/exp digest to d2f985d (#71) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * improved docs * removed testing script due for the simplicity of support * feat: cluster access definitions deletion (#69) * feat: refine cluster access definitions deletion (#69) * feat: add tests for cluster access definitions deletion and edit (#69) * fix: use the observed path for path changes (#69) * fix: fix the finalizerName value (#69) --------- Signed-off-by: Artem Shcherbatiuk <[email protected]> Co-authored-by: Artem Shcherbatiuk <[email protected]> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
1 parent 1ba53ae commit 86cdb96

File tree

4 files changed

+186
-1
lines changed

4 files changed

+186
-1
lines changed

common/apis/v1alpha1/clusteraccess_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ type ClusterAccessStatus struct {
8383
// Conditions represent the latest available observations of the cluster access state
8484
// +optional
8585
Conditions []metav1.Condition `json:"conditions,omitempty"`
86+
87+
// ObservedPath stores the actual path currently used to store the schema for this resource
88+
// This is maintained by the controller and used for cleanup when .spec.path changes
89+
// +optional
90+
ObservedPath string `json:"observedPath,omitempty"`
8691
}
8792

8893
type ServiceAccountRef struct {

config/crd/gateway.platform-mesh.io_clusteraccesses.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,11 @@ spec:
197197
- type
198198
type: object
199199
type: array
200+
observedPath:
201+
description: |-
202+
ObservedPath stores the actual path currently used to store the schema for this resource
203+
This is maintained by the controller and used for cleanup when .spec.path changes
204+
type: string
200205
type: object
201206
type: object
202207
served: true

listener/reconciler/clusteraccess/subroutines.go

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import (
1717
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
1818
)
1919

20+
// generateSchemaSubroutine processes ClusterAccess resources and generates schemas
21+
const (
22+
finalizerName = "gateway.platform-mesh.io/clusteraccess-finalizer"
23+
)
24+
2025
// generateSchemaSubroutine processes ClusterAccess resources and generates schemas
2126
type generateSchemaSubroutine struct {
2227
reconciler *ClusterAccessReconciler
@@ -72,12 +77,33 @@ func (s *generateSchemaSubroutine) Process(ctx context.Context, instance runtime
7277
return ctrl.Result{}, commonserrors.NewOperatorError(err, false, false)
7378
}
7479

80+
// If path changed, delete the old schema file referenced in the status
81+
prevPath := clusterAccess.Status.ObservedPath
82+
if prevPath != "" && prevPath != clusterName {
83+
if err := s.reconciler.ioHandler.Delete(prevPath); err != nil {
84+
// Log and continue; do not fail reconciliation on cleanup issues
85+
s.reconciler.log.Warn().Err(err).Str("previousPath", prevPath).Str("clusterAccess", clusterAccessName).Msg("failed to delete previous schema file")
86+
}
87+
}
88+
7589
// Write schema to file using cluster name from path or resource name
7690
if err := s.reconciler.ioHandler.Write(schemaWithMetadata, clusterName); err != nil {
7791
s.reconciler.log.Error().Err(err).Str("clusterAccess", clusterAccessName).Msg("failed to write schema")
7892
return ctrl.Result{}, commonserrors.NewOperatorError(err, false, false)
7993
}
8094

95+
// Update status.ObservedPath to reflect the current observed path
96+
if prevPath != clusterName {
97+
obj := clusterAccess.DeepCopy()
98+
obj.Status.ObservedPath = clusterName
99+
if err := s.reconciler.opts.Client.Status().Update(ctx, obj); err != nil {
100+
// Log but do not fail reconciliation; file has been written already
101+
s.reconciler.log.Warn().Err(err).Str("clusterAccess", clusterAccessName).Msg("failed to update observed path in status")
102+
} else {
103+
clusterAccess.Status.ObservedPath = obj.Status.ObservedPath
104+
}
105+
}
106+
81107
s.reconciler.log.Info().Str("clusterAccess", clusterAccessName).Msg("successfully processed ClusterAccess resource")
82108
return ctrl.Result{}, nil
83109
}
@@ -97,6 +123,33 @@ func (s *generateSchemaSubroutine) restMapperFromConfig(cfg *rest.Config) (meta.
97123
}
98124

99125
func (s *generateSchemaSubroutine) Finalize(ctx context.Context, instance runtimeobject.RuntimeObject) (ctrl.Result, commonserrors.OperatorError) {
126+
clusterAccess, ok := instance.(*gatewayv1alpha1.ClusterAccess)
127+
if !ok {
128+
s.reconciler.log.Error().Msg("instance is not a ClusterAccess resource in Finalize")
129+
return ctrl.Result{}, commonserrors.NewOperatorError(errors.New("invalid resource type"), false, false)
130+
}
131+
132+
// Determine current and previously used paths
133+
currentPath := clusterAccess.Spec.Path
134+
if currentPath == "" {
135+
currentPath = clusterAccess.GetName()
136+
}
137+
prevPath := clusterAccess.Status.ObservedPath
138+
139+
// Try deleting current path file
140+
if currentPath != "" {
141+
if err := s.reconciler.ioHandler.Delete(currentPath); err != nil {
142+
// Log and continue; do not block finalization just because file was missing or deletion failed
143+
s.reconciler.log.Warn().Err(err).Str("path", currentPath).Str("clusterAccess", clusterAccess.GetName()).Msg("failed to delete schema file during finalization")
144+
}
145+
}
146+
// If previous differs, try deleting it as well
147+
if prevPath != "" && prevPath != currentPath {
148+
if err := s.reconciler.ioHandler.Delete(prevPath); err != nil {
149+
s.reconciler.log.Warn().Err(err).Str("path", prevPath).Str("clusterAccess", clusterAccess.GetName()).Msg("failed to delete previous schema file during finalization")
150+
}
151+
}
152+
100153
return ctrl.Result{}, nil
101154
}
102155

@@ -105,5 +158,5 @@ func (s *generateSchemaSubroutine) GetName() string {
105158
}
106159

107160
func (s *generateSchemaSubroutine) Finalizers() []string {
108-
return nil
161+
return []string{finalizerName}
109162
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package clusteraccess
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/mock"
9+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/runtime"
11+
"k8s.io/client-go/rest"
12+
ctrl "sigs.k8s.io/controller-runtime"
13+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
14+
15+
"github.com/platform-mesh/golang-commons/logger"
16+
gatewayv1alpha1 "github.com/platform-mesh/kubernetes-graphql-gateway/common/apis/v1alpha1"
17+
workspacefile_mocks "github.com/platform-mesh/kubernetes-graphql-gateway/listener/pkg/workspacefile/mocks"
18+
"github.com/platform-mesh/kubernetes-graphql-gateway/listener/reconciler"
19+
)
20+
21+
func TestGenerateSchemaSubroutine_Process_InvalidResourceType(t *testing.T) {
22+
mockIO := workspacefile_mocks.NewMockIOHandler(t)
23+
log, _ := logger.New(logger.DefaultConfig())
24+
25+
r := &ClusterAccessReconciler{
26+
ioHandler: mockIO,
27+
log: log,
28+
}
29+
s := &generateSchemaSubroutine{reconciler: r}
30+
31+
_, opErr := s.Process(context.Background(), &metav1.PartialObjectMetadata{})
32+
33+
assert.NotNil(t, opErr)
34+
}
35+
36+
func TestGenerateSchemaSubroutine_Process_MissingHostReturnsError(t *testing.T) {
37+
scheme := runtime.NewScheme()
38+
_ = gatewayv1alpha1.AddToScheme(scheme)
39+
40+
ca := &gatewayv1alpha1.ClusterAccess{
41+
ObjectMeta: metav1.ObjectMeta{
42+
Name: "test-cluster",
43+
Annotations: map[string]string{},
44+
},
45+
Spec: gatewayv1alpha1.ClusterAccessSpec{
46+
// Host is intentionally empty to trigger validation error
47+
},
48+
}
49+
50+
fakeClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(ca).Build()
51+
52+
mockIO := workspacefile_mocks.NewMockIOHandler(t)
53+
mockIO.EXPECT().Write(mock.Anything, mock.Anything).Maybe().Return(nil)
54+
mockIO.EXPECT().Delete(mock.Anything).Maybe().Return(nil)
55+
56+
log, _ := logger.New(logger.DefaultConfig())
57+
58+
r := &ClusterAccessReconciler{
59+
ioHandler: mockIO,
60+
log: log,
61+
opts: reconciler.ReconcilerOpts{
62+
Client: fakeClient,
63+
Config: &rest.Config{Host: "https://unit-test.invalid"},
64+
ManagerOpts: ctrl.Options{Scheme: scheme},
65+
Scheme: scheme,
66+
},
67+
}
68+
s := &generateSchemaSubroutine{reconciler: r}
69+
70+
res, opErr := s.Process(context.Background(), ca)
71+
72+
assert.NotNil(t, opErr)
73+
assert.Equal(t, ctrl.Result{}, res)
74+
}
75+
76+
func TestGenerateSchemaSubroutine_Finalize_DeletesCurrentAndPreviousPaths(t *testing.T) {
77+
mockIO := workspacefile_mocks.NewMockIOHandler(t)
78+
log, _ := logger.New(logger.DefaultConfig())
79+
80+
// Expect deletion of both current and previous paths
81+
mockIO.EXPECT().Delete("current-path").Return(nil).Once()
82+
mockIO.EXPECT().Delete("previous-path").Return(nil).Once()
83+
84+
r := &ClusterAccessReconciler{
85+
ioHandler: mockIO,
86+
log: log,
87+
}
88+
s := &generateSchemaSubroutine{reconciler: r}
89+
90+
ca := &gatewayv1alpha1.ClusterAccess{
91+
ObjectMeta: metav1.ObjectMeta{
92+
Name: "my-resource",
93+
},
94+
Spec: gatewayv1alpha1.ClusterAccessSpec{
95+
Path: "current-path",
96+
},
97+
Status: gatewayv1alpha1.ClusterAccessStatus{
98+
ObservedPath: "previous-path",
99+
},
100+
}
101+
102+
res, opErr := s.Finalize(context.Background(), ca)
103+
104+
assert.Nil(t, opErr)
105+
assert.Equal(t, ctrl.Result{}, res)
106+
}
107+
108+
func TestGenerateSchemaSubroutine_restMapperFromConfig_SucceedsWithMinimalConfig(t *testing.T) {
109+
mockIO := workspacefile_mocks.NewMockIOHandler(t)
110+
log, _ := logger.New(logger.DefaultConfig())
111+
112+
r := &ClusterAccessReconciler{
113+
ioHandler: mockIO,
114+
log: log,
115+
}
116+
s := &generateSchemaSubroutine{reconciler: r}
117+
118+
rm, err := s.restMapperFromConfig(&rest.Config{})
119+
120+
assert.NotNil(t, rm)
121+
assert.NoError(t, err)
122+
}

0 commit comments

Comments
 (0)