Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (

"github.com/tigera/operator/internal/controller"
"github.com/tigera/operator/pkg/active"
"github.com/tigera/operator/pkg/apigroup"
"github.com/tigera/operator/pkg/apis"
"github.com/tigera/operator/pkg/awssgsetup"
"github.com/tigera/operator/pkg/common"
Expand Down Expand Up @@ -217,6 +218,11 @@ If a value other than 'all' is specified, the first CRD with a prefix of the spe
os.Exit(1)
}

// Tell the component handler which API group to inject into workloads.
if v3CRDs {
apigroup.Set(apigroup.V3)
}

// Add the Calico API to the scheme, now that we know which backing CRD version to use.
utilruntime.Must(apis.AddToScheme(scheme, v3CRDs))

Expand Down
76 changes: 76 additions & 0 deletions pkg/apigroup/apigroup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (c) 2026 Tigera, Inc. All rights reserved.

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package apigroup tracks which Calico API group the operator should configure
// on the workloads it manages. The value is set once at startup (or when a
// datastore migration completes) and read by the component handler to inject
// the CALICO_API_GROUP env var into all workload containers.
package apigroup

import (
"sync"

corev1 "k8s.io/api/core/v1"
)

// APIGroup identifies which Calico CRD API group to use.
type APIGroup int

const (
// Unknown means the API group hasn't been determined yet.
Unknown APIGroup = iota
// V1 uses crd.projectcalico.org/v1 (legacy, via aggregated API server).
V1
// V3 uses projectcalico.org/v3 (native CRDs, no API server).
V3
)

const (
envVarName = "CALICO_API_GROUP"
v3Value = "projectcalico.org/v3"
)

var (
mu sync.RWMutex
current APIGroup
envVars []corev1.EnvVar
)

// Set records the active API group. If V3, subsequent calls to EnvVars will
// return a CALICO_API_GROUP env var for injection into workload containers.
func Set(g APIGroup) {
mu.Lock()
defer mu.Unlock()
current = g
if g == V3 {
envVars = []corev1.EnvVar{{Name: envVarName, Value: v3Value}}
} else {
envVars = nil
}
}

// Get returns the current API group.
func Get() APIGroup {
mu.RLock()
defer mu.RUnlock()
return current
}

// EnvVars returns the env vars to inject into workload containers, or nil if
// no explicit API group has been configured.
func EnvVars() []corev1.EnvVar {
mu.RLock()
defer mu.RUnlock()
return envVars
}
29 changes: 29 additions & 0 deletions pkg/apigroup/apigroup_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) 2026 Tigera, Inc. All rights reserved.

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package apigroup

import (
"testing"

"github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
)

func TestAPIGroup(t *testing.T) {
gomega.RegisterFailHandler(ginkgo.Fail)
suiteConfig, reporterConfig := ginkgo.GinkgoConfiguration()
reporterConfig.JUnitReport = "../../report/ut/apigroup_suite.xml"
ginkgo.RunSpecs(t, "pkg/apigroup Suite", suiteConfig, reporterConfig)
}
51 changes: 51 additions & 0 deletions pkg/apigroup/apigroup_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright (c) 2026 Tigera, Inc. All rights reserved.

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package apigroup

import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

corev1 "k8s.io/api/core/v1"
)

var _ = Describe("apigroup", func() {
It("should default to Unknown with nil env vars", func() {
Set(Unknown)
Expect(Get()).To(Equal(Unknown))
Expect(EnvVars()).To(BeNil())
})

It("should return CALICO_API_GROUP env var when set to V3", func() {
Set(V3)
Expect(Get()).To(Equal(V3))
Expect(EnvVars()).To(Equal([]corev1.EnvVar{
{Name: "CALICO_API_GROUP", Value: "projectcalico.org/v3"},
}))
})

It("should return nil env vars when set to V1", func() {
Set(V1)
Expect(Get()).To(Equal(V1))
Expect(EnvVars()).To(BeNil())
})

It("should clear env vars when set back to Unknown", func() {
Set(V3)
Set(Unknown)
Expect(Get()).To(Equal(Unknown))
Expect(EnvVars()).To(BeNil())
})
})
74 changes: 65 additions & 9 deletions pkg/controller/utils/component.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

v3 "github.com/tigera/api/pkg/apis/projectcalico/v3"
"github.com/tigera/operator/pkg/apigroup"
"github.com/tigera/operator/pkg/common"
"github.com/tigera/operator/pkg/controller/status"
"github.com/tigera/operator/pkg/render"
Expand Down Expand Up @@ -75,19 +76,21 @@ type ComponentHandler interface {
// this is useful for CRD management so that they are not removed automatically.
func NewComponentHandler(log logr.Logger, cli client.Client, scheme *runtime.Scheme, cr metav1.Object) ComponentHandler {
return &componentHandler{
client: cli,
scheme: scheme,
cr: cr,
log: log,
client: cli,
scheme: scheme,
cr: cr,
log: log,
apiGroupEnvs: apigroup.EnvVars(),
}
}

type componentHandler struct {
client client.Client
scheme *runtime.Scheme
cr metav1.Object
log logr.Logger
createOnly bool
client client.Client
scheme *runtime.Scheme
cr metav1.Object
log logr.Logger
createOnly bool
apiGroupEnvs []v1.EnvVar
}

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

if len(c.apiGroupEnvs) > 0 {
for _, obj := range objsToCreate {
c.injectAPIGroupEnv(obj)
}
}

var alreadyExistsErr error = nil

for _, obj := range objsToCreate {
Expand Down Expand Up @@ -1129,3 +1138,50 @@ func (r *ReadyFlag) MarkAsReady() {
defer r.mu.Unlock()
r.isReady = true
}

// injectAPIGroupEnv adds the CALICO_API_GROUP env var to all containers in
// workload objects. This ensures every component uses the correct API group
// during and after a datastore migration.
func (c *componentHandler) injectAPIGroupEnv(obj client.Object) {
var podSpec *v1.PodSpec
switch o := obj.(type) {
case *apps.Deployment:
podSpec = &o.Spec.Template.Spec
case *apps.DaemonSet:
podSpec = &o.Spec.Template.Spec
case *apps.StatefulSet:
podSpec = &o.Spec.Template.Spec
case *batchv1.Job:
podSpec = &o.Spec.Template.Spec
case *batchv1.CronJob:
podSpec = &o.Spec.JobTemplate.Spec.Template.Spec
default:
return
}
for i := range podSpec.Containers {
podSpec.Containers[i].Env = mergeEnvVars(podSpec.Containers[i].Env, c.apiGroupEnvs)
}
for i := range podSpec.InitContainers {
podSpec.InitContainers[i].Env = mergeEnvVars(podSpec.InitContainers[i].Env, c.apiGroupEnvs)
}
}

// mergeEnvVars adds or updates env vars in existing. If an env var with the
// same name already exists, its value is updated in place.
func mergeEnvVars(existing []v1.EnvVar, toMerge []v1.EnvVar) []v1.EnvVar {
for _, env := range toMerge {
found := false
for i, e := range existing {
if e.Name == env.Name {
existing[i].Value = env.Value
existing[i].ValueFrom = env.ValueFrom
found = true
break
}
}
if !found {
existing = append(existing, env)
}
}
return existing
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ spec:
type: string
type: array
x-kubernetes-list-type: set
ipv4NormalRoutePriority:
maximum: 2147483646
minimum: 1
type: integer
ipv6NormalRoutePriority:
maximum: 2147483646
minimum: 1
type: integer
listenPort:
maximum: 65535
minimum: 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1330,6 +1330,30 @@ spec:
to reduce Felix CPU usage. [Default: 10s]
pattern: ^([0-9]+(\\.[0-9]+)?(ms|s|m|h))*$
type: string
ipv4ElevatedRoutePriority:
description: |-
Route Priority value for an elevated priority Calico-programmed IPv4 route. Note, higher
values mean lower priority. Elevated priority is used during VM live migration, and for
optimal behaviour IPv4ElevatedRoutePriority must be less than IPv4NormalRoutePriority
[Default: 512]
type: integer
ipv4NormalRoutePriority:
description: |-
Route Priority value for a normal priority Calico-programmed IPv4 route. Note, higher
values mean lower priority. [Default: 1024]
type: integer
ipv6ElevatedRoutePriority:
description: |-
Route Priority value for an elevated priority Calico-programmed IPv6 route. Note, higher
values mean lower priority. Elevated priority is used during VM live migration, and for
optimal behaviour IPv6ElevatedRoutePriority must be less than IPv6NormalRoutePriority
[Default: 512]
type: integer
ipv6NormalRoutePriority:
description: |-
Route Priority value for a normal priority Calico-programmed IPv6 route. Note, higher
values mean lower priority. [Default: 1024]
type: integer
ipv6Support:
description:
IPv6Support controls whether Felix enables support for
Expand Down Expand Up @@ -1479,6 +1503,13 @@ spec:
[Default: 300s]
pattern: ^([0-9]+(\\.[0-9]+)?(ms|s|m|h))*$
type: string
liveMigrationRouteConvergenceTime:
description: |-
LiveMigrationRouteConvergenceTime is the time to keep elevated route priority after a
VM live migration completes. This allows routes to converge across the cluster before
reverting to normal priority. [Default: 30s]
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))*$
type: string
logActionRateLimit:
description: |-
LogActionRateLimit sets the rate of hitting a Log action. The value must be in the format "N/unit",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ spec:
type: string
type: array
x-kubernetes-list-type: set
ipv4NormalRoutePriority:
maximum: 2147483646
minimum: 1
type: integer
ipv6NormalRoutePriority:
maximum: 2147483646
minimum: 1
type: integer
listenPort:
maximum: 65535
minimum: 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,14 @@ spec:
iptablesRefreshInterval:
pattern: ^([0-9]+(\\.[0-9]+)?(ms|s|m|h))*$
type: string
ipv4ElevatedRoutePriority:
type: integer
ipv4NormalRoutePriority:
type: integer
ipv6ElevatedRoutePriority:
type: integer
ipv6NormalRoutePriority:
type: integer
ipv6Support:
type: boolean
istioAmbientMode:
Expand Down Expand Up @@ -602,6 +610,9 @@ spec:
l7LogsFlushInterval:
pattern: ^([0-9]+(\\.[0-9]+)?(ms|s|m|h))*$
type: string
liveMigrationRouteConvergenceTime:
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))*$
type: string
logActionRateLimit:
pattern: ^[1-9]\d{0,3}/(?:second|minute|hour|day)$
type: string
Expand Down
Loading