Merge pull request from joebowbeer/init-containers

feat: initContainers
pull/738/head
Karolis Rusenas 2023-08-09 10:13:27 +01:00 committed by GitHub
commit 6defcfcad8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 577 additions and 0 deletions

View File

@ -33,7 +33,12 @@ func updateDeploymentContainer(d *apps_v1.Deployment, index int, image string) {
d.Spec.Template.Spec.Containers[index].Image = image
}
func updateDeploymentInitContainer(d *apps_v1.Deployment, index int, image string) {
d.Spec.Template.Spec.InitContainers[index].Image = image
}
// stateful sets https://kubernetes.io/docs/tutorials/stateful-application/basic-stateful-set/
func getStatefulSetIdentifier(ss *apps_v1.StatefulSet) string {
return "statefulset/" + ss.Namespace + "/" + ss.Name
}
@ -42,6 +47,10 @@ func updateStatefulSetContainer(ss *apps_v1.StatefulSet, index int, image string
ss.Spec.Template.Spec.Containers[index].Image = image
}
func updateStatefulSetInitContainer(ss *apps_v1.StatefulSet, index int, image string) {
ss.Spec.Template.Spec.InitContainers[index].Image = image
}
// daemonsets
func getDaemonsetSetIdentifier(s *apps_v1.DaemonSet) string {
@ -52,6 +61,10 @@ func updateDaemonsetSetContainer(s *apps_v1.DaemonSet, index int, image string)
s.Spec.Template.Spec.Containers[index].Image = image
}
func updateDaemonsetSetInitContainer(s *apps_v1.DaemonSet, index int, image string) {
s.Spec.Template.Spec.InitContainers[index].Image = image
}
// cron
func getCronJobIdentifier(s *batch_v1.CronJob) string {
@ -61,3 +74,8 @@ func getCronJobIdentifier(s *batch_v1.CronJob) string {
func updateCronJobContainer(s *batch_v1.CronJob, index int, image string) {
s.Spec.JobTemplate.Spec.Template.Spec.Containers[index].Image = image
}
func updateCronJobInitContainer(s *batch_v1.CronJob, index int, image string) {
s.Spec.JobTemplate.Spec.Template.Spec.InitContainers[index].Image = image
}

View File

@ -275,6 +275,21 @@ func (r *GenericResource) GetImages() (images []string) {
return
}
// GetInitImages - returns init images used by this resource
func (r *GenericResource) GetInitImages() (images []string) {
switch obj := r.obj.(type) {
case *apps_v1.Deployment:
return getContainerImages(obj.Spec.Template.Spec.InitContainers)
case *apps_v1.StatefulSet:
return getContainerImages(obj.Spec.Template.Spec.InitContainers)
case *apps_v1.DaemonSet:
return getContainerImages(obj.Spec.Template.Spec.InitContainers)
case *batch_v1.CronJob:
return getContainerImages(obj.Spec.JobTemplate.Spec.Template.Spec.InitContainers)
}
return
}
// Containers - returns containers managed by this resource
func (r *GenericResource) Containers() (containers []core_v1.Container) {
switch obj := r.obj.(type) {
@ -290,6 +305,21 @@ func (r *GenericResource) Containers() (containers []core_v1.Container) {
return
}
// InitContainers - returns init containers managed by this resource
func (r *GenericResource) InitContainers() (containers []core_v1.Container) {
switch obj := r.obj.(type) {
case *apps_v1.Deployment:
return obj.Spec.Template.Spec.InitContainers
case *apps_v1.StatefulSet:
return obj.Spec.Template.Spec.InitContainers
case *apps_v1.DaemonSet:
return obj.Spec.Template.Spec.InitContainers
case *batch_v1.CronJob:
return obj.Spec.JobTemplate.Spec.Template.Spec.InitContainers
}
return
}
// UpdateContainer - updates container image
func (r *GenericResource) UpdateContainer(index int, image string) {
switch obj := r.obj.(type) {
@ -304,6 +334,20 @@ func (r *GenericResource) UpdateContainer(index int, image string) {
}
}
// UpdateInitContainer - updates init container image
func (r *GenericResource) UpdateInitContainer(index int, image string) {
switch obj := r.obj.(type) {
case *apps_v1.Deployment:
updateDeploymentInitContainer(obj, index, image)
case *apps_v1.StatefulSet:
updateStatefulSetInitContainer(obj, index, image)
case *apps_v1.DaemonSet:
updateDaemonsetSetInitContainer(obj, index, image)
case *batch_v1.CronJob:
updateCronJobInitContainer(obj, index, image)
}
}
type Status struct {
// Total number of non-terminated pods targeted by this deployment (their labels match the selector).
// +optional

View File

@ -48,6 +48,56 @@ func TestDeployment(t *testing.T) {
}
}
func TestDeploymentInitContainer(t *testing.T) {
d := &apps_v1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{},
},
apps_v1.DeploymentSpec{
Template: core_v1.PodTemplateSpec{
Spec: core_v1.PodSpec{
Containers: []core_v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
InitContainers: []core_v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
},
},
},
apps_v1.DeploymentStatus{},
}
gr, err := NewGenericResource(d)
if err != nil {
t.Fatalf("failed to create generic resource: %s", err)
}
gr.UpdateContainer(0, "hey/there")
gr.UpdateInitContainer(0, "over/here")
updated, ok := gr.GetResource().(*apps_v1.Deployment)
if !ok {
t.Fatalf("conversion failed")
}
if updated.Spec.Template.Spec.Containers[0].Image != "hey/there" {
t.Errorf("unexpected image: %s", updated.Spec.Template.Spec.Containers[0].Image)
}
if updated.Spec.Template.Spec.InitContainers[0].Image != "over/here" {
t.Errorf("unexpected image: %s", updated.Spec.Template.Spec.InitContainers[0].Image)
}
}
func TestDeploymentMultipleContainers(t *testing.T) {
d := &apps_v1.Deployment{
meta_v1.TypeMeta{},

View File

@ -145,6 +145,25 @@ func getImagePullSecretFromMeta(labels map[string]string, annotations map[string
return ""
}
func getInitContainerTrackingFromMeta(labels map[string]string, annotations map[string]string) bool {
searchKey := strings.ToLower(types.KeelInitContainerAnnotation)
for k, v := range labels {
if strings.ToLower(k) == searchKey {
return v == "true"
}
}
for k, v := range annotations {
if strings.ToLower(k) == searchKey {
return v == "true"
}
}
return false
}
// TrackedImages returns a list of tracked images.
func (p *Provider) TrackedImages() ([]*types.TrackedImage, error) {
var trackedImages []*types.TrackedImage
@ -187,6 +206,9 @@ func (p *Provider) TrackedImages() ([]*types.TrackedImage, error) {
secrets = append(secrets, gr.GetImagePullSecrets()...)
images := gr.GetImages()
if getInitContainerTrackingFromMeta(labels, annotations) {
images = append(images, gr.GetInitImages()...)
}
for _, img := range images {
ref, err := image.Parse(img)
if err != nil {

View File

@ -296,6 +296,107 @@ func TestGetImpacted(t *testing.T) {
}
}
func TestGetImpactedInit(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
v1.NamespaceStatus{},
},
},
}
deps := []*apps_v1.Deployment{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{types.KeelInitContainerAnnotation: "true"},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
apps_v1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
InitContainers: []v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
},
},
},
apps_v1.DeploymentStatus{},
},
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-2",
Namespace: "xxxx",
Annotations: map[string]string{types.KeelInitContainerAnnotation: "false"},
Labels: map[string]string{"whatever": "all"},
},
apps_v1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
InitContainers: []v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
},
},
},
apps_v1.DeploymentStatus{},
},
}
grs := MustParseGRS(deps)
grc := &k8s.GenericResourceCache{}
grc.Add(grs...)
approver, teardown := approver()
defer teardown()
provider, err := NewProvider(fp, &fakeSender{}, approver, grc)
if err != nil {
t.Fatalf("failed to get provider: %s", err)
}
// creating "new version" event
repo := &types.Repository{
Name: "gcr.io/v2-namespace/hello-world",
Tag: "1.1.2",
}
plans, err := provider.createUpdatePlans(repo)
if err != nil {
t.Errorf("failed to get deployments: %s", err)
}
if len(plans) != 1 {
t.Fatalf("expected to find 1 deployment update plan but found %d", len(plans))
}
found := false
for _, c := range plans[0].Resource.InitContainers() {
containerImageName := versionreg.ReplaceAllString(c.Image, "")
if containerImageName == repo.Name {
found = true
}
}
if !found {
t.Errorf("couldn't find expected deployment in impacted deployment list")
}
}
func TestGetImpactedPolicyAnnotations(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
@ -1010,6 +1111,107 @@ func TestGetImpactedTwoContainersInSameDeployment(t *testing.T) {
}
// Test to check how many deployments are "impacted" if we have two init containers
func TestGetImpactedTwoInitContainersInSameDeployment(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
v1.NamespaceStatus{},
},
},
}
deps := []*apps_v1.Deployment{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Labels: map[string]string{types.KeelPolicyLabel: "all"},
Annotations: map[string]string{types.KeelInitContainerAnnotation: "true"},
},
apps_v1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
InitContainers: []v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
{
Image: "gcr.io/v2-namespace/greetings-world:1.1.1",
},
},
},
},
},
apps_v1.DeploymentStatus{},
},
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-2",
Namespace: "xxxx",
Labels: map[string]string{"whatever": "all"},
},
apps_v1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
InitContainers: []v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1.1",
},
},
},
},
},
apps_v1.DeploymentStatus{},
},
}
grs := MustParseGRS(deps)
grc := &k8s.GenericResourceCache{}
grc.Add(grs...)
approver, teardown := approver()
defer teardown()
provider, err := NewProvider(fp, &fakeSender{}, approver, grc)
if err != nil {
t.Fatalf("failed to get provider: %s", err)
}
// creating "new version" event
repo := &types.Repository{
Name: "gcr.io/v2-namespace/hello-world",
Tag: "1.1.2",
}
plans, err := provider.createUpdatePlans(repo)
if err != nil {
t.Errorf("failed to get deployments: %s", err)
}
if len(plans) != 1 {
t.Errorf("expected to find 1 deployment but found %d", len(plans))
}
found := false
for _, c := range plans[0].Resource.InitContainers() {
containerImageName := versionreg.ReplaceAllString(c.Image, "")
if containerImageName == repo.Name {
found = true
}
}
if !found {
t.Errorf("couldn't find expected deployment in impacted deployment list")
}
}
func TestGetImpactedTwoSameContainersInSameDeployment(t *testing.T) {
fp := &fakeImplementer{}
@ -1445,3 +1647,74 @@ func TestTrackedImagesWithSecrets(t *testing.T) {
t.Errorf("expected very-secret, got: %s", imgs[0].Secrets[1])
}
}
func TestTrackedInitImagesWithSecrets(t *testing.T) {
fp := &fakeImplementer{}
fp.namespaces = &v1.NamespaceList{
Items: []v1.Namespace{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{Name: "xxxx"},
v1.NamespaceSpec{},
v1.NamespaceStatus{},
},
},
}
deps := []*apps_v1.Deployment{
{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Labels: map[string]string{
types.KeelPolicyLabel: "all",
types.KeelImagePullSecretAnnotation: "foo-bar",
types.KeelInitContainerAnnotation: "true",
},
},
apps_v1.DeploymentSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
ImagePullSecrets: []v1.LocalObjectReference{
{
Name: "very-secret",
},
},
InitContainers: []v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:1.1",
},
},
},
},
},
apps_v1.DeploymentStatus{},
},
}
grs := MustParseGRS(deps)
grc := &k8s.GenericResourceCache{}
grc.Add(grs...)
approver, teardown := approver()
defer teardown()
provider, err := NewProvider(fp, &fakeSender{}, approver, grc)
if err != nil {
t.Fatalf("failed to get provider: %s", err)
}
imgs, err := provider.TrackedImages()
if err != nil {
t.Errorf("failed to get image: %s", err)
}
if len(imgs) != 1 {
t.Errorf("expected to find 1 image, got: %d", len(imgs))
}
if imgs[0].Secrets[0] != "foo-bar" {
t.Errorf("expected foo-bar, got: %s", imgs[0].Secrets[0])
}
if imgs[0].Secrets[1] != "very-secret" {
t.Errorf("expected very-secret, got: %s", imgs[0].Secrets[1])
}
}

View File

@ -27,6 +27,68 @@ func checkForUpdate(plc policy.Policy, repo *types.Repository, resource *k8s.Gen
"policy": plc.Name(),
}).Debug("provider.kubernetes.checkVersionedDeployment: keel policy found, checking resource...")
shouldUpdateDeployment = false
if schedule, ok := resource.GetAnnotations()[types.KeelInitContainerAnnotation]; ok && schedule == "true" {
for idx, c := range resource.InitContainers() {
containerImageRef, err := image.Parse(c.Image)
if err != nil {
log.WithFields(log.Fields{
"error": err,
"image_name": c.Image,
}).Error("provider.kubernetes: failed to parse image name")
continue
}
log.WithFields(log.Fields{
"name": resource.Name,
"namespace": resource.Namespace,
"kind": resource.Kind(),
"parsed_image_name": containerImageRef.Remote(),
"target_image_name": repo.Name,
"target_tag": repo.Tag,
"policy": plc.Name(),
"image": c.Image,
}).Debug("provider.kubernetes: checking image")
if containerImageRef.Repository() != eventRepoRef.Repository() {
log.WithFields(log.Fields{
"parsed_image_name": containerImageRef.Remote(),
"target_image_name": repo.Name,
}).Debug("provider.kubernetes: images do not match, ignoring")
continue
}
shouldUpdateContainer, err := plc.ShouldUpdate(containerImageRef.Tag(), eventRepoRef.Tag())
if err != nil {
log.WithFields(log.Fields{
"error": err,
"parsed_image_name": containerImageRef.Remote(),
"target_image_name": repo.Name,
"policy": plc.Name(),
}).Error("provider.kubernetes: failed to check whether init container should be updated")
continue
}
if !shouldUpdateContainer {
continue
}
// updating spec template annotations
setUpdateTime(resource)
// updating image
if containerImageRef.Registry() == image.DefaultRegistryHostname {
resource.UpdateInitContainer(idx, fmt.Sprintf("%s:%s", containerImageRef.ShortName(), repo.Tag))
} else {
resource.UpdateInitContainer(idx, fmt.Sprintf("%s:%s", containerImageRef.Repository(), repo.Tag))
}
shouldUpdateDeployment = true
updatePlan.CurrentVersion = containerImageRef.Tag()
updatePlan.NewVersion = repo.Tag
updatePlan.Resource = resource
}
}
for idx, c := range resource.Containers() {
containerImageRef, err := image.Parse(c.Image)
if err != nil {

View File

@ -627,6 +627,111 @@ func TestProvider_checkForUpdate(t *testing.T) {
wantShouldUpdateDeployment: true,
wantErr: false,
},
{
name: "update init container if tracking is enabled",
args: args{
policy: policy.NewForcePolicy(false),
repo: &types.Repository{Name: "gcr.io/v2-namespace/hello-world", Tag: "latest"},
resource: MustParseGR(&apps_v1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{types.KeelInitContainerAnnotation: "true"},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
apps_v1.DeploymentSpec{
Template: v1.PodTemplateSpec{
ObjectMeta: meta_v1.ObjectMeta{
Annotations: map[string]string{
"this": "that",
},
},
Spec: v1.PodSpec{
InitContainers: []v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world",
},
},
},
},
},
apps_v1.DeploymentStatus{},
}),
},
wantUpdatePlan: &UpdatePlan{
Resource: MustParseGR(&apps_v1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{types.KeelInitContainerAnnotation: "true"},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
apps_v1.DeploymentSpec{
Template: v1.PodTemplateSpec{
ObjectMeta: meta_v1.ObjectMeta{
Annotations: map[string]string{
"this": "that",
},
},
Spec: v1.PodSpec{
InitContainers: []v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world:latest",
},
},
},
},
},
apps_v1.DeploymentStatus{},
}),
NewVersion: "latest",
CurrentVersion: "latest",
},
wantShouldUpdateDeployment: true,
wantErr: false,
},
{
name: "do not update init container if tracking is disabled (default)",
args: args{
policy: policy.NewForcePolicy(false),
repo: &types.Repository{Name: "gcr.io/v2-namespace/hello-world", Tag: "latest"},
resource: MustParseGR(&apps_v1.Deployment{
meta_v1.TypeMeta{},
meta_v1.ObjectMeta{
Name: "dep-1",
Namespace: "xxxx",
Annotations: map[string]string{},
Labels: map[string]string{types.KeelPolicyLabel: "all"},
},
apps_v1.DeploymentSpec{
Template: v1.PodTemplateSpec{
ObjectMeta: meta_v1.ObjectMeta{
Annotations: map[string]string{
"this": "that",
},
},
Spec: v1.PodSpec{
InitContainers: []v1.Container{
{
Image: "gcr.io/v2-namespace/hello-world",
},
},
},
},
},
apps_v1.DeploymentStatus{},
}),
},
wantUpdatePlan: &UpdatePlan{
// Resource: &k8s.GenericResource{},
Resource: nil,
},
wantShouldUpdateDeployment: false,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -39,6 +39,9 @@ const KeelMatchPreReleaseAnnotation = "keel.sh/matchPreRelease"
// KeelPollScheduleAnnotation - optional variable to setup custom schedule for polling, defaults to @every 10m
const KeelPollScheduleAnnotation = "keel.sh/pollSchedule"
// KeelInitContainerAnnotation - label or annotation to track init containers, defaults to false for backward compatibility
const KeelInitContainerAnnotation = "keel.sh/initContainers"
// KeelPollDefaultSchedule - defaul polling schedule
var KeelPollDefaultSchedule = "@every 1m"