Skip to content

Commit 70be7ac

Browse files
dkwon17claude
andauthored
fix: default init container imagePullPolicy to IfNotPresent (#1692)
Set default init container imagePullPolicy to IfNotPresent for project-clone, restore containers, and DWOC-defined (.config.workspace.initContainers) init containers. Signed-off-by: David Kwon <dakwon@redhat.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9e9bab4 commit 70be7ac

12 files changed

Lines changed: 239 additions & 10 deletions

‎apis/controller/v1alpha1/devworkspaceoperatorconfig_types.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@ type ProjectCloneConfig struct {
438438
Image string `json:"image,omitempty"`
439439
// ImagePullPolicy configures the imagePullPolicy for the project clone container.
440440
// If undefined, the general setting .config.workspace.imagePullPolicy is used instead.
441+
// If neither is set, defaults to IfNotPresent.
441442
ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"`
442443
// Resources defines the resource (cpu, memory) limits and requests for the project
443444
// clone container. To explicitly not specify a limit or request, define the resource
@@ -449,7 +450,7 @@ type ProjectCloneConfig struct {
449450

450451
type RestoreConfig struct {
451452
// ImagePullPolicy configures the imagePullPolicy for the restore container.
452-
// If undefined, the general setting .config.workspace.imagePullPolicy is used instead.
453+
// If undefined, defaults to IfNotPresent.
453454
ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"`
454455
// Resources defines the resource (cpu, memory) limits and requests for the restore
455456
// container. To explicitly not specify a limit or request, define the resource

‎controllers/workspace/devworkspace_controller.go‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -373,8 +373,8 @@ func (r *DevWorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request
373373
Env: env.GetEnvironmentVariablesForProjectRestore(workspace),
374374
Resources: workspace.Config.Workspace.RestoreConfig.Resources,
375375
}
376-
if config.Workspace.ImagePullPolicy != "" {
377-
restoreOptions.PullPolicy = corev1.PullPolicy(config.Workspace.ImagePullPolicy)
376+
if workspace.Config.Workspace.RestoreConfig.ImagePullPolicy != "" {
377+
restoreOptions.PullPolicy = workspace.Config.Workspace.RestoreConfig.ImagePullPolicy
378378
} else {
379379
restoreOptions.PullPolicy = corev1.PullIfNotPresent
380380
}
@@ -445,13 +445,16 @@ func (r *DevWorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request
445445
return r.failWorkspace(workspace, fmt.Sprintf("Failed to merge init containers: %s", err), metrics.ReasonBadRequest, reqLogger, &reconcileStatus), nil
446446
}
447447

448-
// Ensure init-persistent-home container has correct fields after merge
448+
// Post-merge fixups on init containers
449449
for i := range merged {
450450
if merged[i].Name == constants.HomeInitComponentName {
451451
if err := home.EnsureHomeInitContainerFields(&merged[i]); err != nil {
452452
return r.failWorkspace(workspace, fmt.Sprintf("Failed to configure %s container: %s", constants.HomeInitComponentName, err), metrics.ReasonBadRequest, reqLogger, &reconcileStatus), nil
453453
}
454454
}
455+
if merged[i].ImagePullPolicy == "" {
456+
merged[i].ImagePullPolicy = corev1.PullIfNotPresent
457+
}
455458
}
456459

457460
devfilePodAdditions.InitContainers = merged

‎controllers/workspace/devworkspace_controller_test.go‎

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1555,6 +1555,222 @@ var _ = Describe("DevWorkspace Controller", func() {
15551555

15561556
})
15571557

1558+
Context("Init container imagePullPolicy", func() {
1559+
const testURL = "test-url"
1560+
1561+
BeforeEach(func() {
1562+
httpfactory.SetupHttpClientsForTesting(&http.Client{
1563+
Transport: &testutil.TestRoundTripper{
1564+
Data: map[string]testutil.TestResponse{
1565+
fmt.Sprintf("%s/healthz", testURL): {
1566+
StatusCode: http.StatusOK,
1567+
},
1568+
},
1569+
},
1570+
})
1571+
})
1572+
1573+
AfterEach(func() {
1574+
deleteDevWorkspace(devWorkspaceName)
1575+
httpfactory.SetupHttpClientsForTesting(getBasicTestHttpClient())
1576+
})
1577+
1578+
It("Defaults project-clone imagePullPolicy to IfNotPresent", func() {
1579+
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
1580+
devworkspace := getExistingDevWorkspace(devWorkspaceName)
1581+
workspaceID := devworkspace.Status.DevWorkspaceId
1582+
1583+
By("Manually making Routing ready to continue")
1584+
markRoutingReady(testURL, common.DevWorkspaceRoutingName(workspaceID))
1585+
1586+
deploy := &appsv1.Deployment{}
1587+
deployNN := namespacedName(common.DeploymentName(workspaceID), testNamespace)
1588+
Eventually(func() error {
1589+
return k8sClient.Get(ctx, deployNN, deploy)
1590+
}, timeout, interval).Should(Succeed(), "Getting workspace deployment from cluster")
1591+
1592+
var projectClone *corev1.Container
1593+
for i := range deploy.Spec.Template.Spec.InitContainers {
1594+
if deploy.Spec.Template.Spec.InitContainers[i].Name == projects.ProjectClonerContainerName {
1595+
projectClone = &deploy.Spec.Template.Spec.InitContainers[i]
1596+
}
1597+
}
1598+
Expect(projectClone).NotTo(BeNil(), "project-clone init container should be present")
1599+
Expect(projectClone.ImagePullPolicy).To(Equal(corev1.PullIfNotPresent), "project-clone should default to IfNotPresent")
1600+
})
1601+
1602+
It("Uses DWOC override for project-clone imagePullPolicy", func() {
1603+
config.SetGlobalConfigForTesting(&controllerv1alpha1.OperatorConfiguration{
1604+
Workspace: &controllerv1alpha1.WorkspaceConfig{
1605+
ProjectCloneConfig: &controllerv1alpha1.ProjectCloneConfig{
1606+
ImagePullPolicy: corev1.PullAlways,
1607+
},
1608+
},
1609+
})
1610+
defer config.SetGlobalConfigForTesting(nil)
1611+
1612+
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
1613+
devworkspace := getExistingDevWorkspace(devWorkspaceName)
1614+
workspaceID := devworkspace.Status.DevWorkspaceId
1615+
1616+
By("Manually making Routing ready to continue")
1617+
markRoutingReady(testURL, common.DevWorkspaceRoutingName(workspaceID))
1618+
1619+
deploy := &appsv1.Deployment{}
1620+
deployNN := namespacedName(common.DeploymentName(workspaceID), testNamespace)
1621+
Eventually(func() error {
1622+
return k8sClient.Get(ctx, deployNN, deploy)
1623+
}, timeout, interval).Should(Succeed(), "Getting workspace deployment from cluster")
1624+
1625+
var projectClone *corev1.Container
1626+
for i := range deploy.Spec.Template.Spec.InitContainers {
1627+
if deploy.Spec.Template.Spec.InitContainers[i].Name == projects.ProjectClonerContainerName {
1628+
projectClone = &deploy.Spec.Template.Spec.InitContainers[i]
1629+
}
1630+
}
1631+
Expect(projectClone).NotTo(BeNil(), "project-clone init container should be present")
1632+
Expect(projectClone.ImagePullPolicy).To(Equal(corev1.PullAlways), "project-clone should use DWOC override")
1633+
})
1634+
1635+
It("Defaults DWOC init container imagePullPolicy to IfNotPresent", func() {
1636+
config.SetGlobalConfigForTesting(&controllerv1alpha1.OperatorConfiguration{
1637+
Workspace: &controllerv1alpha1.WorkspaceConfig{
1638+
InitContainers: []corev1.Container{
1639+
{
1640+
Name: "test-init",
1641+
Image: "busybox:latest",
1642+
Command: []string{"sh", "-c", "echo hello"},
1643+
},
1644+
},
1645+
},
1646+
})
1647+
defer config.SetGlobalConfigForTesting(nil)
1648+
1649+
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
1650+
devworkspace := getExistingDevWorkspace(devWorkspaceName)
1651+
workspaceID := devworkspace.Status.DevWorkspaceId
1652+
1653+
By("Manually making Routing ready to continue")
1654+
markRoutingReady(testURL, common.DevWorkspaceRoutingName(workspaceID))
1655+
1656+
deploy := &appsv1.Deployment{}
1657+
deployNN := namespacedName(common.DeploymentName(workspaceID), testNamespace)
1658+
Eventually(func() error {
1659+
return k8sClient.Get(ctx, deployNN, deploy)
1660+
}, timeout, interval).Should(Succeed(), "Getting workspace deployment from cluster")
1661+
1662+
var testInit *corev1.Container
1663+
for i := range deploy.Spec.Template.Spec.InitContainers {
1664+
if deploy.Spec.Template.Spec.InitContainers[i].Name == "test-init" {
1665+
testInit = &deploy.Spec.Template.Spec.InitContainers[i]
1666+
}
1667+
}
1668+
Expect(testInit).NotTo(BeNil(), "test-init container should be present")
1669+
Expect(testInit.ImagePullPolicy).To(Equal(corev1.PullIfNotPresent), "DWOC init container should default to IfNotPresent")
1670+
})
1671+
1672+
It("Preserves explicit imagePullPolicy on DWOC init containers", func() {
1673+
config.SetGlobalConfigForTesting(&controllerv1alpha1.OperatorConfiguration{
1674+
Workspace: &controllerv1alpha1.WorkspaceConfig{
1675+
InitContainers: []corev1.Container{
1676+
{
1677+
Name: "test-init",
1678+
Image: "busybox:latest",
1679+
Command: []string{"sh", "-c", "echo hello"},
1680+
ImagePullPolicy: corev1.PullAlways,
1681+
},
1682+
},
1683+
},
1684+
})
1685+
defer config.SetGlobalConfigForTesting(nil)
1686+
1687+
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
1688+
devworkspace := getExistingDevWorkspace(devWorkspaceName)
1689+
workspaceID := devworkspace.Status.DevWorkspaceId
1690+
1691+
By("Manually making Routing ready to continue")
1692+
markRoutingReady(testURL, common.DevWorkspaceRoutingName(workspaceID))
1693+
1694+
deploy := &appsv1.Deployment{}
1695+
deployNN := namespacedName(common.DeploymentName(workspaceID), testNamespace)
1696+
Eventually(func() error {
1697+
return k8sClient.Get(ctx, deployNN, deploy)
1698+
}, timeout, interval).Should(Succeed(), "Getting workspace deployment from cluster")
1699+
1700+
var testInit *corev1.Container
1701+
for i := range deploy.Spec.Template.Spec.InitContainers {
1702+
if deploy.Spec.Template.Spec.InitContainers[i].Name == "test-init" {
1703+
testInit = &deploy.Spec.Template.Spec.InitContainers[i]
1704+
}
1705+
}
1706+
Expect(testInit).NotTo(BeNil(), "test-init container should be present")
1707+
Expect(testInit.ImagePullPolicy).To(Equal(corev1.PullAlways), "Explicit imagePullPolicy should be preserved")
1708+
})
1709+
1710+
It("Does not override base imagePullPolicy when DWOC init container has no explicit policy", func() {
1711+
config.SetGlobalConfigForTesting(&controllerv1alpha1.OperatorConfiguration{
1712+
Workspace: &controllerv1alpha1.WorkspaceConfig{
1713+
ProjectCloneConfig: &controllerv1alpha1.ProjectCloneConfig{
1714+
ImagePullPolicy: corev1.PullAlways,
1715+
},
1716+
InitContainers: []corev1.Container{
1717+
{
1718+
Name: projects.ProjectClonerContainerName,
1719+
Env: []corev1.EnvVar{{Name: "EXTRA_VAR", Value: "injected"}},
1720+
},
1721+
},
1722+
},
1723+
})
1724+
defer config.SetGlobalConfigForTesting(nil)
1725+
1726+
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
1727+
devworkspace := getExistingDevWorkspace(devWorkspaceName)
1728+
workspaceID := devworkspace.Status.DevWorkspaceId
1729+
1730+
By("Manually making Routing ready to continue")
1731+
markRoutingReady(testURL, common.DevWorkspaceRoutingName(workspaceID))
1732+
1733+
deploy := &appsv1.Deployment{}
1734+
deployNN := namespacedName(common.DeploymentName(workspaceID), testNamespace)
1735+
Eventually(func() error {
1736+
return k8sClient.Get(ctx, deployNN, deploy)
1737+
}, timeout, interval).Should(Succeed(), "Getting workspace deployment from cluster")
1738+
1739+
var projectClone *corev1.Container
1740+
for i := range deploy.Spec.Template.Spec.InitContainers {
1741+
if deploy.Spec.Template.Spec.InitContainers[i].Name == projects.ProjectClonerContainerName {
1742+
projectClone = &deploy.Spec.Template.Spec.InitContainers[i]
1743+
}
1744+
}
1745+
Expect(projectClone).NotTo(BeNil(), "project-clone init container should be present")
1746+
Expect(projectClone.ImagePullPolicy).To(Equal(corev1.PullAlways), "DWOC patch should not override base imagePullPolicy")
1747+
})
1748+
1749+
It("Keeps workspace container imagePullPolicy as Always", func() {
1750+
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
1751+
devworkspace := getExistingDevWorkspace(devWorkspaceName)
1752+
workspaceID := devworkspace.Status.DevWorkspaceId
1753+
1754+
By("Manually making Routing ready to continue")
1755+
markRoutingReady(testURL, common.DevWorkspaceRoutingName(workspaceID))
1756+
1757+
deploy := &appsv1.Deployment{}
1758+
deployNN := namespacedName(common.DeploymentName(workspaceID), testNamespace)
1759+
Eventually(func() error {
1760+
return k8sClient.Get(ctx, deployNN, deploy)
1761+
}, timeout, interval).Should(Succeed(), "Getting workspace deployment from cluster")
1762+
1763+
var devContainer *corev1.Container
1764+
for i := range deploy.Spec.Template.Spec.Containers {
1765+
if deploy.Spec.Template.Spec.Containers[i].Name == "web-terminal" {
1766+
devContainer = &deploy.Spec.Template.Spec.Containers[i]
1767+
}
1768+
}
1769+
Expect(devContainer).NotTo(BeNil(), "web-terminal container should be present")
1770+
Expect(devContainer.ImagePullPolicy).To(Equal(corev1.PullAlways), "Workspace containers should still default to Always")
1771+
})
1772+
})
1773+
15581774
Context("Edge cases", func() {
15591775

15601776
It("Allows Kubernetes and Container components to share same target port on endpoint", func() {

‎deploy/bundle/manifests/controller.devfile.io_devworkspaceoperatorconfigs.yaml‎

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎deploy/deployment/kubernetes/combined.yaml‎

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎deploy/deployment/kubernetes/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml‎

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎deploy/deployment/openshift/combined.yaml‎

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎deploy/deployment/openshift/objects/devworkspaceoperatorconfigs.controller.devfile.io.CustomResourceDefinition.yaml‎

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎deploy/templates/components/csv/clusterserviceversion.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
---
12
apiVersion: operators.coreos.com/v1alpha1
23
kind: ClusterServiceVersion
34
metadata:

‎deploy/templates/crd/bases/controller.devfile.io_devworkspaceoperatorconfigs.yaml‎

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)