Skip to content

Commit 4ea5d6c

Browse files
Watch input secret and update deployment if a relevant password changes
1 parent 410264e commit 4ea5d6c

File tree

3 files changed

+145
-12
lines changed

3 files changed

+145
-12
lines changed

api/v1beta1/placementapi_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,8 @@ func SetupDefaults() {
222222

223223
SetupPlacementAPIDefaults(placementDefaults)
224224
}
225+
226+
// GetSecret returns the value of the Nova.Spec.Secret
227+
func (instance PlacementAPI) GetSecret() string {
228+
return instance.Spec.Secret
229+
}

controllers/placementapi_controller.go

Lines changed: 138 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,17 @@ import (
2121
"fmt"
2222
"time"
2323

24+
apimeta "k8s.io/apimachinery/pkg/api/meta"
2425
"k8s.io/apimachinery/pkg/runtime"
26+
"k8s.io/apimachinery/pkg/types"
2527
"k8s.io/client-go/kubernetes"
2628
ctrl "sigs.k8s.io/controller-runtime"
2729
"sigs.k8s.io/controller-runtime/pkg/client"
2830
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
31+
"sigs.k8s.io/controller-runtime/pkg/handler"
2932
"sigs.k8s.io/controller-runtime/pkg/log"
33+
"sigs.k8s.io/controller-runtime/pkg/reconcile"
34+
"sigs.k8s.io/controller-runtime/pkg/source"
3035

3136
"github.com/go-logr/logr"
3237
keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1"
@@ -42,7 +47,6 @@ import (
4247
labels "github.com/openstack-k8s-operators/lib-common/modules/common/labels"
4348
nad "github.com/openstack-k8s-operators/lib-common/modules/common/networkattachment"
4449
common_rbac "github.com/openstack-k8s-operators/lib-common/modules/common/rbac"
45-
oko_secret "github.com/openstack-k8s-operators/lib-common/modules/common/secret"
4650
"github.com/openstack-k8s-operators/lib-common/modules/common/service"
4751
util "github.com/openstack-k8s-operators/lib-common/modules/common/util"
4852

@@ -58,11 +62,133 @@ import (
5862
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
5963
)
6064

65+
type conditionUpdater interface {
66+
Set(c *condition.Condition)
67+
MarkTrue(t condition.Type, messageFormat string, messageArgs ...interface{})
68+
}
69+
70+
type GetSecret interface {
71+
GetSecret() string
72+
client.Object
73+
}
74+
75+
// ensureSecret - ensures that the Secret object exists and the expected fields
76+
// are in the Secret. It returns a hash of the values of the expected fields.
77+
func ensureSecret(
78+
ctx context.Context,
79+
secretName types.NamespacedName,
80+
expectedFields []string,
81+
reader client.Reader,
82+
conditionUpdater conditionUpdater,
83+
) (string, ctrl.Result, corev1.Secret, error) {
84+
secret := &corev1.Secret{}
85+
err := reader.Get(ctx, secretName, secret)
86+
if err != nil {
87+
if k8s_errors.IsNotFound(err) {
88+
conditionUpdater.Set(condition.FalseCondition(
89+
condition.InputReadyCondition,
90+
condition.RequestedReason,
91+
condition.SeverityInfo,
92+
fmt.Sprintf("Input data resources missing: %s", "secret/"+secretName.Name)))
93+
return "",
94+
ctrl.Result{},
95+
*secret,
96+
fmt.Errorf("Secret %s not found", secretName)
97+
}
98+
conditionUpdater.Set(condition.FalseCondition(
99+
condition.InputReadyCondition,
100+
condition.ErrorReason,
101+
condition.SeverityWarning,
102+
condition.InputReadyErrorMessage,
103+
err.Error()))
104+
return "", ctrl.Result{}, *secret, err
105+
}
106+
107+
// collect the secret values the caller expects to exist
108+
values := [][]byte{}
109+
for _, field := range expectedFields {
110+
val, ok := secret.Data[field]
111+
if !ok {
112+
err := fmt.Errorf("field '%s' not found in secret/%s", field, secretName.Name)
113+
conditionUpdater.Set(condition.FalseCondition(
114+
condition.InputReadyCondition,
115+
condition.ErrorReason,
116+
condition.SeverityWarning,
117+
condition.InputReadyErrorMessage,
118+
err.Error()))
119+
return "", ctrl.Result{}, *secret, err
120+
}
121+
values = append(values, val)
122+
}
123+
124+
// TODO(gibi): Do we need to watch the Secret for changes?
125+
126+
hash, err := util.ObjectHash(values)
127+
if err != nil {
128+
conditionUpdater.Set(condition.FalseCondition(
129+
condition.InputReadyCondition,
130+
condition.ErrorReason,
131+
condition.SeverityWarning,
132+
condition.InputReadyErrorMessage,
133+
err.Error()))
134+
return "", ctrl.Result{}, *secret, err
135+
}
136+
137+
return hash, ctrl.Result{}, *secret, nil
138+
}
139+
61140
// GetLog returns a logger object with a prefix of "controller.name" and additional controller context fields
62141
func (r *PlacementAPIReconciler) GetLogger(ctx context.Context) logr.Logger {
63142
return log.FromContext(ctx).WithName("Controllers").WithName("PlacementAPI")
64143
}
65144

145+
func (r *PlacementAPIReconciler) GetSecretMapperFor(crs client.ObjectList, ctx context.Context) func(client.Object) []reconcile.Request {
146+
Log := r.GetLogger(ctx)
147+
mapper := func(secret client.Object) []reconcile.Request {
148+
var namespace string = secret.GetNamespace()
149+
var secretName string = secret.GetName()
150+
result := []reconcile.Request{}
151+
152+
listOpts := []client.ListOption{
153+
client.InNamespace(namespace),
154+
}
155+
if err := r.Client.List(ctx, crs, listOpts...); err != nil {
156+
Log.Error(err, "Unable to retrieve the list of CRs")
157+
panic(err)
158+
}
159+
160+
err := apimeta.EachListItem(crs, func(o runtime.Object) error {
161+
// NOTE(gibi): intentionally let the failed cast panic to catch
162+
// this implementation error as soon as possible.
163+
cr := o.(GetSecret)
164+
if cr.GetSecret() == secretName {
165+
name := client.ObjectKey{
166+
Namespace: namespace,
167+
Name: cr.GetName(),
168+
}
169+
Log.Info(
170+
"Requesting reconcile due to secret change",
171+
"Secret", secretName, "CR", name.Name,
172+
)
173+
result = append(result, reconcile.Request{NamespacedName: name})
174+
}
175+
return nil
176+
})
177+
178+
if err != nil {
179+
Log.Error(err, "Unable to iterate the list of CRs")
180+
panic(err)
181+
}
182+
183+
if len(result) > 0 {
184+
return result
185+
}
186+
return nil
187+
}
188+
189+
return mapper
190+
}
191+
66192
// PlacementAPIReconciler reconciles a PlacementAPI object
67193
type PlacementAPIReconciler struct {
68194
client.Client
@@ -207,6 +333,8 @@ func (r *PlacementAPIReconciler) SetupWithManager(mgr ctrl.Manager) error {
207333
Owns(&corev1.ServiceAccount{}).
208334
Owns(&rbacv1.Role{}).
209335
Owns(&rbacv1.RoleBinding{}).
336+
Watches(&source.Kind{Type: &corev1.Secret{}},
337+
handler.EnqueueRequestsFromMapFunc(r.GetSecretMapperFor(&placementv1.PlacementAPIList{}, context.TODO()))).
210338
Complete(r)
211339
}
212340

@@ -591,7 +719,14 @@ func (r *PlacementAPIReconciler) reconcileNormal(ctx context.Context, instance *
591719
//
592720
// check for required OpenStack secret holding passwords for service/admin user and add hash to the vars map
593721
//
594-
ospSecret, hash, err := oko_secret.GetSecret(ctx, helper, instance.Spec.Secret, instance.Namespace)
722+
hash, result, ospSecret, err := ensureSecret(
723+
ctx,
724+
types.NamespacedName{Namespace: instance.Namespace, Name: instance.Spec.Secret},
725+
[]string{
726+
instance.Spec.PasswordSelectors.Service,
727+
},
728+
helper.GetClient(),
729+
&instance.Status.Conditions)
595730
if err != nil {
596731
if k8s_errors.IsNotFound(err) {
597732
instance.Status.Conditions.Set(condition.FalseCondition(
@@ -607,7 +742,7 @@ func (r *PlacementAPIReconciler) reconcileNormal(ctx context.Context, instance *
607742
condition.SeverityWarning,
608743
condition.InputReadyErrorMessage,
609744
err.Error()))
610-
return ctrl.Result{}, err
745+
return result, err
611746
}
612747
configMapVars[ospSecret.Name] = env.SetValue(hash)
613748
instance.Status.Conditions.MarkTrue(condition.InputReadyCondition, condition.InputReadyMessage)

tests/functional/placementapi_controller_test.go

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ var _ = Describe("PlacementAPI controller", func() {
170170
names.PlacementAPIName,
171171
ConditionGetterFunc(PlacementConditionGetter),
172172
condition.InputReadyCondition,
173-
corev1.ConditionTrue,
173+
corev1.ConditionFalse,
174174
)
175175
})
176176
})
@@ -704,14 +704,7 @@ var _ = Describe("PlacementAPI controller", func() {
704704
Eventually(func(g Gomega) {
705705
deployment := th.GetDeployment(names.DeploymentName)
706706
newConfigHash := GetEnvVarValue(deployment.Spec.Template.Spec.Containers[0].Env, "CONFIG_HASH", "")
707-
g.Expect(newConfigHash).NotTo(Equal(""))
708-
// FIXME(gibi): The placement-operator does not watch the input
709-
// secret so it does not detect that any input is changed.
710-
// Also the password values are not calculated into the input
711-
// hash as they are only applied in the init container
712-
// This should pass when this is fixed
713-
// g.Expect(newConfigHash).NotTo(Equal(oldConfigHash))
714-
g.Expect(newConfigHash).To(Equal(oldConfigHash))
707+
g.Expect(newConfigHash).NotTo(Equal(oldConfigHash))
715708
// TODO(gibi): once the password is in the generated config
716709
// assert it there
717710
}, timeout, interval).Should(Succeed())

0 commit comments

Comments
 (0)