Skip to content

Commit 94edd6a

Browse files
Add CALICO_API_GROUP env var injection via apigroup package
Introduce pkg/apigroup to track the active CRD API group and inject CALICO_API_GROUP into all workload containers via the component handler. This tells components whether to use projectcalico.org/v3 or the legacy crd.projectcalico.org/v1 API group.
1 parent 8cc0c4d commit 94edd6a

File tree

4 files changed

+214
-9
lines changed

4 files changed

+214
-9
lines changed

cmd/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030

3131
"github.com/tigera/operator/internal/controller"
3232
"github.com/tigera/operator/pkg/active"
33+
"github.com/tigera/operator/pkg/apigroup"
3334
"github.com/tigera/operator/pkg/apis"
3435
"github.com/tigera/operator/pkg/awssgsetup"
3536
"github.com/tigera/operator/pkg/common"
@@ -217,6 +218,11 @@ If a value other than 'all' is specified, the first CRD with a prefix of the spe
217218
os.Exit(1)
218219
}
219220

221+
// Tell the component handler which API group to inject into workloads.
222+
if v3CRDs {
223+
apigroup.Set(apigroup.V3)
224+
}
225+
220226
// Add the Calico API to the scheme, now that we know which backing CRD version to use.
221227
utilruntime.Must(apis.AddToScheme(scheme, v3CRDs))
222228

pkg/apigroup/apigroup.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright (c) 2026 Tigera, Inc. All rights reserved.
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 apigroup tracks which Calico API group the operator should configure
16+
// on the workloads it manages. The value is set once at startup (or when a
17+
// datastore migration completes) and read by the component handler to inject
18+
// the CALICO_API_GROUP env var into all workload containers.
19+
package apigroup
20+
21+
import (
22+
"sync"
23+
24+
corev1 "k8s.io/api/core/v1"
25+
)
26+
27+
// APIGroup identifies which Calico CRD API group to use.
28+
type APIGroup int
29+
30+
const (
31+
// Unknown means the API group hasn't been determined yet.
32+
Unknown APIGroup = iota
33+
// V1 uses crd.projectcalico.org/v1 (legacy, via aggregated API server).
34+
V1
35+
// V3 uses projectcalico.org/v3 (native CRDs, no API server).
36+
V3
37+
)
38+
39+
const (
40+
envVarName = "CALICO_API_GROUP"
41+
v3Value = "projectcalico.org/v3"
42+
)
43+
44+
var (
45+
mu sync.RWMutex
46+
current APIGroup
47+
envVars []corev1.EnvVar
48+
)
49+
50+
// Set records the active API group. If V3, subsequent calls to EnvVars will
51+
// return a CALICO_API_GROUP env var for injection into workload containers.
52+
func Set(g APIGroup) {
53+
mu.Lock()
54+
defer mu.Unlock()
55+
current = g
56+
if g == V3 {
57+
envVars = []corev1.EnvVar{{Name: envVarName, Value: v3Value}}
58+
} else {
59+
envVars = nil
60+
}
61+
}
62+
63+
// Get returns the current API group.
64+
func Get() APIGroup {
65+
mu.RLock()
66+
defer mu.RUnlock()
67+
return current
68+
}
69+
70+
// EnvVars returns the env vars to inject into workload containers, or nil if
71+
// no explicit API group has been configured.
72+
func EnvVars() []corev1.EnvVar {
73+
mu.RLock()
74+
defer mu.RUnlock()
75+
return envVars
76+
}

pkg/apigroup/apigroup_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright (c) 2026 Tigera, Inc. All rights reserved.
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 apigroup
16+
17+
import (
18+
"testing"
19+
20+
. "github.com/onsi/gomega"
21+
corev1 "k8s.io/api/core/v1"
22+
)
23+
24+
func TestSetAndGet(t *testing.T) {
25+
tests := []struct {
26+
name string
27+
set APIGroup
28+
wantGet APIGroup
29+
wantEnvVars []corev1.EnvVar
30+
}{
31+
{
32+
name: "default state is Unknown with nil env vars",
33+
set: Unknown,
34+
wantGet: Unknown,
35+
wantEnvVars: nil,
36+
},
37+
{
38+
name: "V3 returns CALICO_API_GROUP env var",
39+
set: V3,
40+
wantGet: V3,
41+
wantEnvVars: []corev1.EnvVar{
42+
{Name: "CALICO_API_GROUP", Value: "projectcalico.org/v3"},
43+
},
44+
},
45+
{
46+
name: "V1 returns nil env vars",
47+
set: V1,
48+
wantGet: V1,
49+
wantEnvVars: nil,
50+
},
51+
{
52+
name: "setting back to Unknown clears env vars",
53+
set: Unknown,
54+
wantGet: Unknown,
55+
wantEnvVars: nil,
56+
},
57+
}
58+
59+
for _, tt := range tests {
60+
t.Run(tt.name, func(t *testing.T) {
61+
g := NewWithT(t)
62+
Set(tt.set)
63+
g.Expect(Get()).To(Equal(tt.wantGet))
64+
g.Expect(EnvVars()).To(Equal(tt.wantEnvVars))
65+
})
66+
}
67+
}

pkg/controller/utils/component.go

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
4343

4444
v3 "github.com/tigera/api/pkg/apis/projectcalico/v3"
45+
"github.com/tigera/operator/pkg/apigroup"
4546
"github.com/tigera/operator/pkg/common"
4647
"github.com/tigera/operator/pkg/controller/status"
4748
"github.com/tigera/operator/pkg/render"
@@ -75,19 +76,21 @@ type ComponentHandler interface {
7576
// this is useful for CRD management so that they are not removed automatically.
7677
func NewComponentHandler(log logr.Logger, cli client.Client, scheme *runtime.Scheme, cr metav1.Object) ComponentHandler {
7778
return &componentHandler{
78-
client: cli,
79-
scheme: scheme,
80-
cr: cr,
81-
log: log,
79+
client: cli,
80+
scheme: scheme,
81+
cr: cr,
82+
log: log,
83+
apiGroupEnvs: apigroup.EnvVars(),
8284
}
8385
}
8486

8587
type componentHandler struct {
86-
client client.Client
87-
scheme *runtime.Scheme
88-
cr metav1.Object
89-
log logr.Logger
90-
createOnly bool
88+
client client.Client
89+
scheme *runtime.Scheme
90+
cr metav1.Object
91+
log logr.Logger
92+
createOnly bool
93+
apiGroupEnvs []v1.EnvVar
9194
}
9295

9396
func (c *componentHandler) SetCreateOnly() {
@@ -444,6 +447,12 @@ func (c *componentHandler) CreateOrUpdateOrDelete(ctx context.Context, component
444447
objsToCreate, objsToDelete := component.Objects()
445448
osType := component.SupportedOSType()
446449

450+
if len(c.apiGroupEnvs) > 0 {
451+
for _, obj := range objsToCreate {
452+
c.injectAPIGroupEnv(obj)
453+
}
454+
}
455+
447456
var alreadyExistsErr error = nil
448457

449458
for _, obj := range objsToCreate {
@@ -1129,3 +1138,50 @@ func (r *ReadyFlag) MarkAsReady() {
11291138
defer r.mu.Unlock()
11301139
r.isReady = true
11311140
}
1141+
1142+
// injectAPIGroupEnv adds the CALICO_API_GROUP env var to all containers in
1143+
// workload objects. This ensures every component uses the correct API group
1144+
// during and after a datastore migration.
1145+
func (c *componentHandler) injectAPIGroupEnv(obj client.Object) {
1146+
var podSpec *v1.PodSpec
1147+
switch o := obj.(type) {
1148+
case *apps.Deployment:
1149+
podSpec = &o.Spec.Template.Spec
1150+
case *apps.DaemonSet:
1151+
podSpec = &o.Spec.Template.Spec
1152+
case *apps.StatefulSet:
1153+
podSpec = &o.Spec.Template.Spec
1154+
case *batchv1.Job:
1155+
podSpec = &o.Spec.Template.Spec
1156+
case *batchv1.CronJob:
1157+
podSpec = &o.Spec.JobTemplate.Spec.Template.Spec
1158+
default:
1159+
return
1160+
}
1161+
for i := range podSpec.Containers {
1162+
podSpec.Containers[i].Env = mergeEnvVars(podSpec.Containers[i].Env, c.apiGroupEnvs)
1163+
}
1164+
for i := range podSpec.InitContainers {
1165+
podSpec.InitContainers[i].Env = mergeEnvVars(podSpec.InitContainers[i].Env, c.apiGroupEnvs)
1166+
}
1167+
}
1168+
1169+
// mergeEnvVars adds or updates env vars in existing. If an env var with the
1170+
// same name already exists, its value is updated in place.
1171+
func mergeEnvVars(existing []v1.EnvVar, toMerge []v1.EnvVar) []v1.EnvVar {
1172+
for _, env := range toMerge {
1173+
found := false
1174+
for i, e := range existing {
1175+
if e.Name == env.Name {
1176+
existing[i].Value = env.Value
1177+
existing[i].ValueFrom = env.ValueFrom
1178+
found = true
1179+
break
1180+
}
1181+
}
1182+
if !found {
1183+
existing = append(existing, env)
1184+
}
1185+
}
1186+
return existing
1187+
}

0 commit comments

Comments
 (0)