Skip to content

Commit b14304e

Browse files
Extract shared controller helpers into internal/common
Move ensureSecret, ensureNetworkAttachments, ensureTopology, and shared errors from nova common into internal/common for reuse across controllers. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 988af71 commit b14304e

15 files changed

Lines changed: 349 additions & 215 deletions

internal/common/errors.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
Copyright 2026.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package common //nolint:revive // common is the established package name for multi-group shared code
18+
19+
import "errors"
20+
21+
// Static errors shared across operator services.
22+
var (
23+
// ErrACSecretNotFound indicates that the ApplicationCredential secret was not found.
24+
ErrACSecretNotFound = errors.New("ApplicationCredential secret not found")
25+
// ErrACSecretMissingKeys indicates that the ApplicationCredential secret is missing required keys.
26+
ErrACSecretMissingKeys = errors.New("ApplicationCredential secret missing required keys")
27+
// ErrTemplateDirUnset indicates that no template directory was provided.
28+
ErrTemplateDirUnset = errors.New("templateDir must be set")
29+
)

internal/common/kolla.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,18 @@ package common //nolint:revive // common is the established package name for mul
1919
import "fmt"
2020

2121
const (
22-
// ServiceCommand is the command used to start a service binary in Kolla containers.
22+
// ServiceCommand - the command to start the service binary in the kolla container
2323
ServiceCommand = "/usr/local/bin/kolla_start"
2424
)
2525

26-
// GetScriptSecretName returns the name of the Secret used for db sync scripts.
26+
// GetScriptSecretName returns the name of the Secret used for the
27+
// db sync scripts
2728
func GetScriptSecretName(crName string) string {
2829
return fmt.Sprintf("%s-scripts", crName)
2930
}
3031

31-
// GetServiceConfigSecretName returns the name of the Secret used to store service configuration.
32+
// GetServiceConfigSecretName returns the name of the Secret used to
33+
// store the service configuration files
3234
func GetServiceConfigSecretName(crName string) string {
3335
return fmt.Sprintf("%s-config-data", crName)
3436
}

internal/common/network.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
Copyright 2026.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package common //nolint:revive // common is the established package name for multi-group shared code
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"time"
23+
24+
networkv1 "github.com/k8snetworkplumbingwg/network-attachment-definition-client/pkg/apis/k8s.cni.cncf.io/v1"
25+
condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition"
26+
helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper"
27+
nad "github.com/openstack-k8s-operators/lib-common/modules/common/networkattachment"
28+
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
29+
ctrl "sigs.k8s.io/controller-runtime"
30+
"sigs.k8s.io/controller-runtime/pkg/log"
31+
)
32+
33+
// EnsureNetworkAttachments - checks the requested network attachments exists and
34+
// returns the annotation to be set on the deployment objects.
35+
func EnsureNetworkAttachments(
36+
ctx context.Context,
37+
h *helper.Helper,
38+
networkAttachments []string,
39+
conditionUpdater ConditionUpdater,
40+
requeueTimeout time.Duration,
41+
) (map[string]string, ctrl.Result, error) {
42+
var nadAnnotations map[string]string
43+
44+
// networks to attach to
45+
nadList := []networkv1.NetworkAttachmentDefinition{}
46+
for _, netAtt := range networkAttachments {
47+
nad, err := nad.GetNADWithName(ctx, h, netAtt, h.GetBeforeObject().GetNamespace())
48+
if err != nil {
49+
if k8s_errors.IsNotFound(err) {
50+
// Since the net-attach-def CR should have been manually created by the user and referenced in the spec,
51+
// we treat this as a warning because it means that the service will not be able to start.
52+
log.FromContext(ctx).Info(fmt.Sprintf("network-attachment-definition %s not found", netAtt))
53+
conditionUpdater.Set(condition.FalseCondition(
54+
condition.NetworkAttachmentsReadyCondition,
55+
condition.ErrorReason,
56+
condition.SeverityWarning,
57+
condition.NetworkAttachmentsReadyWaitingMessage,
58+
netAtt))
59+
return nadAnnotations, ctrl.Result{RequeueAfter: requeueTimeout}, nil
60+
}
61+
conditionUpdater.Set(condition.FalseCondition(
62+
condition.NetworkAttachmentsReadyCondition,
63+
condition.ErrorReason,
64+
condition.SeverityWarning,
65+
condition.NetworkAttachmentsErrorMessage,
66+
err.Error()))
67+
return nadAnnotations, ctrl.Result{}, err
68+
}
69+
70+
if nad != nil {
71+
nadList = append(nadList, *nad)
72+
}
73+
}
74+
75+
nadAnnotations, err := nad.EnsureNetworksAnnotation(nadList)
76+
if err != nil {
77+
return nadAnnotations, ctrl.Result{}, fmt.Errorf("failed create network annotation from %s: %w",
78+
networkAttachments, err)
79+
}
80+
81+
return nadAnnotations, ctrl.Result{}, nil
82+
}

internal/common/secret.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
Copyright 2026.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package common //nolint:revive // common is the established package name for multi-group shared code
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"time"
23+
24+
condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition"
25+
util "github.com/openstack-k8s-operators/lib-common/modules/common/util"
26+
corev1 "k8s.io/api/core/v1"
27+
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
28+
"k8s.io/apimachinery/pkg/types"
29+
ctrl "sigs.k8s.io/controller-runtime"
30+
"sigs.k8s.io/controller-runtime/pkg/client"
31+
"sigs.k8s.io/controller-runtime/pkg/log"
32+
)
33+
34+
// EnsureSecret ensures that the Secret object exists and the expected fields
35+
// are in the Secret. It returns a hash of the values of the expected fields.
36+
func EnsureSecret(
37+
ctx context.Context,
38+
secretName types.NamespacedName,
39+
expectedFields []string,
40+
reader client.Reader,
41+
conditionUpdater ConditionUpdater,
42+
requeueTimeout time.Duration,
43+
) (string, ctrl.Result, corev1.Secret, error) {
44+
secret := &corev1.Secret{}
45+
err := reader.Get(ctx, secretName, secret)
46+
if err != nil {
47+
if k8s_errors.IsNotFound(err) {
48+
// Since the service secret should have been manually created by the user and referenced in the spec,
49+
// we treat this as a warning because it means that the service will not be able to start.
50+
log.FromContext(ctx).Info(fmt.Sprintf("secret %s not found", secretName))
51+
conditionUpdater.Set(condition.FalseCondition(
52+
condition.InputReadyCondition,
53+
condition.ErrorReason,
54+
condition.SeverityWarning,
55+
"Input data resources missing: %s", "secret/"+secretName.Name))
56+
return "",
57+
ctrl.Result{RequeueAfter: requeueTimeout},
58+
*secret,
59+
nil
60+
}
61+
conditionUpdater.Set(condition.FalseCondition(
62+
condition.InputReadyCondition,
63+
condition.ErrorReason,
64+
condition.SeverityWarning,
65+
condition.InputReadyErrorMessage,
66+
err.Error()))
67+
return "", ctrl.Result{}, *secret, err
68+
}
69+
70+
// collect the secret values the caller expects to exist
71+
values := [][]byte{}
72+
for _, field := range expectedFields {
73+
val, ok := secret.Data[field]
74+
if !ok {
75+
err := fmt.Errorf("%w: '%s' not found in secret/%s", util.ErrFieldNotFound, field, secretName.Name)
76+
conditionUpdater.Set(condition.FalseCondition(
77+
condition.InputReadyCondition,
78+
condition.ErrorReason,
79+
condition.SeverityWarning,
80+
condition.InputReadyErrorMessage,
81+
err.Error()))
82+
return "", ctrl.Result{}, *secret, err
83+
}
84+
values = append(values, val)
85+
}
86+
87+
// TODO(gibi): Do we need to watch the Secret for changes?
88+
89+
hash, err := util.ObjectHash(values)
90+
if err != nil {
91+
conditionUpdater.Set(condition.FalseCondition(
92+
condition.InputReadyCondition,
93+
condition.ErrorReason,
94+
condition.SeverityWarning,
95+
condition.InputReadyErrorMessage,
96+
err.Error()))
97+
return "", ctrl.Result{}, *secret, err
98+
}
99+
100+
return hash, ctrl.Result{}, *secret, nil
101+
}

internal/common/topology.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
Copyright 2026.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package common //nolint:revive // common is the established package name for multi-group shared code
18+
19+
import (
20+
"context"
21+
"fmt"
22+
23+
topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1"
24+
condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition"
25+
helper "github.com/openstack-k8s-operators/lib-common/modules/common/helper"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27+
)
28+
29+
// topologyHandler provides topology reference state for a reconciled object.
30+
type topologyHandler interface {
31+
GetSpecTopologyRef() *topologyv1.TopoRef
32+
GetLastAppliedTopology() *topologyv1.TopoRef
33+
SetLastAppliedTopology(t *topologyv1.TopoRef)
34+
}
35+
36+
// EnsureTopology - when a Topology CR is referenced, remove the
37+
// finalizer from a previous referenced Topology (if any), and retrieve the
38+
// newly referenced topology object
39+
func EnsureTopology(
40+
ctx context.Context,
41+
helper *helper.Helper,
42+
instance topologyHandler,
43+
finalizer string,
44+
conditionUpdater ConditionUpdater,
45+
defaultLabelSelector metav1.LabelSelector,
46+
) (*topologyv1.Topology, error) {
47+
topology, err := topologyv1.EnsureServiceTopology(
48+
ctx,
49+
helper,
50+
instance.GetSpecTopologyRef(),
51+
instance.GetLastAppliedTopology(),
52+
finalizer,
53+
defaultLabelSelector,
54+
)
55+
if err != nil {
56+
conditionUpdater.Set(condition.FalseCondition(
57+
condition.TopologyReadyCondition,
58+
condition.ErrorReason,
59+
condition.SeverityWarning,
60+
condition.TopologyReadyErrorMessage,
61+
err.Error()))
62+
return nil, fmt.Errorf("waiting for Topology requirements: %w", err)
63+
}
64+
// update the Status with the last retrieved Topology (or set it to nil)
65+
tr := instance.GetSpecTopologyRef()
66+
instance.SetLastAppliedTopology(tr)
67+
// update the Topology condition only when a Topology is referenced and has
68+
// been retrieved (err == nil)
69+
if tr != nil {
70+
// update the TopologyRef associated condition
71+
conditionUpdater.MarkTrue(
72+
condition.TopologyReadyCondition,
73+
condition.TopologyReadyMessage,
74+
)
75+
}
76+
return topology, nil
77+
}

internal/common/util.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
Copyright 2026.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package common //nolint:revive // common is the established package name for multi-group shared code
18+
19+
import (
20+
condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition"
21+
)
22+
23+
// ConditionUpdater updates conditions on a reconciled object.
24+
type ConditionUpdater interface {
25+
Set(c *condition.Condition)
26+
MarkTrue(t condition.Type, messageFormat string, messageArgs ...any)
27+
}

0 commit comments

Comments
 (0)