Skip to content

Commit

Permalink
feat: #164 Send events on image change (#167)
Browse files Browse the repository at this point in the history
* Send events on image change

* fix lints

* fix the rest of lints

* Addressing feedback: parameterize kubeEvents

* small fixes

* Running kustomize

* try to fix the problem after running make manifests

* Move the message to the correct place

* reduce annotation size and randomness

* Provide full image names as well

* goimports/lint
  • Loading branch information
magmax committed Mar 26, 2021
1 parent 21df300 commit 274d19c
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 19 deletions.
21 changes: 13 additions & 8 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ type ImageUpdaterConfig struct {
AppNamePatterns []string
GitCommitUser string
GitCommitMail string
DisableKubeEvents bool
}

// warmupImageCache performs a cache warm-up, which is basically one cycle of
Expand Down Expand Up @@ -145,13 +146,14 @@ func runImageUpdater(cfg *ImageUpdaterConfig, warmUp bool) (argocd.ImageUpdaterR
defer sem.Release(1)
log.Debugf("Processing application %s", app)
upconf := &argocd.UpdateConfiguration{
NewRegFN: registry.NewClient,
ArgoClient: cfg.ArgoClient,
KubeClient: cfg.KubeClient,
UpdateApp: &curApplication,
DryRun: dryRun,
GitCommitUser: cfg.GitCommitUser,
GitCommitEmail: cfg.GitCommitMail,
NewRegFN: registry.NewClient,
ArgoClient: cfg.ArgoClient,
KubeClient: cfg.KubeClient,
UpdateApp: &curApplication,
DryRun: dryRun,
GitCommitUser: cfg.GitCommitUser,
GitCommitEmail: cfg.GitCommitMail,
DisableKubeEvents: cfg.DisableKubeEvents,
}
res := argocd.UpdateApplication(upconf)
result.NumApplicationsProcessed += 1
Expand Down Expand Up @@ -238,6 +240,7 @@ func newTestCommand() *cobra.Command {
kubeConfig string
disableKubernetes bool
ignoreTags []string
disableKubeEvents bool
)
var runCmd = &cobra.Command{
Use: "test IMAGE",
Expand All @@ -251,7 +254,7 @@ to using the given parametrization. Command line switches can be used as a
way to supply the required parameters.
`,
Example: `
# In the most simple form, check for the latest available (semver) version of
# In the most simple form, check for the latest available (semver) version of
# an image in the registry
argocd-image-updater test nginx
Expand Down Expand Up @@ -372,6 +375,7 @@ argocd-image-updater test nginx --allow-tags '^1.19.\d+(\-.*)*$' --update-strate
runCmd.Flags().BoolVar(&disableKubernetes, "disable-kubernetes", false, "whether to disable the Kubernetes client")
runCmd.Flags().StringVar(&kubeConfig, "kubeconfig", "", "path to your Kubernetes client configuration")
runCmd.Flags().StringVar(&credentials, "credentials", "", "the credentials definition for the test (overrides registry config)")
runCmd.Flags().BoolVar(&disableKubeEvents, "disable-kubernetes-events", false, "Disable kubernetes events")
return runCmd
}

Expand Down Expand Up @@ -542,6 +546,7 @@ func newRunCommand() *cobra.Command {
runCmd.Flags().BoolVar(&warmUpCache, "warmup-cache", true, "whether to perform a cache warm-up on startup")
runCmd.Flags().StringVar(&cfg.GitCommitUser, "git-commit-user", env.GetStringVal("GIT_COMMIT_USER", "argocd-image-updater"), "Username to use for Git commits")
runCmd.Flags().StringVar(&cfg.GitCommitMail, "git-commit-email", env.GetStringVal("GIT_COMMIT_EMAIL", "noreply@argoproj.io"), "E-Mail address to use for Git commits")
runCmd.Flags().BoolVar(&cfg.DisableKubeEvents, "disable-kube-events", env.GetBoolVal("IMAGE_UPDATER_KUBE_EVENTS", false), "Disable kubernetes events")

return runCmd
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ spec:
name: argocd-image-updater-config
key: git.email
optional: true
- name: IMAGE_UPDATER_KUBE_EVENTS
valueFrom:
configMapKeyRef:
name: argocd-image-updater-config
key: kube.events
optional: true
livenessProbe:
httpGet:
path: /healthz
Expand Down
7 changes: 7 additions & 0 deletions manifests/base/rbac/argocd-image-updater-role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,10 @@ rules:
- list
- update
- patch
- apiGroups:
- ""
resources:
- events
verbs:
- create

12 changes: 12 additions & 0 deletions manifests/install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ rules:
- list
- update
- patch
- apiGroups:
- ""
resources:
- events
verbs:
- create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
Expand Down Expand Up @@ -143,6 +149,12 @@ spec:
key: git.email
name: argocd-image-updater-config
optional: true
- name: IMAGE_UPDATER_KUBE_EVENTS
valueFrom:
configMapKeyRef:
key: kube.events
name: argocd-image-updater-config
optional: true
image: argoprojlabs/argocd-image-updater:latest
imagePullPolicy: Always
livenessProbe:
Expand Down
43 changes: 34 additions & 9 deletions pkg/argocd/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/argoproj-labs/argocd-image-updater/pkg/kube"
"github.com/argoproj-labs/argocd-image-updater/pkg/log"
"github.com/argoproj-labs/argocd-image-updater/pkg/registry"
"github.com/argoproj-labs/argocd-image-updater/pkg/tag"

"gopkg.in/yaml.v2"

Expand All @@ -32,13 +33,14 @@ type ImageUpdaterResult struct {
}

type UpdateConfiguration struct {
NewRegFN registry.NewRegistryClient
ArgoClient ArgoCD
KubeClient *kube.KubernetesClient
UpdateApp *ApplicationImages
DryRun bool
GitCommitUser string
GitCommitEmail string
NewRegFN registry.NewRegistryClient
ArgoClient ArgoCD
KubeClient *kube.KubernetesClient
UpdateApp *ApplicationImages
DryRun bool
GitCommitUser string
GitCommitEmail string
DisableKubeEvents bool
}

type GitCredsSource func(app *v1alpha1.Application) (git.Creds, error)
Expand Down Expand Up @@ -79,12 +81,19 @@ type helmOverride struct {
Helm helmParameters `json:"helm"`
}

type change struct {
image *image.ContainerImage
oldTag *tag.ImageTag
newTag *tag.ImageTag
}

// UpdateApplication update all images of a single application. Will run in a goroutine.
func UpdateApplication(updateConf *UpdateConfiguration) ImageUpdaterResult {
var needUpdate bool = false

result := ImageUpdaterResult{}
app := updateConf.UpdateApp.Application.GetName()
changeList := make([]change, 0)

// Get all images that are deployed with the current application
applicationImages := GetImagesFromApplication(&updateConf.UpdateApp.Application)
Expand Down Expand Up @@ -203,8 +212,9 @@ func UpdateApplication(updateConf *UpdateConfiguration) ImageUpdaterResult {
result.NumErrors += 1
continue
} else {
imgCtx.Infof("Successfully updated image '%s' to '%s', but pending spec update (dry run=%v)", updateableImage.GetFullNameWithTag(), updateableImage.WithTag(latest).GetFullNameWithTag(), updateConf.DryRun)
result.NumImagesUpdated += 1
containerImageNew := updateableImage.WithTag(latest)
imgCtx.Infof("Successfully updated image '%s' to '%s', but pending spec update (dry run=%v)", updateableImage.GetFullNameWithTag(), containerImageNew.GetFullNameWithTag(), updateConf.DryRun)
changeList = append(changeList, change{containerImageNew, updateableImage.ImageTag, containerImageNew.ImageTag})
}
} else {
// We need to explicitly set the up-to-date images in the spec too, so
Expand Down Expand Up @@ -244,6 +254,21 @@ func UpdateApplication(updateConf *UpdateConfiguration) ImageUpdaterResult {
result.NumImagesUpdated = 0
} else {
logCtx.Infof("Successfully updated the live application spec")
result.NumImagesUpdated += 1
if !updateConf.DisableKubeEvents && updateConf.KubeClient != nil {
annotations := map[string]string{}
for i, c := range changeList {
annotations[fmt.Sprintf("argocd-image-updater.image-%d/full-image-name", i)] = c.image.GetFullNameWithoutTag()
annotations[fmt.Sprintf("argocd-image-updater.image-%d/image-name", i)] = c.image.ImageName
annotations[fmt.Sprintf("argocd-image-updater.image-%d/old-tag", i)] = c.oldTag.TagName
annotations[fmt.Sprintf("argocd-image-updater.image-%d/new-tag", i)] = c.newTag.TagName
}
message := fmt.Sprintf("Successfully updated application '%s'", app)
_, err = updateConf.KubeClient.CreateApplicationEvent(&updateConf.UpdateApp.Application, "ImagesUpdated", message, annotations)
if err != nil {
logCtx.Warnf("Event could not be sent: %v", err)
}
}
}
} else {
logCtx.Infof("Dry run - not commiting %d changes to application", result.NumImagesUpdated)
Expand Down
43 changes: 41 additions & 2 deletions pkg/kube/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import (
"context"
"fmt"
"os"
"time"

"github.com/argoproj-labs/argocd-image-updater/pkg/metrics"

appv1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
"github.com/argoproj/argo-cd/pkg/client/clientset/versioned"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
)
Expand Down Expand Up @@ -68,7 +71,7 @@ func NewKubernetesClientFromConfig(ctx context.Context, namespace string, kubeco

// GetSecretData returns the raw data from named K8s secret in given namespace
func (client *KubernetesClient) GetSecretData(namespace string, secretName string) (map[string][]byte, error) {
secret, err := client.Clientset.CoreV1().Secrets(namespace).Get(client.Context, secretName, v1.GetOptions{})
secret, err := client.Clientset.CoreV1().Secrets(namespace).Get(client.Context, secretName, metav1.GetOptions{})
metrics.Clients().IncreaseK8sClientRequest(1)
if err != nil {
metrics.Clients().IncreaseK8sClientRequest(1)
Expand All @@ -91,3 +94,39 @@ func (client *KubernetesClient) GetSecretField(namespace string, secretName stri
return string(data), nil
}
}

// CreateApplicationevent creates a kubernetes event with a custom reason and message for an application.
func (client *KubernetesClient) CreateApplicationEvent(app *appv1alpha1.Application, reason string, message string, annotations map[string]string) (*v1.Event, error) {
t := metav1.Time{Time: time.Now()}

event := v1.Event{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%v.%x", app.ObjectMeta.Name, t.UnixNano()),
Namespace: client.Namespace,
Annotations: annotations,
},
Source: v1.EventSource{
Component: "ArgocdImageUpdater",
},
InvolvedObject: v1.ObjectReference{
Kind: app.Kind,
APIVersion: app.APIVersion,
Name: app.ObjectMeta.Name,
Namespace: app.ObjectMeta.Namespace,
ResourceVersion: app.ObjectMeta.ResourceVersion,
UID: app.ObjectMeta.UID,
},
FirstTimestamp: t,
LastTimestamp: t,
Count: 1,
Message: message,
Type: v1.EventTypeNormal,
Reason: reason,
}

result, err := client.Clientset.CoreV1().Events(client.Namespace).Create(client.Context, &event, metav1.CreateOptions{})
if err != nil {
return nil, err
}
return result, nil
}
30 changes: 30 additions & 0 deletions pkg/kube/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"context"
"testing"

appv1alpha1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/argoproj-labs/argocd-image-updater/test/fake"
"github.com/argoproj-labs/argocd-image-updater/test/fixture"

Expand Down Expand Up @@ -66,3 +69,30 @@ func Test_GetDataFromSecrets(t *testing.T) {
require.Empty(t, data)
})
}

func Test_CreateApplicationEvent(t *testing.T) {
t.Run("Create Event", func(t *testing.T) {
application := &appv1alpha1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "argocd",
},
Spec: appv1alpha1.ApplicationSpec{},
Status: appv1alpha1.ApplicationStatus{
Summary: appv1alpha1.ApplicationSummary{
Images: []string{"nginx:1.12.2", "that/image", "quay.io/dexidp/dex:v1.23.0"},
},
},
}
annotations := map[string]string{
"origin": "nginx:1.12.2",
}
clientset := fake.NewFakeClientsetWithResources()
client := &KubernetesClient{Clientset: clientset, Namespace: "default"}
event, err := client.CreateApplicationEvent(application, "TestEvent", "test-message", annotations)
require.NoError(t, err)
require.NotNil(t, event)
assert.Equal(t, "ArgocdImageUpdater", event.Source.Component)
assert.Equal(t, "default", client.Namespace)
})
}

0 comments on commit 274d19c

Please sign in to comment.