Skip to content

Commit 4bd1dd2

Browse files
committed
controlplane: add non-intrusive EKS outputs status; keep secrets.go unchanged; update VERSION to 0.4.9 and CHANGELOG for v0.4.9
1 parent 9ac9b40 commit 4bd1dd2

File tree

6 files changed

+376
-1
lines changed

6 files changed

+376
-1
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,20 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [v0.4.9] - 2025-11-12
9+
10+
### Changed
11+
- CAPTControlPlane: `reconcileEKSOutputsStatus` を新規追加し、コントローラ本体(`controller.go`)からベストエフォートで呼び出して `status.eksOutputs` を更新するようにしました。既存の `secrets.go` の実装は変更せず後方互換を維持しています。
12+
- 参照する Workspace/Secret 名の解決は、WTA.Spec → Workspace.Spec → 既存命名候補の順でフォールバックし、`status.workspaceOutputsRef``EKSOutputsReady` Condition で待機状態を可視化します。
13+
14+
### Notes
15+
- Helm Chart のチャート版数は今回更新していません。
16+
817
## [v0.4.8] - 2025-11-11
918

19+
### Added
20+
- CAPTControlPlane: `status.eksOutputs`, `status.workspaceOutputsRef`, `status.eksOutputsChecksum` を追加。EKS Workspace の connection Secret から endpoint / CA / OIDC / ExternalDNS / Karpenter を型付きで収集し、`EKSOutputsReady` Condition を更新します(チェックサムで変化検知)。
21+
1022
### Fixed
1123
- Cascading deletion could fail in some paths due to missing ownerReferences on existing resources. Controllers now adopt pre-existing resources and ensure ownerReferences are present:
1224
- CAPTControlPlane main WorkspaceTemplateApply

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
VERSION = 0.4.8
1+
VERSION = 0.4.9

api/controlplane/v1beta1/captcontrolplane_types.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ const (
1919

2020
// ControlPlaneCreatingCondition indicates the control plane is being created
2121
ControlPlaneCreatingCondition = "Creating"
22+
23+
// EKSOutputsReadyCondition indicates the EKS outputs (from Workspace connection secret)
24+
// have been successfully parsed and stored on the status.
25+
EKSOutputsReadyCondition = "EKSOutputsReady"
2226
)
2327

2428
// Default timeout values
@@ -200,6 +204,53 @@ type WorkspaceStatus struct {
200204
AtProvider *runtime.RawExtension `json:"atProvider,omitempty"`
201205
}
202206

207+
// EKSOutputsStatus holds parsed outputs from the EKS Workspace connection Secret.
208+
type EKSOutputsStatus struct {
209+
ClusterName string `json:"clusterName,omitempty"`
210+
ClusterEndpoint string `json:"clusterEndpoint,omitempty"`
211+
ClusterCertificateAuthorityData string `json:"clusterCertificateAuthorityData,omitempty"`
212+
OIDCProvider string `json:"oidcProvider,omitempty"`
213+
OIDCProviderARN string `json:"oidcProviderArn,omitempty"`
214+
ExternalDNS *ExternalDNSStatus `json:"externalDNS,omitempty"`
215+
Karpenter *KarpenterStatus `json:"karpenter,omitempty"`
216+
}
217+
218+
type ExternalDNSStatus struct {
219+
IAMRoleARN string `json:"iamRoleArn,omitempty"`
220+
ServiceAccount *NamespacedNameStatus `json:"serviceAccount,omitempty"`
221+
}
222+
223+
type NamespacedNameStatus struct {
224+
Name string `json:"name,omitempty"`
225+
Namespace string `json:"namespace,omitempty"`
226+
}
227+
228+
type KarpenterStatus struct {
229+
DiscoveryTag *KVPStatus `json:"discoveryTag,omitempty"`
230+
EC2NodeClass *KarpenterNodeClass `json:"ec2NodeClass,omitempty"`
231+
QueueName string `json:"queueName,omitempty"`
232+
ServiceAccount *ServiceAccountStatus `json:"serviceAccount,omitempty"`
233+
}
234+
235+
type KarpenterNodeClass struct {
236+
Role string `json:"role,omitempty"`
237+
}
238+
239+
type KVPStatus struct {
240+
Key string `json:"key,omitempty"`
241+
Value string `json:"value,omitempty"`
242+
}
243+
244+
type ServiceAccountStatus struct {
245+
Annotations map[string]string `json:"annotations,omitempty"`
246+
}
247+
248+
// WorkspaceOutputsRef references the Secret containing the EKS outputs.
249+
type WorkspaceOutputsRef struct {
250+
Name string `json:"name,omitempty"`
251+
Namespace string `json:"namespace,omitempty"`
252+
}
253+
203254
// CAPTControlPlaneStatus defines the observed state of CAPTControlPlane
204255
type CAPTControlPlaneStatus struct {
205256
// Ready denotes that the control plane is ready
@@ -223,6 +274,18 @@ type CAPTControlPlaneStatus struct {
223274
// +optional
224275
WorkspaceStatus *WorkspaceStatus `json:"workspaceStatus,omitempty"`
225276

277+
// EKSOutputs contains parsed EKS connection details from the Workspace secret.
278+
// +optional
279+
EKSOutputs *EKSOutputsStatus `json:"eksOutputs,omitempty"`
280+
281+
// EKSOutputsChecksum is a checksum of the normalized EKSOutputs for change detection.
282+
// +optional
283+
EKSOutputsChecksum string `json:"eksOutputsChecksum,omitempty"`
284+
285+
// WorkspaceOutputsRef holds a reference to the source Secret for the EKSOutputs.
286+
// +optional
287+
WorkspaceOutputsRef *WorkspaceOutputsRef `json:"workspaceOutputsRef,omitempty"`
288+
226289
// FailureReason indicates that there is a terminal problem reconciling the
227290
// state, and will be set to a token value suitable for programmatic
228291
// interpretation.

internal/controller/controlplane/controller.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu
518518
}
519519
}
520520

521+
// Best-effort: Update EKS outputs status from the connection secret (does not alter secrets.go path)
522+
_ = r.reconcileEKSOutputsStatus(ctx, controlPlane, workspaceApply)
523+
521524
// Fetch the final updated object
522525
if err := r.Get(ctx, req.NamespacedName, controlPlane); err != nil {
523526
return ctrl.Result{}, err
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
package controlplane
2+
3+
import (
4+
"context"
5+
"crypto/sha256"
6+
"encoding/hex"
7+
"encoding/json"
8+
"fmt"
9+
"strings"
10+
11+
controlplanev1beta1 "github.com/appthrust/capt/api/controlplane/v1beta1"
12+
infrastructurev1beta1 "github.com/appthrust/capt/api/v1beta1"
13+
corev1 "k8s.io/api/core/v1"
14+
apierrors "k8s.io/apimachinery/pkg/api/errors"
15+
"k8s.io/apimachinery/pkg/api/meta"
16+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
18+
"k8s.io/apimachinery/pkg/runtime/schema"
19+
"sigs.k8s.io/controller-runtime/pkg/client"
20+
"sigs.k8s.io/controller-runtime/pkg/log"
21+
)
22+
23+
// reconcileEKSOutputsStatus collects EKS connection outputs from the Workspace connection Secret
24+
// and updates CAPTControlPlane status fields without modifying the legacy secrets flow.
25+
func (r *Reconciler) reconcileEKSOutputsStatus(
26+
ctx context.Context,
27+
controlPlane *controlplanev1beta1.CAPTControlPlane,
28+
workspaceApply *infrastructurev1beta1.WorkspaceTemplateApply,
29+
) error {
30+
logger := log.FromContext(ctx)
31+
logger.Info("Reconciling EKS outputs status (non-intrusive)")
32+
33+
// Workspace must exist to resolve accurate secret reference
34+
if workspaceApply == nil || workspaceApply.Status.WorkspaceName == "" {
35+
return nil
36+
}
37+
38+
// Get workspace (unstructured)
39+
workspace := &unstructured.Unstructured{}
40+
workspace.SetGroupVersionKind(schema.GroupVersionKind{
41+
Group: "tf.upbound.io",
42+
Version: "v1beta1",
43+
Kind: "Workspace",
44+
})
45+
if err := r.Get(ctx, client.ObjectKey{
46+
Name: workspaceApply.Status.WorkspaceName,
47+
Namespace: workspaceApply.Namespace,
48+
}, workspace); err != nil {
49+
// Best-effort; do not fail reconciliation chain
50+
logger.Error(err, "Failed to get workspace for EKS outputs status")
51+
return nil
52+
}
53+
54+
// Resolve connection secret name (compatibility-first)
55+
var resolvedName, resolvedNamespace string
56+
if workspaceApply.Spec.WriteConnectionSecretToRef != nil {
57+
resolvedName = workspaceApply.Spec.WriteConnectionSecretToRef.Name
58+
resolvedNamespace = workspaceApply.Spec.WriteConnectionSecretToRef.Namespace
59+
}
60+
// Fallback to Workspace spec reference if not set
61+
if resolvedName == "" {
62+
if wsSecName, found, _ := unstructured.NestedString(workspace.Object, "spec", "writeConnectionSecretToRef", "name"); found && wsSecName != "" {
63+
resolvedName = wsSecName
64+
if wsSecNs, foundNs, _ := unstructured.NestedString(workspace.Object, "spec", "writeConnectionSecretToRef", "namespace"); foundNs && wsSecNs != "" {
65+
resolvedNamespace = wsSecNs
66+
} else {
67+
resolvedNamespace = workspace.GetNamespace()
68+
}
69+
}
70+
}
71+
// Derive candidates if still unknown
72+
candidates := []string{}
73+
if resolvedName == "" {
74+
wsName := workspace.GetName()
75+
if strings.HasSuffix(wsName, "-eks-controlplane") {
76+
candidates = append(candidates, strings.TrimSuffix(wsName, "-eks-controlplane")+"-eks-connection")
77+
}
78+
if strings.HasSuffix(wsName, "-controlplane") {
79+
candidates = append(candidates, strings.TrimSuffix(wsName, "-controlplane")+"-connection")
80+
}
81+
candidates = append(candidates, fmt.Sprintf("%s-eks-connection", controlPlane.Name))
82+
resolvedNamespace = workspace.GetNamespace()
83+
}
84+
85+
// Try to get the secret
86+
secret := &corev1.Secret{}
87+
var getErr error
88+
if resolvedName != "" {
89+
getErr = r.Get(ctx, client.ObjectKey{
90+
Name: resolvedName,
91+
Namespace: resolvedNamespace,
92+
}, secret)
93+
} else {
94+
// Probe candidates in order
95+
for _, name := range candidates {
96+
try := &corev1.Secret{}
97+
if err := r.Get(ctx, client.ObjectKey{Name: name, Namespace: resolvedNamespace}, try); err == nil {
98+
secret = try
99+
resolvedName = name
100+
getErr = nil
101+
break
102+
} else {
103+
getErr = err
104+
}
105+
}
106+
}
107+
108+
// Handle not found: record ref and exit gracefully
109+
if apierrors.IsNotFound(getErr) || (resolvedName == "" && getErr == nil && secret.Name == "") {
110+
patchBase := controlPlane.DeepCopy()
111+
// Prefer showing first candidate if any, otherwise keep last resolved
112+
refName := resolvedName
113+
if refName == "" && len(candidates) > 0 {
114+
refName = candidates[0]
115+
}
116+
controlPlane.Status.WorkspaceOutputsRef = &controlplanev1beta1.WorkspaceOutputsRef{
117+
Name: refName,
118+
Namespace: resolvedNamespace,
119+
}
120+
meta.SetStatusCondition(&controlPlane.Status.Conditions, metav1.Condition{
121+
Type: controlplanev1beta1.EKSOutputsReadyCondition,
122+
Status: metav1.ConditionFalse,
123+
Reason: "NotFound",
124+
Message: "Waiting for workspace connection secret to be created",
125+
})
126+
_ = r.Status().Patch(ctx, controlPlane, client.MergeFrom(patchBase))
127+
return nil
128+
}
129+
if getErr != nil {
130+
// Transient error; log and exit
131+
logger.Error(getErr, "Failed to get EKS connection secret")
132+
return nil
133+
}
134+
135+
// Build outputs and compute checksum
136+
outputs, checksum, err := buildEKSOutputsFromSecret(secret)
137+
if err != nil {
138+
patchBase := controlPlane.DeepCopy()
139+
meta.SetStatusCondition(&controlPlane.Status.Conditions, metav1.Condition{
140+
Type: controlplanev1beta1.EKSOutputsReadyCondition,
141+
Status: metav1.ConditionFalse,
142+
Reason: "ParseError",
143+
Message: fmt.Sprintf("Failed to parse EKS outputs: %v", err),
144+
})
145+
_ = r.Status().Patch(ctx, controlPlane, client.MergeFrom(patchBase))
146+
return nil
147+
}
148+
149+
// Patch status when changed
150+
patchBase := controlPlane.DeepCopy()
151+
if controlPlane.Status.EKSOutputsChecksum != checksum || controlPlane.Status.EKSOutputs == nil {
152+
controlPlane.Status.EKSOutputs = outputs
153+
controlPlane.Status.EKSOutputsChecksum = checksum
154+
}
155+
controlPlane.Status.WorkspaceOutputsRef = &controlplanev1beta1.WorkspaceOutputsRef{
156+
Name: resolvedName,
157+
Namespace: resolvedNamespace,
158+
}
159+
meta.SetStatusCondition(&controlPlane.Status.Conditions, metav1.Condition{
160+
Type: controlplanev1beta1.EKSOutputsReadyCondition,
161+
Status: metav1.ConditionTrue,
162+
Reason: "Success",
163+
Message: "EKS outputs parsed and stored",
164+
})
165+
if err := r.Status().Patch(ctx, controlPlane, client.MergeFrom(patchBase)); err != nil {
166+
logger.Error(err, "Failed to patch EKS outputs status")
167+
}
168+
169+
return nil
170+
}
171+
172+
// buildEKSOutputsFromSecret extracts and normalizes EKS outputs from the connection Secret.
173+
func buildEKSOutputsFromSecret(s *corev1.Secret) (*controlplanev1beta1.EKSOutputsStatus, string, error) {
174+
get := func(key string) (string, bool) {
175+
if s == nil || s.Data == nil {
176+
return "", false
177+
}
178+
b, ok := s.Data[key]
179+
return string(b), ok
180+
}
181+
182+
out := &controlplanev1beta1.EKSOutputsStatus{}
183+
184+
if v, ok := get("cluster_name"); ok {
185+
out.ClusterName = v
186+
}
187+
if v, ok := get("cluster_endpoint"); ok {
188+
out.ClusterEndpoint = v
189+
}
190+
if v, ok := get("cluster_certificate_authority_data"); ok {
191+
out.ClusterCertificateAuthorityData = v
192+
}
193+
if v, ok := get("oidc_provider"); ok {
194+
out.OIDCProvider = v
195+
}
196+
if v, ok := get("oidc_provider_arn"); ok {
197+
out.OIDCProviderARN = v
198+
}
199+
200+
if v, ok := get("external_dns"); ok && len(v) > 0 {
201+
var tmp struct {
202+
IAMRoleARN string `json:"iam_role_arn"`
203+
ServiceAccount struct {
204+
Name string `json:"name"`
205+
Namespace string `json:"namespace"`
206+
} `json:"service_account"`
207+
}
208+
if err := json.Unmarshal([]byte(v), &tmp); err != nil {
209+
return nil, "", fmt.Errorf("parse external_dns: %w", err)
210+
}
211+
out.ExternalDNS = &controlplanev1beta1.ExternalDNSStatus{
212+
IAMRoleARN: tmp.IAMRoleARN,
213+
ServiceAccount: &controlplanev1beta1.NamespacedNameStatus{
214+
Name: tmp.ServiceAccount.Name,
215+
Namespace: tmp.ServiceAccount.Namespace,
216+
},
217+
}
218+
}
219+
220+
if v, ok := get("karpenter"); ok && len(v) > 0 {
221+
var tmp struct {
222+
DiscoveryTag struct {
223+
Key string `json:"key"`
224+
Value string `json:"value"`
225+
} `json:"discovery_tag"`
226+
EC2NodeClass struct {
227+
Role string `json:"role"`
228+
} `json:"ec2_node_class"`
229+
QueueName string `json:"queue_name"`
230+
ServiceAccount struct {
231+
Annotations map[string]string `json:"annotations"`
232+
} `json:"service_account"`
233+
}
234+
if err := json.Unmarshal([]byte(v), &tmp); err != nil {
235+
return nil, "", fmt.Errorf("parse karpenter: %w", err)
236+
}
237+
out.Karpenter = &controlplanev1beta1.KarpenterStatus{
238+
DiscoveryTag: &controlplanev1beta1.KVPStatus{Key: tmp.DiscoveryTag.Key, Value: tmp.DiscoveryTag.Value},
239+
EC2NodeClass: &controlplanev1beta1.KarpenterNodeClass{Role: tmp.EC2NodeClass.Role},
240+
QueueName: tmp.QueueName,
241+
ServiceAccount: &controlplanev1beta1.ServiceAccountStatus{
242+
Annotations: tmp.ServiceAccount.Annotations,
243+
},
244+
}
245+
}
246+
247+
normalized, _ := json.Marshal(out)
248+
sum := sha256.Sum256(normalized)
249+
return out, hex.EncodeToString(sum[:]), nil
250+
}

0 commit comments

Comments
 (0)