Skip to content
Open
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
124 changes: 124 additions & 0 deletions e2e/examples_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright 2024 Google LLC

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2026?

Usually addlicense should handle this correctly.

//
// 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
//
// https://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 e2e

import (
"context"
"os/exec"
"testing"
"time"

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
"sigs.k8s.io/controller-runtime/pkg/client"
)

func TestInjectGMPSidecarExample(t *testing.T) {
ctx := contextWithDeadline(t)

kubeClient, _, err := setupCluster(ctx, t)
if err != nil {
t.Fatalf("error setting up cluster: %s", err)
}

// 1. Create example-deployment in default namespace.
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "example-deployment",
Namespace: "default",
},
Spec: appsv1.DeploymentSpec{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": "example-service",
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": "example-service",
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "example-service",
Image: "nginx:latest", // Just a dummy image.
},
},
},
},
},
}

if err := kubeClient.Create(ctx, deployment); err != nil {
t.Fatalf("error creating example-deployment: %s", err)
}
defer func() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the test times out and ctx expires, these deferred deletions will fail with context deadline exceeded, leaving the deployment and ConfigMap behind in the test cluster. Consider creating a fresh background context with a short timeout here for cleanup operations.

_ = kubeClient.Delete(ctx, deployment)
_ = kubeClient.Delete(ctx, &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "example-deployment",
Namespace: deployment.Namespace,
},
})
}()

// 2. Run the bash script.
cmd := exec.CommandContext(ctx, "../examples/inject-gmp-sidecar.sh")
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("error running inject-gmp-sidecar.sh: %s\n%s", err, string(out))
}
t.Logf("script output: %s", string(out))

// 3. Verify sidecars are injected and ConfigMap is created.
if err := wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(ctx context.Context) (bool, error) {
cm := &corev1.ConfigMap{}
if getErr := kubeClient.Get(ctx, client.ObjectKey{Name: "example-deployment", Namespace: "default"}, cm); getErr != nil {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep this consistent with the cleanup logic above and avoid hardcoded strings, consider referencing deployment.Name and deployment.Namespace here and on line 100:

Suggested change
if getErr := kubeClient.Get(ctx, client.ObjectKey{Name: "example-deployment", Namespace: "default"}, cm); getErr != nil {
if getErr := kubeClient.Get(ctx, client.ObjectKey{Name: deployment.Name, Namespace: deployment.Namespace}, cm); getErr != nil {

return false, nil //nolint:nilerr
}
if cm.Data["config.yaml"] == "" {
return false, nil //nolint:nilerr
}

dep := &appsv1.Deployment{}
if getErr := kubeClient.Get(ctx, client.ObjectKey{Name: "example-deployment", Namespace: "default"}, dep); getErr != nil {
return false, nil //nolint:nilerr
}

containers := dep.Spec.Template.Spec.Containers
if len(containers) != 3 {
return false, nil //nolint:nilerr
}

hasProm := false
hasReloader := false
for _, c := range containers {
if c.Name == "prometheus" {
hasProm = true
}
if c.Name == "config-reloader" {
hasReloader = true
}
}

return hasProm && hasReloader, nil
}); err != nil {
t.Fatalf("failed to verify injected sidecars or config map: %s", err)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This polling loop confirms that the Deployment spec in the API server was patched with the sidecars, but it doesn't verify that the pods actually start up cleanly at runtime without crashing or failing image pulls. Consider waiting for the deployment pods to reach ready status after this check so the test catches runtime failures or config unmarshalling errors.

}
140 changes: 140 additions & 0 deletions examples/inject-gmp-sidecar.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
# Copyright 2024 Google LLC
#
# 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.

set -exuo pipefail

KIND="deployment"
NAMESPACE="default"
NAME="example-deployment"
CONTAINER="example-service"
METRICS_PATH="/metrics"
PORT=80

if ! command -v yq &> /dev/null; then
echo "Error: yq is not installed. Please install it (e.g., via 'go install github.com/mikefarah/yq/v4@latest' or from https://github.com/mikefarah/yq)." >&2
exit 1
fi

# Extract labels.
CLUSTER_NAME=$(kubectl -n gmp-system get configmap/collector -o jsonpath='{.data.config\.yaml}' | yq '.global.external_labels.cluster')
LOCATION=$(kubectl -n gmp-system get configmap/collector -o jsonpath='{.data.config\.yaml}' | yq '.global.external_labels.location')
PROJECT_ID=$(kubectl -n gmp-system get configmap/collector -o jsonpath='{.data.config\.yaml}' | yq '.global.external_labels.project_id')


if [[ -z "${CLUSTER_NAME}" || -z "${LOCATION}" || -z "${PROJECT_ID}" ]]; then

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If any of these label keys are missing in the ConfigMap, yq outputs the literal string "null" rather than an empty string, which bypasses the -z check. We should check for "null" explicitly as well:

Suggested change
if [[ -z "${CLUSTER_NAME}" || -z "${LOCATION}" || -z "${PROJECT_ID}" ]]; then
if [[ -z "${CLUSTER_NAME}" || "${CLUSTER_NAME}" == "null" || -z "${LOCATION}" || "${LOCATION}" == "null" || -z "${PROJECT_ID}" || "${PROJECT_ID}" == "null" ]]; then

echo "Error: Failed to extract cluster, location, or project_id from gmp-system/collector configmap." >&2
exit 1
fi

# Images need to be updated periodically.
DISTROLESS_IMAGE=gke.gcr.io/gke-distroless/bash:gke_distroless_20240220.00_p0@sha256:828371616edc2c38e36868e2f8c992df37e484df72670f148de59867dfdd2490
PROMETHEUS_IMAGE=gke.gcr.io/prometheus-engine/prometheus:v2.53.5-gmp.4-gke.0@sha256:6f349dc0be36c8a61be183254f1126c9935f5332daa96c481f7e0e1b20fe0513
CONFIG_RELOADER_IMAGE=gke.gcr.io/prometheus-engine/config-reloader:v0.18.0-gke.2@sha256:b41862ee7ee3e9f24112ccdb0e53060085af1a8347054a7dbcff04467d3e1e9c
Comment on lines +41 to +44

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should integrate this into our regular scanning/bumping. Or reference images from the manifests? 🤔

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I was thinking to do this in separate step, but we should


# Define the scrape configuration.
CONFIG_MAP=$(
cat <<INNER_EOF
global:
scrape_interval: 30s
external_labels:
cluster: ${CLUSTER_NAME}
location: ${LOCATION}
project_id: ${PROJECT_ID}
scrape_configs:
- job_name: DedicatedCollector/${NAME}
metrics_path: ${METRICS_PATH}
static_configs:
- targets: ['localhost:${PORT}']
labels:
# Common GMP labels.
container: ${CONTAINER}
node: \$(NODE_NAME)
pod: \$(POD_NAME)
top_level_controller_name: ${NAME}
top_level_controller_type: ${KIND}
INNER_EOF
)



kubectl -n "${NAMESPACE}" create configmap "${NAME}" --from-literal="config.yaml=${CONFIG_MAP}" --dry-run=client -o yaml | kubectl apply -f -

# Construct Strategic Merge Patch.
STRATEGIC_PATCH=$(
cat <<INNER_EOF
spec:
template:
spec:
volumes:
- name: config
configMap:
name: ${NAME}
- name: prometheus-db
emptyDir: {}
- name: config-out
emptyDir: {}
initContainers:
- name: config-init
image: ${DISTROLESS_IMAGE}
command: ["/bin/bash", "-c", ": > /prometheus/config_out/config.yaml"]
volumeMounts:
- name: config-out
mountPath: /prometheus/config_out
containers:
- name: prometheus
image: ${PROMETHEUS_IMAGE}
args:
- --config.file=/prometheus/config_out/config.yaml
- --storage.tsdb.path=/prometheus/data
- --storage.tsdb.retention.time=24h
- --web.enable-lifecycle
- --storage.tsdb.no-lockfile
- --web.route-prefix=/
- --log.level=debug
ports:
- containerPort: 9090
volumeMounts:
- name: config-out
mountPath: /prometheus/config_out
readOnly: true
- name: prometheus-db
mountPath: /prometheus/data
- name: config-reloader
image: ${CONFIG_RELOADER_IMAGE}
args:
- --config-file=/prometheus/config/config.yaml
- --config-file-output=/prometheus/config_out/config.yaml
- --reload-url=http://localhost:9090/-/reload
- --ready-url=http://localhost:9090/-/ready
- --listen-address=:19091
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
volumeMounts:
- name: config
mountPath: /prometheus/config
- name: config-out
mountPath: /prometheus/config_out
INNER_EOF
)

# Apply the Patch
kubectl -n "${NAMESPACE}" patch "${KIND}" "${NAME}" --patch "${STRATEGIC_PATCH}"
3 changes: 3 additions & 0 deletions hack/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ RUN ARCH=$(dpkg --print-architecture) && \
curl -Lo ./jq "https://github.com/jqlang/jq/releases/download/jq-1.7/jq-linux-${ARCH}" && \
install -o root -g root -m 0755 jq /usr/local/bin/jq && \
rm jq && \
curl -Lo ./yq "https://github.com/mikefarah/yq/releases/download/v4.44.3/yq_linux_${ARCH}" && \
install -o root -g root -m 0755 yq /usr/local/bin/yq && \
rm yq && \
curl -Lo ./kind "https://kind.sigs.k8s.io/dl/v0.26.0/kind-linux-${ARCH}" && \
install -o root -g root -m 0755 kind /usr/local/bin/kind && \
rm kind
Expand Down
Loading