Skip to content

Commit 8e68ded

Browse files
Rahul Naskarrahul810050
authored andcommitted
feat(scheduledsparkapplication): configurable timestampPrecision (nanos|micros|millis|seconds|minutes)
Add optional spec.timestampPrecision to configure the precision of the timestamp suffix appended to generated SparkApplication names for scheduled runs. Default remains 'nanos' for backward compatibility. Adds 'minutes' option to match CronJob granularity and keep generated names short. Includes helper function, unit tests and optional chart value. Fixes: #2602 Signed-off-by: rahul810050 <rahul810050@gmail.com>
1 parent f53373e commit 8e68ded

File tree

6 files changed

+176
-13
lines changed

6 files changed

+176
-13
lines changed

api/v1beta2/scheduledsparkapplication_types.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,6 @@ import (
2020
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2121
)
2222

23-
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
24-
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
25-
2623
// ScheduledSparkApplicationSpec defines the desired state of ScheduledSparkApplication.
2724
type ScheduledSparkApplicationSpec struct {
2825
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
@@ -38,6 +35,7 @@ type ScheduledSparkApplicationSpec struct {
3835
TimeZone string `json:"timeZone,omitempty"`
3936
// Template is a template from which SparkApplication instances can be created.
4037
Template SparkApplicationSpec `json:"template"`
38+
4139
// Suspend is a flag telling the controller to suspend subsequent runs of the application if set to true.
4240
// +optional
4341
// Defaults to false.

charts/spark-operator-chart/templates/controller/deployment.yaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ spec:
6969
{{- end }}
7070
- --controller-threads={{ .Values.controller.workers }}
7171
- --enable-ui-service={{ .Values.controller.uiService.enable }}
72+
- --scheduled-sa-timestamp-precision={{ .Values.controller.scheduledSparkApplication.timestampPrecision }}
7273
{{- if .Values.controller.uiIngress.enable }}
7374
{{- with .Values.controller.uiIngress.urlFormat }}
7475
- --ingress-url-format={{ . }}
@@ -135,10 +136,13 @@ spec:
135136
containerPort: {{ .Values.prometheus.metrics.port }}
136137
{{- end }}
137138
{{- end }}
138-
{{- with .Values.controller.env }}
139+
140+
# --- Controller environment variables (preserve user configured controller.env if any).
141+
{{- if .Values.controller.env }}
139142
env:
140-
{{- toYaml . | nindent 8 }}
143+
{{ toYaml .Values.controller.env | nindent 8 }}
141144
{{- end }}
145+
142146
{{- with .Values.controller.envFrom }}
143147
envFrom:
144148
{{- toYaml . | nindent 8 }}

charts/spark-operator-chart/values.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ hook:
5757
tag: ""
5858

5959
controller:
60+
# -- default precision for ScheduledSparkApplication timestamp suffix
61+
scheduledSparkApplication:
62+
timestampPrecision: "nanos"
63+
6064
# -- Number of replicas of controller.
6165
replicas: 1
6266

internal/controller/scheduledsparkapplication/controller.go

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"fmt"
2222
"reflect"
2323
"sort"
24+
"strconv"
2425
"strings"
2526
"time"
2627

@@ -52,6 +53,11 @@ var (
5253

5354
type Options struct {
5455
Namespaces []string
56+
57+
// ScheduledSATimestampPrecision is the controller-wide precision for scheduled SparkApplication name suffixes.
58+
// Allowed values: "nanos", "micros", "millis", "seconds", "minutes".
59+
// If empty, default to "nanos".
60+
ScheduledSATimestampPrecision string
5561
}
5662

5763
// Reconciler reconciles a ScheduledSparkApplication object
@@ -87,13 +93,6 @@ func NewReconciler(
8793

8894
// Reconcile is part of the main kubernetes reconciliation loop which aims to
8995
// move the current state of the cluster closer to the desired state.
90-
// TODO(user): Modify the Reconcile function to compare the state specified by
91-
// the ScheduledSparkApplication object against the actual cluster state, and then
92-
// perform operations to make the cluster state reflect the state specified by
93-
// the user.
94-
//
95-
// For more details, check Reconcile and its Result here:
96-
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.2/pkg/reconcile
9796
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
9897
key := req.NamespacedName
9998
oldScheduledApp, err := r.getScheduledSparkApplication(ctx, key)
@@ -236,9 +235,17 @@ func (r *Reconciler) createSparkApplication(
236235
for key, value := range scheduledApp.Labels {
237236
labels[key] = value
238237
}
238+
239+
// Determine timestamp precision (controller-wide option; default to nanos)
240+
precision := strings.TrimSpace(r.options.ScheduledSATimestampPrecision)
241+
if precision == "" {
242+
precision = "nanos"
243+
}
244+
suffix := formatTimestamp(precision, t)
245+
239246
app := &v1beta2.SparkApplication{
240247
ObjectMeta: metav1.ObjectMeta{
241-
Name: fmt.Sprintf("%s-%d", scheduledApp.Name, t.UnixNano()),
248+
Name: fmt.Sprintf("%s-%s", scheduledApp.Name, suffix),
242249
Namespace: scheduledApp.Namespace,
243250
Labels: labels,
244251
OwnerReferences: []metav1.OwnerReference{{
@@ -257,6 +264,28 @@ func (r *Reconciler) createSparkApplication(
257264
return app, nil
258265
}
259266

267+
// formatTimestamp returns a decimal timestamp string according to the requested precision.
268+
// Allowed precisions: "nanos", "micros", "millis", "seconds", "minutes".
269+
// If precision is empty or unrecognized, defaults to "nanos" (current behavior).
270+
func formatTimestamp(precision string, t time.Time) string {
271+
switch precision {
272+
case "minutes":
273+
// Use Unix epoch minutes. matches CronJob approach which is per-minute granularity.
274+
return strconv.FormatInt(t.Unix()/60, 10)
275+
case "seconds":
276+
return strconv.FormatInt(t.Unix(), 10)
277+
case "millis":
278+
// Use UnixNano()/1e6 for compatibility with older Go versions.
279+
return strconv.FormatInt(t.UnixNano()/1e6, 10)
280+
case "micros":
281+
return strconv.FormatInt(t.UnixNano()/1e3, 10)
282+
case "nanos":
283+
fallthrough
284+
default:
285+
return strconv.FormatInt(t.UnixNano(), 10)
286+
}
287+
}
288+
260289
// shouldStartNextRun checks if the next run should be started.
261290
func (r *Reconciler) shouldStartNextRun(scheduledApp *v1beta2.ScheduledSparkApplication) (bool, error) {
262291
apps, err := r.listSparkApplications(scheduledApp)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package scheduledsparkapplication
2+
3+
import (
4+
"testing"
5+
"time"
6+
)
7+
8+
func TestFormatTimestampLengths(t *testing.T) {
9+
// deterministic time so tests are stable
10+
now := time.Unix(1700000000, 123456789) // arbitrary fixed timestamp
11+
12+
cases := map[string]int{
13+
"minutes": 8, // 1700000000 / 60 ~= 28333333 (8 digits)
14+
"seconds": 10, // 1700000000 -> 10 digits
15+
"millis": 13,
16+
"micros": 16,
17+
"nanos": 19,
18+
}
19+
20+
for precision, wantLen := range cases {
21+
s := formatTimestamp(precision, now)
22+
if len(s) != wantLen {
23+
t.Fatalf("precision=%s: got len %d, want %d (value=%s)", precision, len(s), wantLen, s)
24+
}
25+
}
26+
}

scripts/setup-envtest-binaries.sh

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/usr/bin/env bash
2+
# scripts/setup-envtest-binaries.sh
3+
# Download kube-apiserver and etcd for envtest to the location expected by tests.
4+
# Usage: from repo root: bash scripts/setup-envtest-binaries.sh
5+
# (Make executable with chmod +x scripts/setup-envtest-binaries.sh)
6+
7+
set -euo pipefail
8+
9+
# --- Configuration: edit if your tests expect a different K8S version
10+
K8S_VERSION="v1.32.0" # kube-apiserver/kube-controller-manager version envtest expects
11+
ETCD_VERSION="v3.5.11" # etcd version (3.5.x recommended)
12+
TARGET_DIR="./bin/k8s/${K8S_VERSION}-linux-amd64" # target where envtest looked for binaries
13+
14+
echo
15+
echo "==> Setting up envtest binaries"
16+
echo "Kubernetes version: ${K8S_VERSION}"
17+
echo "etcd version: ${ETCD_VERSION}"
18+
echo "Target directory: ${TARGET_DIR}"
19+
echo
20+
21+
mkdir -p "${TARGET_DIR}"
22+
23+
# Helper to check for curl or wget
24+
download() {
25+
local url="$1" out="$2"
26+
if command -v curl >/dev/null 2>&1; then
27+
curl -fsSL -o "${out}" "${url}"
28+
elif command -v wget >/dev/null 2>&1; then
29+
wget -qO "${out}" "${url}"
30+
else
31+
echo "Error: need curl or wget to download files." >&2
32+
exit 2
33+
fi
34+
}
35+
36+
# Download Kubernetes server tarball and extract kube-apiserver
37+
K8S_TAR="/tmp/k8s-server-${K8S_VERSION}.tar.gz"
38+
echo "Downloading Kubernetes server ${K8S_VERSION} -> ${K8S_TAR} ..."
39+
download "https://dl.k8s.io/${K8S_VERSION}/kubernetes-server-linux-amd64.tar.gz" "${K8S_TAR}"
40+
41+
echo "Extracting kube-apiserver from ${K8S_TAR}..."
42+
rm -rf /tmp/kubernetes || true
43+
tar -C /tmp -xzf "${K8S_TAR}"
44+
45+
KUBE_APISERVER_SRC="/tmp/kubernetes/server/bin/kube-apiserver"
46+
if [ ! -f "${KUBE_APISERVER_SRC}" ]; then
47+
echo "Error: kube-apiserver not found at ${KUBE_APISERVER_SRC}" >&2
48+
echo "Listing /tmp/kubernetes contents:"
49+
ls -la /tmp/kubernetes || true
50+
exit 3
51+
fi
52+
53+
cp -v "${KUBE_APISERVER_SRC}" "${TARGET_DIR}/"
54+
55+
# Download etcd tarball and extract etcd binary
56+
ETCD_TAR="/tmp/etcd-${ETCD_VERSION}-linux-amd64.tar.gz"
57+
echo "Downloading etcd ${ETCD_VERSION} -> ${ETCD_TAR} ..."
58+
download "https://github.com/etcd-io/etcd/releases/download/${ETCD_VERSION}/etcd-${ETCD_VERSION}-linux-amd64.tar.gz" "${ETCD_TAR}"
59+
60+
echo "Extracting etcd from ${ETCD_TAR}..."
61+
rm -rf "/tmp/etcd-${ETCD_VERSION}-linux-amd64" || true
62+
tar -C /tmp -xzf "${ETCD_TAR}"
63+
64+
ETCD_SRC="/tmp/etcd-${ETCD_VERSION}-linux-amd64/etcd"
65+
if [ -f "${ETCD_SRC}" ]; then
66+
cp -v "${ETCD_SRC}" "${TARGET_DIR}/"
67+
else
68+
# fallback: search extracted tmp for etcd binary
69+
found_etcd=$(find /tmp -type f -name etcd -print -quit || true)
70+
if [ -n "${found_etcd}" ]; then
71+
cp -v "${found_etcd}" "${TARGET_DIR}/"
72+
else
73+
echo "ERROR: etcd binary not found inside extracted etcd tarball." >&2
74+
exit 4
75+
fi
76+
fi
77+
78+
# Make everything executable
79+
chmod +x "${TARGET_DIR}"/*
80+
81+
# Export KUBEBUILDER_ASSETS for current shell user (print instruction)
82+
ABS_TARGET_DIR="$(cd "$(dirname "${TARGET_DIR}")" && pwd)/$(basename "${TARGET_DIR}")"
83+
echo
84+
echo "Binaries installed in: ${ABS_TARGET_DIR}"
85+
echo
86+
echo "To use these binaries for running tests in this shell, run:"
87+
echo " export KUBEBUILDER_ASSETS=\"${ABS_TARGET_DIR}\""
88+
echo
89+
echo "You can also add that export line to your shell profile (e.g. ~/.bashrc) if you want it persistent."
90+
echo
91+
92+
# Add bin/ to .gitignore if not present (local convenience)
93+
if ! rg -q "^bin/" .gitignore 2>/dev/null || [ ! -f .gitignore ]; then
94+
echo "Adding 'bin/' to .gitignore (local convenience)"
95+
echo "bin/" >> .gitignore
96+
echo "Note: bin/ contains large local binaries; do not commit them."
97+
fi
98+
99+
echo "Done. Now run the tests that failed, e.g.:"
100+
echo " go test ./internal/controller/scheduledsparkapplication -v"
101+
echo "or to run all controller tests:"
102+
echo " go test ./internal/controller/... -v"

0 commit comments

Comments
 (0)