57 Commits

Author SHA1 Message Date
a36e72877d feat(jar): add OCI registry pull for JAR artifacts with backward compat
- Add jarRef and jarPullSecret fields to FlinkJob CRD (jarUri/basicAuth deprecated)
- OCI pull via go-containerregistry with dual auth (K8s pull secret + env vars)
- Media type validation on pulled layers
- Atomic status patches with runningJarRef/runningJarDigest tracking
- NeedsUpgrade/RunningRef/RunningRefPatchData domain helpers
- README with usage guide, pushing JARs, and GitHub Actions CI/CD workflow
- CONTEXT.md domain glossary
2026-07-24 17:37:30 +03:30
91c10c89da feat(crd): add dual-format Args field supporting map and list
Add a custom Args type that accepts both a key-value map and a flat
CLI-style list, normalizing internally to []string so downstream code
requires no changes.

- Args: new []string alias with UnmarshalJSON (map + list) and
  MarshalJSON (map) — keys are sorted for deterministic ordering
- CRD schema: switch args from typed array to
  x-kubernetes-preserve-unknown-fields to accept either shape
- Bump go-flink-client to v0.2.2 (fix: programArgsList → programArg
  query parameter name for Flink REST API)

Args can now be written as:
  args:
    kafka-host: broker:9092
    kafka-group-id: my-group

Or the existing list format:
  args:
    - "--kafka-host"
    - broker:9092
2026-07-23 15:25:08 +03:30
7fb18cd45f feat: support multiple operator in single namespace 2026-07-23 13:51:33 +03:30
494d32c565 feat: update flink to 2.3 2026-07-22 10:48:53 +03:30
5ca1c28b33 fix(helm): wrong savepoint path config when storage-type is filesystem 2025-07-18 18:12:21 +03:30
d73292ac54 fix: resolve missing task manager statefulset savepoint pvc mount 2025-05-17 14:35:22 +03:30
f0df5ff937 fix: wrong fieldPath in task-manager statefulset spec.hostname 2025-05-17 14:02:01 +03:30
83c4b5ded2 feat(helm): add filesystem savepoint storage mode 2025-05-17 13:02:24 +03:30
89647f3b5b fix(helm): add flink taskmanager host env to task manager 2025-04-15 12:08:17 +03:30
dedbe00fba fix(helm): wrong checkpoint path flink properties 2025-04-13 10:38:15 +03:30
62c340bc64 feat(helm): add filesystem checkpoint storage mode 2025-04-13 10:00:32 +03:30
44ff3627fc feat(helm): add flink properties variable to values 2025-04-12 23:14:52 +03:30
392004d99a ci(docker): add zstd dependency jar to flink docker file 2025-04-12 23:07:53 +03:30
22c7d712f4 feat: update flink http client library 2025-04-07 13:20:39 +03:30
2dd625ec7c feat: update flink http client library 2025-04-07 11:28:33 +03:30
c991215a9d Merge branch 'main' of https://git.logicamp.tech/Logicamp/flink-kube-operator 2025-04-06 08:48:54 +03:30
1c32bfbbe0 chore: create index and chart package 2025-04-06 01:53:33 +03:30
f210090dff Merge branch 'feature/new-helm-structure' into HEAD 2025-04-06 01:49:21 +03:30
54008669cb fix(helm): wrong savepoint and checkpoint s3 configs 2025-04-06 01:49:00 +03:30
830e265162 feat: apply new helm structure
use minio s3 for savepoint and checkpoint path
separate task-manager, job-manager and operator
use statefulset for task-manager to handle replication
support basic credential for download jar request
update to flink 1.20.1
2025-04-05 01:39:02 +03:30
7f78faeed7 fix(helm): missing task-manager and job-manager configs in values 2025-04-04 13:51:57 +03:30
f2b627cee2 feat: add LOG_LEVEL to envs 2025-03-12 23:33:09 +03:30
4d6b06efe7 fix: restore watcher if channel is closed 2025-03-12 23:27:40 +03:30
6f91ad607f perf: remove go-nanoid and rxgo 2025-03-12 21:20:57 +03:30
b33dc0ba1d fix: resolve finalize is not working in some cases 2025-03-09 02:17:59 +03:30
556d9ff6af fix: wong update status in some situations 2025-03-05 11:40:22 +03:30
346f69100c fix: resolve missing restrict resources namespace 2025-03-04 23:55:28 +03:30
75d0557286 fix(helm): missing namespace env for operator container 2025-03-04 23:14:41 +03:30
012c525915 feat(helm): add clusterId to values 2025-03-04 18:05:01 +03:30
550b6882e1 fix(helm): missing kubernetes.cluster-id option in flink cluster 2025-03-04 17:46:32 +03:30
55dbe9f8c2 feat: add debug logs 2025-02-18 23:08:19 +03:30
1ff69e086f fix(log): add log 2025-01-25 01:05:17 +03:30
e60b96cac7 feat(docker): add jnanoid and jdbc connector jar files 2025-01-24 18:13:32 +03:30
222d70125c fix(helm): missing checkpoint interval configuration 2025-01-18 01:32:56 +03:30
9629e70ed7 fix(savepoint): wrong savepoint path in download savepoint route 2025-01-18 01:32:24 +03:30
e32addcea5 fix(helm): missing checkpoint interval configuration 2025-01-18 01:31:53 +03:30
625d5056e6 feat: merge operator to flink deployment 2025-01-18 01:14:40 +03:30
6591748d16 fix: resolve savepoint trigger no SavepointTriggerId problem 2025-01-18 00:31:05 +03:30
8f3c32ed95 fix: resolve missing handle job not found 2025-01-17 22:48:29 +03:30
4bbb027c41 fix: resolve job start after pause bug 2025-01-17 21:16:38 +03:30
896d45e15a fix: resolve crash operator in some cases 2025-01-17 21:11:17 +03:30
322877ad8e fix: resolve job start after pause bug 2025-01-17 21:09:53 +03:30
5066dc650f feat: add start and trigger savepoint routes 2025-01-17 20:27:32 +03:30
c977c8a15d feat: add download savepoint route 2025-01-17 18:34:06 +03:30
ef7b16af68 feet: add finalizer 2025-01-11 00:25:57 +03:30
14aba80181 feat: separate job-manager and task-manager containers 2025-01-10 12:39:54 +03:30
4cd00f25f0 feat: limit control to single current only namespace 2025-01-10 12:30:15 +03:30
0df874b222 feat: add rest routes to take the control of flink jobs 2025-01-09 00:22:53 +03:30
4dd82c6380 ci(docker): flink docker > add jdbc and postgress cdc and postgres sink jars 2024-12-26 22:20:57 +03:30
e4f756666d fix: missing savepoint path env in operator deployment 2024-12-25 01:46:20 +03:30
0bc45845dc fix(flink): wrong upload dir prop 2024-12-20 18:09:55 +03:30
b8e051911e fix(flink): wrong upload dir prop 2024-12-20 18:05:38 +03:30
b6e2838756 feat(flink): add readiness and liveness probe 2024-12-20 17:57:55 +03:30
8fa99f4ea8 fix(flink): wrong ha path 2024-12-20 17:56:13 +03:30
23a4de92e4 fix: missing ha pvc 2024-12-20 17:22:32 +03:30
37936c8c58 feat: separate ha and checkpoint pvc 2024-12-20 17:12:51 +03:30
4ed533f284 fix(manage-job): missing patch kube resource after run job 2024-12-20 14:45:29 +03:30
79 changed files with 2337 additions and 436 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@ db
tmp tmp
*.jar *.jar
.env .env
/operator

View File

@@ -8,12 +8,13 @@
"flink", "flink",
"gitea", "gitea",
"gonanoid", "gonanoid",
"huma",
"logicamp", "logicamp",
"Namespaceable", "Namespaceable",
"nindent", "nindent",
"reactivex", "reactivex",
"repsert", "repsert",
"rxgo", "taskmanager",
"tolerations" "tolerations"
] ]
} }

41
CONTEXT.md Normal file
View File

@@ -0,0 +1,41 @@
# Flink Kube Operator
Kubernetes operator that manages the lifecycle of Apache Flink jobs via CRDs.
## Language
**FlinkJob**:
A Kubernetes Custom Resource representing a desired Flink job. Contains spec (desired state) and status (observed state).
_Avoid_: Job CR, CRD instance
**ManagedJob**:
Runtime wrapper that drives a FlinkJob's lifecycle through its state machine (init → run → savepoint → upgrade).
_Avoid_: Job controller, job runner
**jarRef**:
Full OCI reference identifying a JAR artifact in a registry (e.g. `registry.io/org/app:1.0.0`).
_Avoid_: jarUri, jar URL, artifact URL
**jarDigest**:
SHA256 content hash of the JAR layer in the OCI manifest. Immutable identifier for the artifact bytes.
_Avoid_: content hash, checksum
**pullSecret**:
A `kubernetes.io/dockerconfigjson` Kubernetes Secret containing registry credentials, referenced by name in the FlinkJob spec.
_Avoid_: imagePullSecret, registry secret
**JarFile**:
Ephemeral artifact handle — a downloaded/pulled JAR on local disk awaiting upload to Flink, then deleted.
_Avoid_: temp jar, local jar
**LifeCycleStatus**:
Operator-managed lifecycle state of a FlinkJob (HEALTHY, RESTORING, GRACEFULLY_PAUSED, FAILED, etc.).
_Avoid_: operator status, lifecycle state
**JobStatus**:
Flink-reported job state (RUNNING, FAILED, FINISHED, etc.). Mirrored from the Flink REST API.
_Avoid_: flink status, job state
**Savepoint**:
A consistent snapshot of a Flink job's state, used for restart and upgrade.
_Avoid_: checkpoint, snapshot

View File

@@ -1,8 +1,8 @@
FROM public.ecr.aws/docker/library/golang:1.23.4-bookworm AS build FROM public.ecr.aws/docker/library/golang:1.24.1-bookworm AS build
ARG upx_version=4.2.4 ARG upx_version=4.2.4
RUN apt-get update && apt-get install -y --no-install-recommends xz-utils && \ RUN apt-get update && apt-get install -y --no-install-recommends xz-utils ca-certificates && \
curl -Ls https://github.com/upx/upx/releases/download/v${upx_version}/upx-${upx_version}-amd64_linux.tar.xz -o - | tar xvJf - -C /tmp && \ curl -Ls https://github.com/upx/upx/releases/download/v${upx_version}/upx-${upx_version}-amd64_linux.tar.xz -o - | tar xvJf - -C /tmp && \
cp /tmp/upx-${upx_version}-amd64_linux/upx /usr/local/bin/ && \ cp /tmp/upx-${upx_version}-amd64_linux/upx /usr/local/bin/ && \
chmod +x /usr/local/bin/upx && \ chmod +x /usr/local/bin/upx && \
@@ -20,13 +20,14 @@ COPY . .
# Build # Build
ENV GOCACHE=/root/.cache/go-build ENV GOCACHE=/root/.cache/go-build
RUN --mount=type=cache,target="/go" --mount=type=cache,target="/root/.cache/go-build" CGO_ENABLED=1 GOOS=linux go build -ldflags '-s -w' -o /flink-kube-operator ./cmd/operator RUN --mount=type=cache,target="/go" --mount=type=cache,target="/root/.cache/go-build" CGO_ENABLED=1 GOOS=linux go build -ldflags '-s -w' -o /flink-kube-operator ./cmd/operator \
RUN upx -q -5 /flink-kube-operator && upx -q -9 /flink-kube-operator
FROM public.ecr.aws/docker/library/busybox:1.37.0 AS final FROM public.ecr.aws/docker/library/busybox:1.37.0 AS final
COPY --from=build /flink-kube-operator /flink-kube-operator COPY --from=build /flink-kube-operator /flink-kube-operator
COPY --from=build /etc/ssl/certs /etc/ssl/certs
EXPOSE 8083 EXPOSE 8083

View File

@@ -1,27 +1,74 @@
FROM public.ecr.aws/docker/library/flink:1.20.0-scala_2.12-java17 ARG FLINK_VERSION=2.3.0
ARG SCALA_VERSION=2.12
ARG JAVA_VERSION=21
FROM flink:${FLINK_VERSION}-scala_${SCALA_VERSION}-java${JAVA_VERSION}
# Set working directory # Set working directory
WORKDIR /opt/flink WORKDIR /opt/flink
# Set environment variables for Flink mini-cluster # Set environment variables for Flink mini-cluster
ENV FLINK_HOME /opt/flink ENV FLINK_HOME=/opt/flink
ENV PATH=$FLINK_HOME/bin:$PATH ENV PATH=$FLINK_HOME/bin:$PATH
# Expose necessary ports for the Flink UI (JobManager) and job manager # Expose necessary ports for the Flink UI (JobManager) and job manager
EXPOSE 8081 6123 EXPOSE 8081 6123
COPY ./start-cluster.sh /opt/flink/bin/start-cluster.sh COPY ./start-cluster.sh /opt/flink/bin/start-cluster.sh
RUN chmod +x /opt/flink/bin/start-cluster.sh
RUN wget -q https://repo1.maven.org/maven2/org/apache/flink/flink-connector-kafka/3.4.0-1.20/flink-connector-kafka-3.4.0-1.20.jar -P /opt/flink/lib/ # ---- Flink-version-dependent connectors ----
# Kafka connector
ARG KAFKA_CONNECTOR_VERSION=5.0.0-2.2
RUN wget -q https://repo1.maven.org/maven2/org/apache/flink/flink-connector-kafka/${KAFKA_CONNECTOR_VERSION}/flink-connector-kafka-${KAFKA_CONNECTOR_VERSION}.jar -P /opt/flink/lib/
# Kafka clients (version-independent)
RUN wget -q https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/3.9.0/kafka-clients-3.9.0.jar -P /opt/flink/lib/ RUN wget -q https://repo1.maven.org/maven2/org/apache/kafka/kafka-clients/3.9.0/kafka-clients-3.9.0.jar -P /opt/flink/lib/
RUN wget -q https://repo1.maven.org/maven2/org/apache/flink/flink-avro/1.20.0/flink-avro-1.20.0.jar -P /opt/flink/lib/
RUN wget -q https://repo1.maven.org/maven2/org/apache/flink/flink-avro-confluent-registry/1.20.0/flink-avro-confluent-registry-1.20.0.jar -P /opt/flink/lib/
RUN wget -q https://repo1.maven.org/maven2/name/nkonev/flink/flink-sql-connector-clickhouse/1.17.1-8/flink-sql-connector-clickhouse-1.17.1-8.jar -P /opt/flink/lib/
RUN wget -q https://repo1.maven.org/maven2/org/apache/flink/flink-sql-connector-postgres-cdc/3.2.1/flink-sql-connector-postgres-cdc-3.2.1.jar -P /opt/flink/lib/
RUN wget -q https://repo1.maven.org/maven2/org/apache/avro/avro/1.8.2/avro-1.8.2.jar -P /opt/flink/lib/
RUN wget -q https://repo1.maven.org/maven2/net/objecthunter/exp4j/0.4.5/exp4j-0.4.5.jar -P /opt/flink/lib/
# Avro format
ARG AVRO_VERSION=2.2.1
RUN wget -q https://repo1.maven.org/maven2/org/apache/flink/flink-avro/${AVRO_VERSION}/flink-avro-${AVRO_VERSION}.jar -P /opt/flink/lib/
# Avro Confluent Registry
ARG AVRO_REGISTRY_VERSION=2.2.1
RUN wget -q https://repo1.maven.org/maven2/org/apache/flink/flink-avro-confluent-registry/${AVRO_REGISTRY_VERSION}/flink-avro-confluent-registry-${AVRO_REGISTRY_VERSION}.jar -P /opt/flink/lib/
# ---- Version-independent connectors ----
# ClickHouse SQL connector
RUN wget -q https://repo1.maven.org/maven2/name/nkonev/flink/flink-sql-connector-clickhouse/1.17.1-8/flink-sql-connector-clickhouse-1.17.1-8.jar -P /opt/flink/lib/
# PostgreSQL CDC
ARG PG_CDC_VERSION=3.6.0-2.2
RUN wget -q https://repo1.maven.org/maven2/org/apache/flink/flink-sql-connector-postgres-cdc/${PG_CDC_VERSION}/flink-sql-connector-postgres-cdc-${PG_CDC_VERSION}.jar -P /opt/flink/lib/
# Legacy Avro
RUN wget -q https://repo1.maven.org/maven2/org/apache/avro/avro/1.8.2/avro-1.8.2.jar -P /opt/flink/lib/
# misc libs
RUN wget -q https://repo1.maven.org/maven2/net/objecthunter/exp4j/0.4.5/exp4j-0.4.5.jar -P /opt/flink/lib/
RUN wget -q https://jdbc.postgresql.org/download/postgresql-42.7.4.jar -P /opt/flink/lib/
# JDBC driver
ARG JDBC_DRIVER_VERSION=2.2.1
RUN wget -q https://repo1.maven.org/maven2/org/apache/flink/flink-sql-jdbc-driver/${JDBC_DRIVER_VERSION}/flink-sql-jdbc-driver-${JDBC_DRIVER_VERSION}.jar -P /opt/flink/lib/
# Legacy Scala JDBC connector (no 2.x version exists)
RUN wget -q https://repo1.maven.org/maven2/org/apache/flink/flink-jdbc_2.12/1.10.3/flink-jdbc_2.12-1.10.3.jar -P /opt/flink/lib/
# JDBC connector (latest: 3.3.0-1.20, no 2.x version yet)
ARG JDBC_CONNECTOR_VERSION=3.3.0-1.20
RUN wget -q https://repo1.maven.org/maven2/org/apache/flink/flink-connector-jdbc/${JDBC_CONNECTOR_VERSION}/flink-connector-jdbc-${JDBC_CONNECTOR_VERSION}.jar -P /opt/flink/lib/
# misc libs
RUN wget -q https://repo1.maven.org/maven2/com/aventrix/jnanoid/jnanoid/2.0.0/jnanoid-2.0.0.jar -P /opt/flink/lib/
# S3 filesystem
ARG S3_FS_VERSION=2.2.1
RUN wget -q https://repo1.maven.org/maven2/org/apache/flink/flink-s3-fs-presto/${S3_FS_VERSION}/flink-s3-fs-presto-${S3_FS_VERSION}.jar -P /opt/flink/lib/
# Compression
RUN wget -q https://repo1.maven.org/maven2/com/github/luben/zstd-jni/1.5.7-2/zstd-jni-1.5.7-2.jar -P /opt/flink/lib/
# Command to start Flink JobManager and TaskManager in a mini-cluster setup # Command to start Flink JobManager and TaskManager in a mini-cluster setup
CMD ["bin/start-cluster.sh"] CMD ["bin/start-cluster.sh"]

199
README.md
View File

@@ -1,6 +1,203 @@
Installation: # Flink Kube Operator
A Kubernetes operator for managing Apache Flink jobs via Custom Resource Definitions.
## Installation
```bash ```bash
helm repo add lc-flink-operator https://git.logicamp.tech/Logicamp/flink-kube-operator/raw/branch/main/helm/ helm repo add lc-flink-operator https://git.logicamp.tech/Logicamp/flink-kube-operator/raw/branch/main/helm/
helm install flink-kube-operator lc-flink-operator/flink-kube-operator helm install flink-kube-operator lc-flink-operator/flink-kube-operator
``` ```
## FlinkJob CRD
Define Flink jobs as Kubernetes resources:
```yaml
apiVersion: flink.logicamp.dev/v1alpha1
kind: FlinkJob
metadata:
name: my-flink-job
spec:
key: word-count
name: "Word Count Example"
entryClass: "com.example.WordCount"
parallelism: 1
jarRef: "registry.example.com/myorg/word-count:1.0.0"
jarPullSecret: "registry-creds"
args:
kafka-host: kafka:9092
kafka-group-id: my-group
```
### Spec Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `key` | string | yes | Unique identifier for the job |
| `name` | string | yes | Human-readable job name |
| `entryClass` | string | yes | Flink job entry class |
| `parallelism` | int | yes | Job parallelism |
| `jarRef` | string | yes | OCI reference to the JAR artifact (e.g. `registry.io/org/app:tag`) |
| `jarPullSecret` | string | no | Name of a `kubernetes.io/dockerconfigjson` Secret for registry auth |
| `args` | map/list | no | Program arguments (map or list format) |
| `savepointInterval` | duration | no | Interval between automatic savepoints |
### Deprecated Fields (backward compat)
| Field | Replaced by |
|-------|-------------|
| `jarUri` | `jarRef` |
| `jarURIBasicAuthUsername` | `jarPullSecret` |
| `jarURIBasicAuthPassword` | `jarPullSecret` |
If both `jarRef` and `jarUri` are set, `jarRef` takes precedence.
## Registry Authentication
### Kubernetes (pull secret)
Create a `kubernetes.io/dockerconfigjson` Secret and reference it via `jarPullSecret`:
```bash
kubectl create secret docker-registry registry-creds \
--docker-server=registry.example.com \
--docker-username=user \
--docker-password=pass
```
```yaml
spec:
jarRef: "registry.example.com/myorg/my-job:1.0.0"
jarPullSecret: "registry-creds"
```
### Docker Compose / local dev (environment variables)
Set registry credentials as environment variables on the operator container:
```yaml
# docker-compose.yaml
services:
operator:
image: flink-kube-operator:latest
environment:
REGISTRY_SERVER: "registry.example.com"
REGISTRY_USERNAME: "user"
REGISTRY_PASSWORD: "pass"
```
### Credential Resolution Order
1. `spec.jarPullSecret` — reads the K8s Secret
2. `REGISTRY_USERNAME` + `REGISTRY_PASSWORD` env vars — fallback (optionally scoped by `REGISTRY_SERVER`)
3. Anonymous — public registries
## Pushing JARs to an OCI Registry
Flink job JARs are stored as OCI artifacts with media type `application/java-archive`. Use one of the methods below to push your JAR.
### Using crane (recommended)
[crane](https://github.com/google/go-containerregistry/tree/main/cmd/crane) is a tool for interacting with OCI registries.
```bash
# Install crane
go install github.com/google/go-containerregistry/cmd/crane@latest
# Log in to your registry
crane auth login registry.example.com -u user -p pass
# Push a JAR as an OCI artifact
crane append \
--file=my-job.jar \
--new_tag=registry.example.com/myorg/my-job:1.0.0 \
--base=scratch
```
### Using oras
[oras](https://oras.land/) is a tool for pushing OCI artifacts.
```bash
# Install oras
# https://oras.land/docs/installation
# Log in to your registry
oras login registry.example.com -u user -p pass
# Push a JAR with the correct media type
oras push registry.example.com/myorg/my-job:1.0.0 \
--artifact-type application/java-archive \
my-job.jar:application/java-archive
```
### Using Docker (buildkit)
```bash
# Create a minimal Dockerfile
cat > Dockerfile.jar <<'EOF'
FROM scratch
COPY my-job.jar /my-job.jar
EOF
# Build and push
docker build -f Dockerfile.jar -t registry.example.com/myorg/my-job:1.0.0 .
docker push registry.example.com/myorg/my-job:1.0.0
```
### GitHub Actions (CI/CD)
Use [crane](https://github.com/google/go-containerregistry/tree/main/cmd/crane) in your GitHub Actions workflow to push JARs as OCI artifacts. `docker/build-push-action` is not suitable — it builds container images, not arbitrary OCI artifacts.
```yaml
name: Publish JAR
on:
push:
tags:
- 'v*'
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}/my-job
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up crane
uses: imjasonh/setup-crane@v0.4
- name: Build JAR
run: mvn package -DskipTests
- name: Push JAR to OCI registry
run: |
crane append \
--base=scratch \
--new_tag=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} \
--new_layer=target/my-job.jar \
--media_type=application/java-archive
```
For GitLab CI, Bitbucket Pipelines, or other CI systems, use the same `crane append` command after authenticating to your registry.
### Versioning Strategy
- Use semantic version tags (`:1.0.0`, `:1.1.0`) for explicit version control
- Changing the tag in `spec.jarRef` triggers a job upgrade (pause → re-upload → resume from savepoint)
- The operator stores the SHA256 digest in `status.runningJarDigest` after each pull

View File

@@ -2,7 +2,8 @@ package main
import ( import (
"flink-kube-operator/internal/crd" "flink-kube-operator/internal/crd"
"flink-kube-operator/internal/manager" "flink-kube-operator/internal/managed_job"
"flink-kube-operator/internal/rest"
"flink-kube-operator/pkg" "flink-kube-operator/pkg"
"log" "log"
"os" "os"
@@ -30,7 +31,7 @@ func main() {
pkg.Logger.Info("[main]", zap.Any("cluster-config", clusterConfig)) pkg.Logger.Info("[main]", zap.Any("cluster-config", clusterConfig))
// init flink job manager // init flink job manager
manager.NewManager(c, crdInstance) managed_job.NewManager(c, crdInstance)
// for _, jobDef := range config.Jobs { // for _, jobDef := range config.Jobs {
// managed_job.NewManagedJob(c, db, jobDef) // managed_job.NewManagedJob(c, db, jobDef)
@@ -48,6 +49,8 @@ func main() {
// // pkg.Logger.Debug("[main]", zap.Any("job", job)) // // pkg.Logger.Debug("[main]", zap.Any("job", job))
// } // }
go rest.Init()
cancelChan := make(chan os.Signal, 1) cancelChan := make(chan os.Signal, 1)
sig := <-cancelChan sig := <-cancelChan
log.Printf("Caught signal %v", sig) log.Printf("Caught signal %v", sig)

View File

@@ -2,9 +2,9 @@
apiVersion: apiextensions.k8s.io/v1 apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition kind: CustomResourceDefinition
metadata: metadata:
name: flink-jobs.flink.logicamp.tech name: flink-jobs.flink.logicamp.dev
spec: spec:
group: flink.logicamp.tech group: flink.logicamp.dev
names: names:
kind: FlinkJob kind: FlinkJob
plural: flink-jobs plural: flink-jobs
@@ -24,22 +24,36 @@ spec:
type: object type: object
required: required:
- key - key
- jarUri
properties: properties:
key: key:
type: string type: string
name: name:
type: string type: string
flinkCluster:
type: string
description: "Identifier of the Flink cluster that manages this job. Defaults to the OPERATOR_ID (usually the Helm release name) if not set."
entryClass: entryClass:
type: string type: string
parallelism: parallelism:
type: integer type: integer
jarRef:
type: string
description: "OCI reference to the JAR artifact (e.g. registry.io/org/app:tag). Takes precedence over jarUri."
jarPullSecret:
type: string
description: "Name of a kubernetes.io/dockerconfigjson Secret for registry authentication."
jarUri: jarUri:
type: string type: string
args: description: "Deprecated. Use jarRef instead. HTTP(S) URL to the JAR file."
type: array jarURIBasicAuthUsername:
items:
type: string type: string
description: "Deprecated. Use jarPullSecret instead."
jarURIBasicAuthPassword:
type: string
description: "Deprecated. Use jarPullSecret instead."
args:
description: "Program arguments. Accepts a list of strings (--key, value, ...) or a map of key-value pairs."
x-kubernetes-preserve-unknown-fields: true
savepointInterval: savepointInterval:
type: string type: string
format: duration format: duration
@@ -92,8 +106,15 @@ spec:
lastRestoredSavepointRestoredDate: lastRestoredSavepointRestoredDate:
type: string type: string
format: time format: time
runningJarDigest:
type: string
description: "SHA256 digest of the currently running JAR layer."
runningJarRef:
type: string
description: "OCI reference of the currently running JAR."
runningJarURI: runningJarURI:
type: string type: string
description: "Deprecated. HTTP URI of the currently running JAR."
pauseSavepointTriggerId: pauseSavepointTriggerId:
type: string type: string
restoredCount: restoredCount:

View File

@@ -1,15 +1,26 @@
# flink-job-instance.yaml # flink-job-instance.yaml
apiVersion: flink.logicamp.tech/v1alpha1 apiVersion: flink.logicamp.dev/v1alpha1
kind: FlinkJob kind: FlinkJob
metadata: metadata:
name: my-flink-job name: my-flink-job
namespace: default
spec: spec:
key: word-count key: word-count
name: "Word Count Example" name: "Word Count Example"
entryClass: "org.apache.flink.examples.java.wordcount.WordCount" entryClass: "tech.logicamp.logiline.FacilityEnrichment"
parallelism: 2 parallelism: 1
jarUri: "http://192.168.7.7:8080/product-enrichment-processor.jar" jarRef: "lcr.logicamp.tech/logiline/facility-enrichment:1.0.0"
jarPullSecret: "lcr-registry-creds"
# args accepts two formats:
# Map (recommended):
args:
kafka-host: kafka.pg-release:9092
kafka-group-id: batch-sales-config-dedup-processor
# List (also valid):
# args:
# - "--kafka-host"
# - kafka.pg-release:9092
# - "--kafka-group-id"
# - batch-sales-config-dedup-processor
flinkConfiguration: flinkConfiguration:
taskmanager.numberOfTaskSlots: "2" taskmanager.numberOfTaskSlots: "1"
parallelism.default: "2" parallelism.default: "1"

38
go.mod
View File

@@ -3,8 +3,10 @@ module flink-kube-operator
go 1.23.2 go 1.23.2
require ( require (
github.com/logi-camp/go-flink-client v0.2.0 github.com/danielgtaylor/huma/v2 v2.27.0
github.com/matoous/go-nanoid/v2 v2.1.0 github.com/gofiber/fiber/v2 v2.52.6
github.com/google/go-containerregistry v0.20.3
github.com/logi-camp/go-flink-client v0.2.2
github.com/samber/lo v1.47.0 github.com/samber/lo v1.47.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
k8s.io/apimachinery v0.31.3 k8s.io/apimachinery v0.31.3
@@ -12,6 +14,11 @@ require (
) )
require ( require (
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect
github.com/docker/cli v27.5.0+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.8.2 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch/v5 v5.9.0 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect
@@ -21,16 +28,26 @@ require (
github.com/google/gnostic-models v0.6.9 // indirect github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.58.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/vbatts/tar-split v0.11.6 // indirect
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
google.golang.org/protobuf v1.35.1 // indirect golang.org/x/sync v0.10.0 // indirect
google.golang.org/protobuf v1.36.3 // indirect
) )
require ( require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
@@ -43,24 +60,19 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/reactivex/rxgo/v2 v2.5.0
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/teivah/onecontext v0.0.0-20200513185103-40f981bfd775 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.31.0 // indirect golang.org/x/net v0.31.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/oauth2 v0.25.0 // indirect
golang.org/x/sys v0.27.0 // indirect golang.org/x/sys v0.29.0 // indirect
golang.org/x/term v0.26.0 // indirect golang.org/x/term v0.26.0 // indirect
golang.org/x/text v0.20.0 // indirect golang.org/x/text v0.20.0 // indirect
golang.org/x/time v0.6.0 // indirect golang.org/x/time v0.6.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.31.3 // indirect k8s.io/api v0.31.3
k8s.io/klog/v2 v2.130.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20241127205056-99599406b04f // indirect k8s.io/kube-openapi v0.0.0-20241127205056-99599406b04f // indirect
k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078 // indirect k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078 // indirect

82
go.sum
View File

@@ -1,15 +1,22 @@
github.com/cenkalti/backoff/v4 v4.0.0/go.mod h1:eEew/i+1Q6OrCDZh3WiXYv3+nJwBASZ8Bog/87DQnVg= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8=
github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/danielgtaylor/huma/v2 v2.27.0 h1:yxgJ8GqYqKeXw/EnQ4ZNc2NBpmn49AlhxL2+ksSXjUI=
github.com/danielgtaylor/huma/v2 v2.27.0/go.mod h1:NbSFXRoOMh3BVmiLJQ9EbUpnPas7D9BeOxF/pZBAGa0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM=
github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo=
github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
@@ -28,6 +35,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -37,6 +46,8 @@ github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcb
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.20.3 h1:oNx7IdTI936V8CQRveCjaxOiegWwvM7kqkbXTpyiovI=
github.com/google/go-containerregistry v0.20.3/go.mod h1:w00pIgBRDVUDFM6bq+Qx8lwNWK+cxgCuX1vd3PIBDNI=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -52,7 +63,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -60,17 +72,19 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/logi-camp/go-flink-client v0.2.0 h1:PIyfJq7FjW28bnvemReCicIuQD7JzVgJDk2xPTZUS2s= github.com/logi-camp/go-flink-client v0.2.2 h1:aWqibzcJiv02Myf/WowZafI0hhHp5x7BJGRCmI6PT20=
github.com/logi-camp/go-flink-client v0.2.0/go.mod h1:A79abedX6wGQI0FoICdZI7SRoGHj15QwMwWowgsKYFI= github.com/logi-camp/go-flink-client v0.2.2/go.mod h1:A79abedX6wGQI0FoICdZI7SRoGHj15QwMwWowgsKYFI=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE=
github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -82,38 +96,50 @@ github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA
github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/reactivex/rxgo/v2 v2.5.0 h1:FhPgHwX9vKdNQB2gq9EPt+EKk9QrrzoeztGbEEnZam4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/reactivex/rxgo/v2 v2.5.0/go.mod h1:bs4fVZxcb5ZckLIOeIeVH942yunJLWDABWGbrHAW+qU= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/teivah/onecontext v0.0.0-20200513185103-40f981bfd775 h1:BLNsFR8l/hj/oGjnJXkd4Vi3s4kQD3/3x8HSAE4bzN0= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/teivah/onecontext v0.0.0-20200513185103-40f981bfd775/go.mod h1:XUZ4x3oGhWfiOnUvTslnKKs39AWUct3g3yJvXTQSJOQ= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE=
github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs=
github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -125,10 +151,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -137,18 +161,21 @@ golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -158,21 +185,21 @@ golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
@@ -181,7 +208,6 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

6
helm/chart/Chart.lock Normal file
View File

@@ -0,0 +1,6 @@
dependencies:
- name: minio
repository: https://charts.bitnami.com/bitnami
version: 16.0.2
digest: sha256:9a822e9c5a4eee1b6515c143150c1dd6f84ceb080a7be4573e09396c5916f7d3
generated: "2025-04-04T14:42:09.771390014+03:30"

View File

@@ -2,5 +2,5 @@ apiVersion: v2
name: flink-kube-operator name: flink-kube-operator
description: Helm chart for flink kube operator description: Helm chart for flink kube operator
type: application type: application
version: 0.1.1 version: 1.3.0
appVersion: "0.1.0" appVersion: "0.1.1"

View File

@@ -17,6 +17,6 @@
{{- else if contains "ClusterIP" .Values.service.type }} {{- else if contains "ClusterIP" .Values.service.type }}
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "flink-kube-operator.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "flink-kube-operator.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}")
echo "Visit http://127.0.0.1:8080 to use your application" echo "Visit http://127.0.0.1:8081 to use your application"
kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8081:$CONTAINER_PORT
{{- end }} {{- end }}

View File

@@ -0,0 +1,12 @@
{{- if eq .Values.flink.state.checkpoint.storageType "filesystem" }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ .Release.Name }}-flink-checkpoint-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: {{ .Values.flink.state.checkpoint.size }}
{{- end }}

View File

@@ -0,0 +1,51 @@
{{- define "flink.env" -}}
- name: JOB_MANAGER_RPC_ADDRESS
value: "localhost"
- name: NAMESPACE
value: {{ .Release.Namespace }}
- name: FLINK_PROPERTIES
value: |
jobmanager.rpc.address: {{ .Release.Name }}-flink-job-manager
jobmanager.memory.process.size: {{ .Values.flink.jobManager.processMemory }}
taskmanager.memory.process.size: {{ .Values.flink.taskManager.processMemory }}
taskmanager.data.port: 6125
taskmanager.numberOfTaskSlots: {{ .Values.flink.taskManager.numberOfTaskSlots }}
parallelism.default: {{ .Values.flink.parallelism.default }}
{{- if semverCompare ">=2.0.0" .Values.flink.version }}
state.backend.type: {{ .Values.flink.state.backend }}
{{- else }}
state.backend: {{ .Values.flink.state.backend }}
{{- end }}
rest.port: 8081
rootLogger.level = DEBUG
rootLogger.appenderRef.console.ref = ConsoleAppender
high-availability.type: kubernetes
kubernetes.namespace: {{ .Release.Namespace }}
kubernetes.cluster-id: {{ .Values.clusterId | default (print .Release.Name "-cluster") }}
execution.checkpointing.interval: {{ .Values.flink.state.checkpoint.interval }}
execution.checkpointing.mode: {{ .Values.flink.state.checkpoint.mode }}
{{- if eq .Values.flink.state.checkpoint.storageType "filesystem" }}
state.checkpoints.dir: file:///opt/flink/checkpoints/
{{- else if eq .Values.flink.state.checkpoint.storageType "s3" }}
state.checkpoints.dir: s3://{{ .Release.Name }}-flink/checkpoints/
{{- end }}
{{- if semverCompare ">=2.0.0" .Values.flink.version }}
state.backend.rocksdb.local-dir: /opt/flink/rocksdb
{{- else }}
state.backend.rocksdb.localdir: /opt/flink/rocksdb
{{- end }}
high-availability.storageDir: /opt/flink/ha
{{- if eq .Values.flink.state.savepoint.storageType "filesystem" }}
state.savepoints.dir: file:///opt/flink/savepoints/
{{- else if eq .Values.flink.state.savepoint.storageType "s3" }}
state.savepoints.dir: s3://{{ .Release.Name }}-flink/savepoints/
{{- end }}
state.backend.incremental: {{ .Values.flink.state.incremental }}
rest.profiling.enabled: true
{{- if or (eq .Values.flink.state.checkpoint.storageType "s3") (eq .Values.flink.state.savepoint.storageType "s3") }}
s3.endpoint: http://{{ .Release.Name }}-minio:9000
s3.path.style.access: true
{{- end }}
{{- toYaml .Values.flink.properties | default "" | nindent 4 }}
{{- end }}

View File

@@ -1,67 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-flink
labels:
app.kubernetes.io/name: {{ .Release.Name }}-flink
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: {{ .Release.Name }}-flink
app.kubernetes.io/instance: {{ .Release.Name }}
template:
metadata:
labels:
app.kubernetes.io/name: {{ .Release.Name }}-flink
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
serviceAccountName: {{ include "flink-kube-operator.serviceAccountName" . }}
containers:
- name: flink
image: {{ .Values.flink.image.repository }}:{{ .Values.flink.image.tag }}
imagePullPolicy: Always
ports:
- containerPort: 8081 # JobManager Web UI port
- containerPort: 6121 # TaskManager communication port
- containerPort: 6122 # TaskManager communication port
env:
- name: JOB_MANAGER_RPC_ADDRESS
value: "localhost" # JobManager and TaskManager in the same container
- name: FLINK_PROPERTIES
value: |
jobmanager.rpc.address: localhost
jobmanager.memory.process.size: 2048m
taskmanager.memory.process.size: 2048m
taskmanager.data.port: 6125
taskmanager.numberOfTaskSlots: {{ .Values.flink.taskManager.numberOfTaskSlots }}
parallelism.default: {{ .Values.flink.parallelism.default }}
state.backend: {{ .Values.flink.state.backend }}
state.savepoints.dir: {{ .Values.flink.state.savepoints.dir }}
rest.port: 8081
rootLogger.level = DEBUG
rootLogger.appenderRef.console.ref = ConsoleAppender
web.upload.dir: /opt/flink/data/web-upload
state.checkpoints.dir: file:///tmp/flink-checkpoints
high-availability.type: kubernetes
high-availability.storageDir: file:///opt/flink/ha
kubernetes.cluster-id: cluster-one
kubernetes.namespace: {{ .Release.Namespace }}
volumeMounts:
- name: flink-data
mountPath: /opt/flink/data
subPath: data
- name: flink-savepoints
mountPath: /opt/flink/savepoints
- name: flink-savepoints
mountPath: /opt/flink/ha
subPath: ha
volumes:
- name: flink-data
emptyDir: {} # Temporary storage for internal data
- name: flink-savepoints
persistentVolumeClaim:
claimName: {{ .Values.flink.state.savepoints.pvcName }} # PVC for savepoints persistence

View File

@@ -0,0 +1,10 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ .Release.Name }}-flink-ha-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .Values.flink.state.ha.size }} # Use size defined in values.yaml

View File

@@ -0,0 +1,103 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-flink-job-manager
labels:
app: {{ .Release.Name }}-flink-operator
component: {{ .Release.Name }}-flink-job-manager
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: {{ .Release.Name }}-flink-operator
component: {{ .Release.Name }}-flink-job-manager
template:
metadata:
labels:
app: {{ .Release.Name }}-flink-operator
component: {{ .Release.Name }}-flink-job-manager
spec:
serviceAccountName: {{ include "flink-kube-operator.serviceAccountName" . }}
initContainers:
- name: volume-mount-hack
image: {{ .Values.flink.image.repository }}:{{ .Values.flink.image.tag }}
securityContext:
runAsUser: 0
command: ["sh", "-c", "chown -R flink {{ .Values.flink.state.ha.dir }}"]
volumeMounts:
- name: flink-ha
mountPath: {{ .Values.flink.state.ha.dir }}
containers:
- name: jobmanager
image: {{ .Values.flink.image.repository }}:{{ .Values.flink.image.tag }}
imagePullPolicy: Always
args: ["jobmanager"]
ports:
- containerPort: 6123 # JobManager RPC port
name: rpc
- containerPort: 6124 # JobManager blob server port
name: blob
- containerPort: 6125 # JobManager queryable state port
name: query
- containerPort: 8081 # JobManager Web UI port
name: ui
env:
{{- include "flink.env" . | nindent 12 }}
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
{{- if or (eq .Values.flink.state.checkpoint.storageType "s3") (eq .Values.flink.state.savepoint.storageType "s3") }}
- name: S3_ENDPOINT
value: "http://{{ .Release.Name }}-minio:9000"
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-minio
key: root-user
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-minio
key: root-password
{{- end }}
volumeMounts:
- name: flink-ha
mountPath: {{ .Values.flink.state.ha.dir }}
{{- if eq .Values.flink.state.checkpoint.storageType "filesystem" }}
- name: flink-checkpoint
mountPath: /opt/flink/checkpoints
{{- end }}
{{- if eq .Values.flink.state.savepoint.storageType "filesystem" }}
- name: flink-savepoint
mountPath: /opt/flink/savepoints
{{- end }}
volumes:
- name: flink-ha
persistentVolumeClaim:
claimName: {{ .Release.Name }}-flink-ha-pvc
{{- if eq .Values.flink.state.checkpoint.storageType "filesystem" }}
- name: flink-checkpoint
persistentVolumeClaim:
claimName: {{ .Release.Name }}-flink-checkpoint-pvc
{{- end }}
{{- if eq .Values.flink.state.savepoint.storageType "filesystem" }}
- name: flink-savepoint
persistentVolumeClaim:
claimName: {{ .Release.Name }}-flink-savepoint-pvc
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -0,0 +1,28 @@
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-flink-job-manager
labels:
app.kubernetes.io/name: {{ .Release.Name }}-flink-job-manager
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
ports:
- name: flink-web-ui
port: 8081
targetPort: 8081
- name: rpc
port: 6123
targetPort: 6123
- name: blob
port: 6124
targetPort: 6124
- name: query
port: 6125
targetPort: 6125
- name: operator
port: 3000
targetPort: 3000
selector:
app: {{ .Release.Name }}-flink-operator
component: {{ .Release.Name }}-flink-job-manager
type: ClusterIP # Change to LoadBalancer if you want external access

View File

@@ -1,10 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ .Values.flink.state.savepoints.pvcName }}
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .Values.flink.persistence.size }} # Use size defined in values.yaml

View File

@@ -0,0 +1,12 @@
{{- if eq .Values.flink.state.savepoint.storageType "filesystem" }}
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ .Release.Name }}-flink-savepoint-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: {{ .Values.flink.state.savepoint.size }}
{{- end }}

View File

@@ -1,15 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: flink
labels:
app.kubernetes.io/name: {{ .Release.Name }}-flink
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
ports:
- port: 8081
targetPort: 8081
selector:
app.kubernetes.io/name: {{ .Release.Name }}-flink
app.kubernetes.io/instance: {{ .Release.Name }}
type: ClusterIP # Change to LoadBalancer if you want external access

View File

@@ -0,0 +1,80 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ .Release.Name }}-flink-task-manager
labels:
app: {{ .Release.Name }}-flink-operator
component: taskmanager
spec:
serviceName: {{ .Release.Name }}-flink-task-manager
replicas: {{ .Values.flink.taskManager.replicas }}
selector:
matchLabels:
app: {{ .Release.Name }}-flink-operator
component: {{ .Release.Name }}-flink-task-manager
template:
metadata:
labels:
app: {{ .Release.Name }}-flink-operator
component: {{ .Release.Name }}-flink-task-manager
spec:
serviceAccountName: {{ include "flink-kube-operator.serviceAccountName" . }}
containers:
- name: task-manager
image: {{ .Values.flink.image.repository }}:{{ .Values.flink.image.tag }}
imagePullPolicy: Always
args: ["taskmanager"]
env:
{{- include "flink.env" . | nindent 8 }}
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
{{- if or (eq .Values.flink.state.checkpoint.storageType "s3") (eq .Values.flink.state.savepoint.storageType "s3") }}
- name: S3_ENDPOINT
value: "http://{{ .Release.Name }}-minio:9000"
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-minio
key: root-user
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-minio
key: root-password
{{- end }}
volumeMounts:
- name: rocksdb-storage
mountPath: /opt/flink/rocksdb
{{- if eq .Values.flink.state.checkpoint.storageType "filesystem" }}
- name: flink-checkpoint
mountPath: /opt/flink/checkpoints
{{- end }}
{{- if eq .Values.flink.state.savepoint.storageType "filesystem" }}
- name: flink-savepoint
mountPath: /opt/flink/savepoints
{{- end }}
resources:
{{- toYaml .Values.flink.taskManager.resources | nindent 10 }}
volumes:
{{- if eq .Values.flink.state.checkpoint.storageType "filesystem" }}
- name: flink-checkpoint
persistentVolumeClaim:
claimName: {{ .Release.Name }}-flink-checkpoint-pvc
{{- end }}
{{- if eq .Values.flink.state.savepoint.storageType "filesystem" }}
- name: flink-savepoint
persistentVolumeClaim:
claimName: {{ .Release.Name }}-flink-savepoint-pvc
{{- end }}
volumeClaimTemplates:
- metadata:
name: rocksdb-storage
spec:
accessModes: [ ReadWriteOnce ]
resources:
requests:
storage: {{ .Values.flink.taskManager.storage.rocksDb.size }}

View File

@@ -1,61 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "flink-kube-operator.fullname" . }}
labels:
{{- include "flink-kube-operator.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "flink-kube-operator.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "flink-kube-operator.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "flink-kube-operator.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
env:
- name: FLINK_API_URL
value: {{ .Values.config.flinkApiUrl }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}

View File

@@ -8,8 +8,8 @@ metadata:
spec: spec:
scaleTargetRef: scaleTargetRef:
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: StatefulSet
name: {{ include "flink-kube-operator.fullname" . }} name: {{ .Release.Name }}-flink-operator
minReplicas: {{ .Values.autoscaling.minReplicas }} minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics: metrics:

View File

@@ -2,12 +2,11 @@ apiVersion: rbac.authorization.k8s.io/v1
kind: Role kind: Role
metadata: metadata:
name: {{ include "flink-kube-operator.serviceAccountName" . }} name: {{ include "flink-kube-operator.serviceAccountName" . }}
namespace: {{ .Release.Namespace }} # Namespace where the role is created
labels: labels:
{{- include "flink-kube-operator.labels" . | nindent 4 }} {{- include "flink-kube-operator.labels" . | nindent 4 }}
rules: rules:
- apiGroups: - apiGroups:
- flink.logicamp.tech # API group of the FlinkJob CRD - flink.logicamp.dev # API group of the FlinkJob CRD
resources: resources:
- flink-jobs # The plural name of your custom resource - flink-jobs # The plural name of your custom resource
verbs: verbs:

View File

@@ -1,9 +1,10 @@
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: {{ include "flink-kube-operator.fullname" . }} name: {{ .Release.Name }}-flink-operator
labels: labels:
{{- include "flink-kube-operator.labels" . | nindent 4 }} app: {{ .Release.Name }}-flink-operator
component: {{ .Release.Name }}-flink-operator
spec: spec:
type: {{ .Values.service.type }} type: {{ .Values.service.type }}
ports: ports:
@@ -12,4 +13,5 @@ spec:
protocol: TCP protocol: TCP
name: http name: http
selector: selector:
{{- include "flink-kube-operator.selectorLabels" . | nindent 4 }} app: {{ .Release.Name }}-flink-operator
component: {{ .Release.Name }}-flink-operator

View File

@@ -0,0 +1,72 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ .Release.Name }}-flink-operator
labels:
app: {{ .Release.Name }}-flink-operator
component: {{ .Release.Name }}-flink-operator
spec:
serviceName: {{ .Release.Name }}-flink-operator
replicas: 1
selector:
matchLabels:
app: {{ .Release.Name }}-flink-operator
component: {{ .Release.Name }}-flink-operator
template:
metadata:
labels:
app: {{ .Release.Name }}-flink-operator
component: {{ .Release.Name }}-flink-operator
spec:
serviceAccountName: {{ include "flink-kube-operator.serviceAccountName" . }}
initContainers:
- name: wait-for-jobmanager
image: curlimages/curl:8.5.0 # Lightweight curl image
command:
- sh
- -c
- |
echo "Waiting for Flink JobManager to be ready..."
until curl -sSf "http://{{ .Release.Name }}-flink-job-manager:8081/taskmanagers"; do
echo "JobManager not ready yet - retrying in 5s..."
sleep 5
done
echo "JobManager is ready!"
containers:
- name: operator
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
env:
- name: OPERATOR_ID
value: {{ .Values.operatorId | default .Release.Name }}
- name: FLINK_API_URL
value: {{ .Release.Name }}-flink-job-manager:8081
- name: NAMESPACE
value: "{{ .Release.Namespace }}"
{{- if eq .Values.flink.state.savepoint.storageType "s3" }}
- name: SAVEPOINT_PATH
value: s3://{{ .Release.Name }}-flink/savepoints/
- name: S3_ENDPOINT
value: "http://{{ .Release.Name }}-minio:9000"
- name: AWS_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-minio
key: root-user
- name: AWS_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-minio
key: root-password
{{- else }}
- name: SAVEPOINT_PATH
value: /opt/flink/savepoints/
{{- end }}

View File

@@ -7,11 +7,11 @@ replicaCount: 1
# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ # This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/
image: image:
repository: lcr.logicamp.tech/library/flink-kube-operator repository: logicampdev/flink-kube-operator
# This sets the pull policy for images. # This sets the pull policy for images.
pullPolicy: Always pullPolicy: Always
# Overrides the image tag whose default is the chart appVersion. # Overrides the image tag whose default is the chart appVersion.
tag: latest tag: v1.3.1
# This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ # This is for the secretes for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
imagePullSecrets: [] imagePullSecrets: []
@@ -38,8 +38,7 @@ podAnnotations: {}
# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ # For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
podLabels: {} podLabels: {}
podSecurityContext: {} podSecurityContext: {} # fsGroup: 2000
# fsGroup: 2000
securityContext: {} securityContext: {}
# capabilities: # capabilities:
@@ -54,7 +53,7 @@ service:
# This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types
type: ClusterIP type: ClusterIP
# This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports
port: 80 port: 3000
# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ # This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/
ingress: ingress:
@@ -106,6 +105,10 @@ autoscaling:
config: config:
flinkApiUrl: flink:8081 flinkApiUrl: flink:8081
# Identifier for this operator instance. Used to partition FlinkJob CRs via
# label selector. Defaults to the Helm release name if left empty.
# Set explicitly when you need stable names across upgrades.
operatorId: ""
nodeSelector: {} nodeSelector: {}
@@ -115,24 +118,50 @@ affinity: {}
# Global values for the Flink deployment # Global values for the Flink deployment
flink: flink:
# Flink major.minor version (semver string). Used by config template to emit
# the correct configuration key names for the target Flink version.
# Override to "1.20" for backward compatibility with Flink 1.20.x.
version: "2.3"
image: image:
repository: lcr.logicamp.tech/library/flink repository: logicampdev/flink-kube-operator-flink
tag: 1.20.0-scala_2.12-java17-minicluster tag: 2.3.0-java21-v1.3.0
parallelism: parallelism:
default: 1 # Default parallelism for Flink jobs default: 1 # Default parallelism for Flink jobs
state: state:
backend: rocksdb # Use RocksDB for state backend backend: rocksdb # Use RocksDB for state backend
savepoints: incremental: true
dir: "file:///opt/flink/savepoints" # Directory to store savepoints ha:
pvcName: flink-savepoints-pvc # PVC for savepoints persistence dir: "/opt/flink/ha" # Directory to store ha data
pvcName: flink-ha-pvc # PVC for ha
size: 10Gi # PVC size for ha
checkpoint:
storageType: s3 # s3 / filesystem
interval: 5min
mode: EXACTLY_ONCE
size: 8Gi
savepoint:
storageType: s3
size: 8Gi
jobManager:
processMemory: 4096m # Size of job manager process memory
properties:
jobmanager.rpc.timeout: 300s
taskManager: taskManager:
numberOfTaskSlots: 100 # Number of task slots for TaskManager numberOfTaskSlots: 12 # Number of task slots for task manager
processMemory: 4096m # Size of task manager process memory
persistence: replicas: 1 # Number of task manager replicas
enabled: true storage:
size: 10Gi # PVC size for savepoints storage rocksDb:
accessModes: size: 4Gi
- ReadWriteOnce resources:
limits:
cpu: 3
memory: 4Gi
requests:
cpu: 1
memory: 2Gi

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,9 +1,225 @@
apiVersion: v1 apiVersion: v1
entries: entries:
flink-kube-operator: flink-kube-operator:
- apiVersion: v2
appVersion: 0.1.1
created: "2025-07-18T18:09:46.27166563+03:30"
description: Helm chart for flink kube operator
digest: 597f2c07884bb5411dcc6e1a9cdf7672977858efe30273a46fb6525eb6013091
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-1.2.3.tgz
version: 1.2.3
- apiVersion: v2
appVersion: 0.1.1
created: "2025-05-17T14:34:55.317942453+03:30"
description: Helm chart for flink kube operator
digest: 422a34dc173ebe29adccd46d7ef94505cc022ff20ccbfb85ac3e6e201cba476c
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-1.2.2.tgz
version: 1.2.2
- apiVersion: v2
appVersion: 0.1.1
created: "2025-05-17T14:01:29.891695937+03:30"
description: Helm chart for flink kube operator
digest: 404ed2c28ff43b630b44c1215be5369417a1b9b2747ae24e2963a6b81813e7dc
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-1.2.1.tgz
version: 1.2.1
- apiVersion: v2
appVersion: 0.1.1
created: "2025-05-17T12:47:25.848097207+03:30"
dependencies:
- name: minio
repository: https://charts.bitnami.com/bitnami
version: 16.0.2
description: Helm chart for flink kube operator
digest: 3458b9be97d2a4bcf8574706e44ea9f7fdeb11e83058a615566e6e094a51b920
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-1.2.0.tgz
version: 1.2.0
- apiVersion: v2
appVersion: 0.1.1
created: "2025-04-15T12:06:59.425538953+03:30"
dependencies:
- name: minio
repository: https://charts.bitnami.com/bitnami
version: 16.0.2
description: Helm chart for flink kube operator
digest: 2b307a113476eebb34f58308bf1d4d0d36ca5e08fe6541f369a1c231ae0a71be
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-1.1.2.tgz
version: 1.1.2
- apiVersion: v2
appVersion: 0.1.1
created: "2025-04-12T23:13:39.394371646+03:30"
dependencies:
- name: minio
repository: https://charts.bitnami.com/bitnami
version: 16.0.2
description: Helm chart for flink kube operator
digest: 14b08b443b4118cee4c279f62b498bc040b4a3e7ebafa8e195606e3d9b21810a
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-1.0.1.tgz
version: 1.0.1
- apiVersion: v2
appVersion: 0.1.1
created: "2025-04-06T01:52:09.478716316+03:30"
dependencies:
- name: minio
repository: https://charts.bitnami.com/bitnami
version: 16.0.2
description: Helm chart for flink kube operator
digest: e177bc2f11987f4add27c09e521476eabaa456df1b9621321200b58f3a330813
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-1.0.0.tgz
version: 1.0.0
- apiVersion: v2 - apiVersion: v2
appVersion: 0.1.0 appVersion: 0.1.0
created: "2024-12-19T00:39:44.4857163+03:30" created: "2025-04-04T13:50:27.971040367+03:30"
description: Helm chart for flink kube operator
digest: 00acef7878bcf372d036fabaac400913097d678087a756102b54a28428197bdf
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-0.1.14.tgz
version: 0.1.14
- apiVersion: v2
appVersion: 0.1.0
created: "2025-03-04T23:13:19.698003661+03:30"
description: Helm chart for flink kube operator
digest: d104b9242362415a7b920e4e2af975730e208ff73db17b8d2afd11ea8b78b4a2
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-0.1.13.tgz
version: 0.1.13
- apiVersion: v2
appVersion: 0.1.0
created: "2025-03-04T23:13:19.697555829+03:30"
description: Helm chart for flink kube operator
digest: f58802990389ecde00a49a442f6e83a007e281e972d07f2979657d2763fe94ba
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-0.1.12.tgz
version: 0.1.12
- apiVersion: v2
appVersion: 0.1.0
created: "2025-03-04T18:04:35.491747333+03:30"
description: Helm chart for flink kube operator
digest: 0daa98c63b443018c2072a2d7448c972faff2274fb04433c613532b408cd3ab1
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-0.1.11.tgz
version: 0.1.11
- apiVersion: v2
appVersion: 0.1.0
created: "2025-03-04T18:04:35.490697387+03:30"
description: Helm chart for flink kube operator
digest: e091256eeb8640b61443cbe4781426ef493737ab0ac1145e568426bb2c1ed3ba
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-0.1.10.tgz
version: 0.1.10
- apiVersion: v2
appVersion: 0.1.0
created: "2025-03-04T18:04:35.495842696+03:30"
description: Helm chart for flink kube operator
digest: abc08853c65ba36ff3485f182555522408e150f2508d4cac672d588972ddca3c
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-0.1.9.tgz
version: 0.1.9
- apiVersion: v2
appVersion: 0.1.0
created: "2025-03-04T18:04:35.495392608+03:30"
description: Helm chart for flink kube operator
digest: 3986a0a2348db1e17a1524eb0d87eabf6d64050d4007c5b393f723393cc4b675
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-0.1.8.tgz
version: 0.1.8
- apiVersion: v2
appVersion: 0.1.0
created: "2025-03-04T18:04:35.494948853+03:30"
description: Helm chart for flink kube operator
digest: 1bbeb92ecd10e36fa7d742a61cced0d842139ada0cfeff6fa1b0cf8718189235
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-0.1.7.tgz
version: 0.1.7
- apiVersion: v2
appVersion: 0.1.0
created: "2025-03-04T18:04:35.49450822+03:30"
description: Helm chart for flink kube operator
digest: 4031f4a79e65f6c5e60b6ebf9dd7e2a663b1fb6f893056ad81ca33660f94406e
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-0.1.6.tgz
version: 0.1.6
- apiVersion: v2
appVersion: 0.1.0
created: "2025-03-04T18:04:35.494040193+03:30"
description: Helm chart for flink kube operator
digest: 22ed155c8538ca5e7dc26863304eb9f76b09c454edbf709a891d7ccc440f35f6
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-0.1.5.tgz
version: 0.1.5
- apiVersion: v2
appVersion: 0.1.0
created: "2025-03-04T18:04:35.493584927+03:30"
description: Helm chart for flink kube operator
digest: b548a64ef89bbcd12d92fefffd1fd37758e8fccda02aecd97c7519a08f10fa4a
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-0.1.4.tgz
version: 0.1.4
- apiVersion: v2
appVersion: 0.1.0
created: "2025-03-04T18:04:35.493138547+03:30"
description: Helm chart for flink kube operator
digest: 05a9664f574e2d5d1cca764efb6481ad21b9176663b907973a8ef5264f15a91f
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-0.1.3.tgz
version: 0.1.3
- apiVersion: v2
appVersion: 0.1.0
created: "2025-03-04T18:04:35.492696005+03:30"
description: Helm chart for flink kube operator
digest: 89345b1a9a79aa18b646705aeb8cfdc547629600cb8a00708a3f64d188f296f2
name: flink-kube-operator
type: application
urls:
- flink-kube-operator-0.1.2.tgz
version: 0.1.2
- apiVersion: v2
appVersion: 0.1.0
created: "2025-03-04T18:04:35.490170385+03:30"
description: Helm chart for flink kube operator description: Helm chart for flink kube operator
digest: 1d2af9af6b9889cc2962d627946464766f1b65b05629073b7fffb9a98cd957e2 digest: 1d2af9af6b9889cc2962d627946464766f1b65b05629073b7fffb9a98cd957e2
name: flink-kube-operator name: flink-kube-operator
@@ -13,7 +229,7 @@ entries:
version: 0.1.1 version: 0.1.1
- apiVersion: v2 - apiVersion: v2
appVersion: 0.1.0 appVersion: 0.1.0
created: "2024-12-19T00:39:44.485286485+03:30" created: "2025-03-04T18:04:35.489734651+03:30"
description: Helm chart for flink kube operator description: Helm chart for flink kube operator
digest: 0890d955904e6a3b2155c086a933b27e45266d896fb69eaad0e811dea40414da digest: 0890d955904e6a3b2155c086a933b27e45266d896fb69eaad0e811dea40414da
name: flink-kube-operator name: flink-kube-operator
@@ -21,4 +237,4 @@ entries:
urls: urls:
- flink-kube-operator-0.1.0.tgz - flink-kube-operator-0.1.0.tgz
version: 0.1.0 version: 0.1.0
generated: "2024-12-19T00:39:44.48463577+03:30" generated: "2025-07-18T18:09:46.244672127+03:30"

83
internal/crd/auth.go Normal file
View File

@@ -0,0 +1,83 @@
package crd
import (
"context"
"encoding/json"
"fmt"
"os"
"github.com/google/go-containerregistry/pkg/authn"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
)
type dockerConfigJSON struct {
Auths map[string]authn.AuthConfig `json:"auths"`
}
// ResolveAuth returns an OCI authenticator for the given registry.
// It checks in order: pull secret from K8s, then environment variables.
// Returns authn.Anonymous if no credentials are found.
func (crd *Crd) ResolveAuth(pullSecretName string, server string) (authn.Authenticator, error) {
if pullSecretName != "" {
return crd.authFromPullSecret(pullSecretName, server)
}
return authFromEnv(server), nil
}
func (crd *Crd) authFromPullSecret(secretName string, server string) (authn.Authenticator, error) {
ns := os.Getenv("NAMESPACE")
secret := &corev1.Secret{}
err := crd.runtimeClient.Get(context.Background(), types.NamespacedName{
Namespace: ns,
Name: secretName,
}, secret)
if err != nil {
return nil, fmt.Errorf("reading pull secret %q: %w", secretName, err)
}
if secret.Type != corev1.SecretTypeDockerConfigJson {
return nil, fmt.Errorf("secret %q is not of type kubernetes.io/dockerconfigjson", secretName)
}
dockerConfigJSONBytes, ok := secret.Data[corev1.DockerConfigJsonKey]
if !ok {
return nil, fmt.Errorf("secret %q missing .dockerconfigjson key", secretName)
}
var cfg dockerConfigJSON
if err := json.Unmarshal(dockerConfigJSONBytes, &cfg); err != nil {
return nil, fmt.Errorf("parsing docker config from secret %q: %w", secretName, err)
}
if server != "" {
if entry, ok := cfg.Auths[server]; ok {
return authn.FromConfig(entry), nil
}
}
for _, entry := range cfg.Auths {
return authn.FromConfig(entry), nil
}
return authn.Anonymous, nil
}
func authFromEnv(server string) authn.Authenticator {
username := os.Getenv("REGISTRY_USERNAME")
password := os.Getenv("REGISTRY_PASSWORD")
envServer := os.Getenv("REGISTRY_SERVER")
if username == "" || password == "" {
return authn.Anonymous
}
if envServer != "" && server != "" && envServer != server {
return authn.Anonymous
}
return authn.FromConfig(authn.AuthConfig{
Username: username,
Password: password,
})
}

View File

@@ -1,14 +1,61 @@
package crd package crd
import ( import (
"github.com/reactivex/rxgo/v2" "context"
"flink-kube-operator/internal/crd/v1alpha1"
"flink-kube-operator/pkg"
"go.uber.org/zap"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
) )
func (crd Crd) manageFinalizer(jobEventObservable rxgo.Observable) { var FinalizerChannel chan (types.UID) = make(chan (types.UID))
for j := range jobEventObservable.Observe() {
jobEvent := j.V.(*FlinkJobCrdEvent) func (crd Crd) manageFinalizer(jobEventChannel chan FlinkJobCrdEvent) {
//pkg.Logger.Debug("[crd] [manage-finalizer] adding finalizer for", zap.String("name", jobEvent.Job.GetName()))
controllerutil.AddFinalizer(jobEvent.Job, "") finalizerName := "flink-operator.logicamp.tech/finalizer"
for jobEvent := range jobEventChannel {
pkg.Logger.Debug("[crd] [manage-finalizer] main loop", zap.String("name", jobEvent.Job.Name))
go func() {
if jobEvent.Job.GetDeletionTimestamp() != nil {
// Resource is being deleted
if controllerutil.ContainsFinalizer(jobEvent.Job, finalizerName) {
// Perform cleanup
pkg.Logger.Debug("[finalizer] stopping managed job", zap.String("name", jobEvent.Job.GetName()))
if err := crd.cleanupResources(jobEvent.Job); err != nil {
pkg.Logger.Info("[crd] [manage-finalizer] cleanup failed", zap.Error(err))
return
}
// Remove finalizer
controllerutil.RemoveFinalizer(jobEvent.Job, finalizerName)
if err := crd.runtimeClient.Update(context.Background(), jobEvent.Job); err != nil {
pkg.Logger.Info("[crd] [manage-finalizer] failed to remove finalizer", zap.Error(err))
return
}
pkg.Logger.Debug("[crd] [manage-finalizer] job removed", zap.String("name", jobEvent.Job.GetName()))
}
return
}
// Add finalizer if not present
if !controllerutil.ContainsFinalizer(jobEvent.Job, finalizerName) {
controllerutil.AddFinalizer(jobEvent.Job, finalizerName)
pkg.Logger.Debug("[finalizer] adding job")
// Update the resource to add the finalizer
if err := crd.runtimeClient.Update(context.Background(), jobEvent.Job); err != nil {
pkg.Logger.Info("[finalizer] failed to add", zap.Error(err))
return
} }
} }
}()
}
}
func (crd Crd) cleanupResources(job *v1alpha1.FlinkJob) error {
FinalizerChannel <- job.GetUID()
return nil
}

View File

@@ -2,44 +2,59 @@ package crd
import ( import (
"flink-kube-operator/internal/crd/v1alpha1" "flink-kube-operator/internal/crd/v1alpha1"
"os"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic"
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/config"
) )
type Crd struct { type Crd struct {
client dynamic.NamespaceableResourceInterface client dynamic.ResourceInterface
runtimeClient client.Client
} }
func New() *Crd { func New() *Crd {
// Get Kubernetes config // Get Kubernetes config_
config, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile) config_, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)
if err != nil { if err != nil {
config, err = rest.InClusterConfig() config_, err = rest.InClusterConfig()
if err != nil { if err != nil {
panic(err) panic(err)
} }
} }
// Create dynamic client // Create dynamic client
dynamicClient, err := dynamic.NewForConfig(config) dynamicClient, err := dynamic.NewForConfig(config_)
if err != nil { if err != nil {
panic(err) panic(err)
} }
scheme := runtime.NewScheme()
v1alpha1.AddKnownTypes(scheme)
// Get FlinkJob resource interface // Get FlinkJob resource interface
flinkJobClient := dynamicClient.Resource(v1alpha1.FlinkJobGVR) flinkJobClient := dynamicClient.Resource(v1alpha1.FlinkJobGVR).Namespace(os.Getenv("NAMESPACE"))
runtimeClient, err := client.New(config.GetConfigOrDie(), client.Options{
Scheme: scheme,
})
if err != nil {
panic(err)
}
crd := Crd{ crd := Crd{
client: flinkJobClient, client: flinkJobClient,
runtimeClient: runtimeClient,
} }
// Watch for FlinkJob creation jobEventCh := make(chan FlinkJobCrdEvent)
jobEventObservable := crd.watchFlinkJobs()
// add finalizer to new resources // add finalizer to new resources
go crd.manageFinalizer(jobEventObservable) go crd.manageFinalizer(jobEventCh)
// Watch for FlinkJob creation
crd.watchFlinkJobs(jobEventCh)
return &crd return &crd
} }

View File

@@ -14,7 +14,7 @@ import (
func (crd *Crd) Patch(jobUid types.UID, patchData map[string]interface{}) error { func (crd *Crd) Patch(jobUid types.UID, patchData map[string]interface{}) error {
job := GetJob(jobUid) job := GetJob(jobUid)
pkg.Logger.Debug("[patch-job]", zap.Any("jobUid", jobUid)) // pkg.Logger.Debug("[patch-job]", zap.Any("jobUid", jobUid))
patchBytes, err := json.Marshal(patchData) patchBytes, err := json.Marshal(patchData)
if err != nil { if err != nil {
@@ -23,7 +23,6 @@ func (crd *Crd) Patch(jobUid types.UID, patchData map[string]interface{}) error
// Patch the status sub-resource // Patch the status sub-resource
unstructuredJob, err := crd.client. unstructuredJob, err := crd.client.
Namespace(job.GetNamespace()).
Patch( Patch(
context.Background(), context.Background(),
job.GetName(), job.GetName(),

View File

@@ -14,6 +14,10 @@ func (crd *Crd) repsert(job *v1alpha1.FlinkJob) {
jobs.Store(job.GetUID(), job) jobs.Store(job.GetUID(), job)
} }
func (crd *Crd) remove(uid types.UID) {
jobs.Delete(uid)
}
func GetJob(uid types.UID) v1alpha1.FlinkJob { func GetJob(uid types.UID) v1alpha1.FlinkJob {
job, _ := jobs.Load(uid) job, _ := jobs.Load(uid)
return *job.DeepCopy() return *job.DeepCopy()

View File

@@ -1,7 +1,10 @@
package v1alpha1 package v1alpha1
import ( import (
"encoding/json"
"errors" "errors"
"sort"
"strings"
"time" "time"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -9,14 +12,70 @@ import (
//go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen object paths=$GOFILE //go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen object paths=$GOFILE
// Args is a list of program arguments passed to the Flink job.
// It accepts two formats in JSON/YAML:
//
// List: ["--kafka-host", "broker:9092", "--kafka-group-id", "my-group"]
// Map: {kafka-host: "broker:9092", kafka-group-id: "my-group"}
//
// Map keys are automatically prefixed with "--" and expanded into alternating
// key/value pairs. The internal representation is always a flat []string.
type Args []string
// UnmarshalJSON implements dual-format deserialization.
func (a *Args) UnmarshalJSON(data []byte) error {
// Try map format first: {"key": "value", ...}
var m map[string]string
if err := json.Unmarshal(data, &m); err == nil {
*a = make(Args, 0, len(m)*2)
// Sort keys for deterministic ordering.
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
*a = append(*a, "--"+k, m[k])
}
return nil
}
// Fall back to list format: ["--key", "value", ...]
var s []string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
*a = Args(s)
return nil
}
// MarshalJSON serializes Args in the compact map format.
func (a Args) MarshalJSON() ([]byte, error) {
m := make(map[string]string, len(a)/2)
for i := 0; i < len(a)-1; i += 2 {
key := strings.TrimPrefix(a[i], "--")
m[key] = a[i+1]
}
return json.Marshal(m)
}
type FlinkJobSpec struct { type FlinkJobSpec struct {
Key string `json:"key"` Key string `json:"key"`
Name string `json:"name"` Name string `json:"name"`
FlinkCluster string `json:"flinkCluster"`
Parallelism int `json:"parallelism"` Parallelism int `json:"parallelism"`
JarURI string `json:"jarUri"` JarRef string `json:"jarRef,omitempty"`
JarPullSecret string `json:"jarPullSecret,omitempty"`
SavepointInterval metaV1.Duration `json:"savepointInterval"` SavepointInterval metaV1.Duration `json:"savepointInterval"`
EntryClass string `json:"entryClass"` EntryClass string `json:"entryClass"`
Args []string `json:"args"` Args Args `json:"args,omitempty"`
// Deprecated: use JarRef instead. Kept for backward compatibility.
JarURI string `json:"jarUri,omitempty"`
// Deprecated: use JarPullSecret instead. Kept for backward compatibility.
JarURIBasicAuthUsername *string `json:"jarURIBasicAuthUsername,omitempty"`
// Deprecated: use JarPullSecret instead. Kept for backward compatibility.
JarURIBasicAuthPassword *string `json:"jarURIBasicAuthPassword,omitempty"`
} }
type FlinkJobStatus struct { type FlinkJobStatus struct {
@@ -32,7 +91,10 @@ type FlinkJobStatus struct {
LastRestoredSavepointDate *time.Time `json:"lastRestoredSavepointDate,omitempty"` LastRestoredSavepointDate *time.Time `json:"lastRestoredSavepointDate,omitempty"`
LastRestoredSavepointRestoredDate *time.Time `json:"lastRestoredSavepointRestoredDate,omitempty"` LastRestoredSavepointRestoredDate *time.Time `json:"lastRestoredSavepointRestoredDate,omitempty"`
RestoredCount int `json:"restoredCount,omitempty"` RestoredCount int `json:"restoredCount,omitempty"`
RunningJarURI *string `json:"runningJarURI"` RunningJarDigest *string `json:"runningJarDigest,omitempty"`
RunningJarRef *string `json:"runningJarRef,omitempty"`
// Deprecated: kept for backward compatibility during migration.
RunningJarURI *string `json:"runningJarURI,omitempty"`
} }
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
@@ -87,3 +149,48 @@ const (
LifeCycleStatusHealthy LifeCycleStatus = "HEALTHY" LifeCycleStatusHealthy LifeCycleStatus = "HEALTHY"
LifeCycleStatusFailed LifeCycleStatus = "FAILED" LifeCycleStatusFailed LifeCycleStatus = "FAILED"
) )
// EffectiveJarRef returns the OCI reference, preferring JarRef over the deprecated JarURI.
func (s *FlinkJobSpec) EffectiveJarRef() string {
if s.JarRef != "" {
return s.JarRef
}
return s.JarURI
}
// IsOCISource returns true if the job uses an OCI reference (jarRef) rather than an HTTP URL (jarUri).
func (s *FlinkJobSpec) IsOCISource() bool {
return s.JarRef != ""
}
// RunningRef returns the currently tracked running jar reference,
// preferring RunningJarRef (OCI) over the deprecated RunningJarURI.
func (st *FlinkJobStatus) RunningRef() string {
if st.RunningJarRef != nil {
return *st.RunningJarRef
}
if st.RunningJarURI != nil {
return *st.RunningJarURI
}
return ""
}
// NeedsUpgrade returns true if the spec's jar reference differs from
// what is currently running, indicating an upgrade is needed.
func (s *FlinkJobSpec) NeedsUpgrade(st FlinkJobStatus) bool {
running := st.RunningRef()
if running == "" {
return false
}
return s.EffectiveJarRef() != running
}
// RunningRefPatchKey returns the status patch key and value for tracking
// which jar reference is currently running.
func (s *FlinkJobSpec) RunningRefPatchData() (key string, value interface{}) {
if s.IsOCISource() {
ref := s.EffectiveJarRef()
return "runningJarRef", &ref
}
return "runningJarURI", s.JarURI
}

View File

@@ -6,7 +6,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
) )
const GroupName = "flink.logicamp.tech" const GroupName = "flink.logicamp.dev"
const GroupVersion = "v1alpha1" const GroupVersion = "v1alpha1"
const ResourceName = "flink-jobs" const ResourceName = "flink-jobs"
@@ -20,11 +20,11 @@ var FlinkJobGVR = schema.GroupVersionResource{
} }
var ( var (
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) SchemeBuilder = runtime.NewSchemeBuilder(AddKnownTypes)
AddToScheme = SchemeBuilder.AddToScheme AddToScheme = SchemeBuilder.AddToScheme
) )
func addKnownTypes(scheme *runtime.Scheme) error { func AddKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion, scheme.AddKnownTypes(SchemeGroupVersion,
&FlinkJob{}, &FlinkJob{},
&FlinkJobList{}, &FlinkJobList{},

View File

@@ -5,6 +5,8 @@
package v1alpha1 package v1alpha1
import ( import (
"time"
runtime "k8s.io/apimachinery/pkg/runtime" runtime "k8s.io/apimachinery/pkg/runtime"
) )
@@ -13,7 +15,8 @@ func (in *FlinkJob) DeepCopyInto(out *FlinkJob) {
*out = *in *out = *in
out.TypeMeta = in.TypeMeta out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
} }
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlinkJob. // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlinkJob.
@@ -34,6 +37,111 @@ func (in *FlinkJob) DeepCopyObject() runtime.Object {
return nil return nil
} }
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FlinkJobSpec) DeepCopyInto(out *FlinkJobSpec) {
*out = *in
if in.JarURIBasicAuthUsername != nil {
in, out := &in.JarURIBasicAuthUsername, &out.JarURIBasicAuthUsername
*out = new(string)
**out = **in
}
if in.JarURIBasicAuthPassword != nil {
in, out := &in.JarURIBasicAuthPassword, &out.JarURIBasicAuthPassword
*out = new(string)
**out = **in
}
if in.Args != nil {
in, out := &in.Args, &out.Args
*out = make(Args, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlinkJobSpec.
func (in *FlinkJobSpec) DeepCopy() *FlinkJobSpec {
if in == nil {
return nil
}
out := new(FlinkJobSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FlinkJobStatus) DeepCopyInto(out *FlinkJobStatus) {
*out = *in
if in.LastSavepointPath != nil {
in, out := &in.LastSavepointPath, &out.LastSavepointPath
*out = new(string)
**out = **in
}
if in.JarId != nil {
in, out := &in.JarId, &out.JarId
*out = new(string)
**out = **in
}
if in.JobId != nil {
in, out := &in.JobId, &out.JobId
*out = new(string)
**out = **in
}
if in.Error != nil {
in, out := &in.Error, &out.Error
*out = new(string)
**out = **in
}
if in.SavepointTriggerId != nil {
in, out := &in.SavepointTriggerId, &out.SavepointTriggerId
*out = new(string)
**out = **in
}
if in.PauseSavepointTriggerId != nil {
in, out := &in.PauseSavepointTriggerId, &out.PauseSavepointTriggerId
*out = new(string)
**out = **in
}
if in.LastSavepointDate != nil {
in, out := &in.LastSavepointDate, &out.LastSavepointDate
*out = new(time.Time)
**out = **in
}
if in.LastRestoredSavepointDate != nil {
in, out := &in.LastRestoredSavepointDate, &out.LastRestoredSavepointDate
*out = new(time.Time)
**out = **in
}
if in.LastRestoredSavepointRestoredDate != nil {
in, out := &in.LastRestoredSavepointRestoredDate, &out.LastRestoredSavepointRestoredDate
*out = new(time.Time)
**out = **in
}
if in.RunningJarDigest != nil {
in, out := &in.RunningJarDigest, &out.RunningJarDigest
*out = new(string)
**out = **in
}
if in.RunningJarRef != nil {
in, out := &in.RunningJarRef, &out.RunningJarRef
*out = new(string)
**out = **in
}
if in.RunningJarURI != nil {
in, out := &in.RunningJarURI, &out.RunningJarURI
*out = new(string)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FlinkJobStatus.
func (in *FlinkJobStatus) DeepCopy() *FlinkJobStatus {
if in == nil {
return nil
}
out := new(FlinkJobStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FlinkJobList) DeepCopyInto(out *FlinkJobList) { func (in *FlinkJobList) DeepCopyInto(out *FlinkJobList) {
*out = *in *out = *in

View File

@@ -3,10 +3,10 @@ package crd
import ( import (
"context" "context"
"flink-kube-operator/internal/crd/v1alpha1" "flink-kube-operator/internal/crd/v1alpha1"
"os"
"flink-kube-operator/pkg" "flink-kube-operator/pkg"
"github.com/reactivex/rxgo/v2"
"go.uber.org/zap" "go.uber.org/zap"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -14,38 +14,42 @@ import (
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
) )
func (crd Crd) watchFlinkJobs() rxgo.Observable { func (crd Crd) watchFlinkJobs(ch chan FlinkJobCrdEvent) {
ch := make(chan rxgo.Item)
go func() { go func() {
for {
pkg.Logger.Debug("[crd] starting watch") pkg.Logger.Debug("[crd] starting watch")
watcher, err := crd.client.Watch(context.Background(), metaV1.ListOptions{}) watcher, err := crd.client.Watch(context.Background(), metaV1.ListOptions{})
if err != nil { if err != nil {
panic(err) panic(err)
} }
defer watcher.Stop() namespace := os.Getenv("NAMESPACE")
pkg.Logger.Debug("[crd] [watch]", zap.String("namespace", namespace))
for event := range watcher.ResultChan() { for event := range watcher.ResultChan() {
unstructuredJob := event.Object.(*unstructured.Unstructured) unstructuredJob := event.Object.(*unstructured.Unstructured)
unstructuredMap, _, err := unstructured.NestedMap(unstructuredJob.Object) unstructuredMap, _, err := unstructured.NestedMap(unstructuredJob.Object)
if err != nil { if err != nil {
pkg.Logger.Error("cannot create unstructured map", zap.Error(err)) pkg.Logger.Error("[crd] [watch]cannot create unstructured map", zap.Error(err))
continue continue
} }
job := &v1alpha1.FlinkJob{} job := &v1alpha1.FlinkJob{}
err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredMap, job) err = runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredMap, job)
if err != nil { if err != nil {
pkg.Logger.Error("cannot convert unstructured to structured", zap.Error(err)) pkg.Logger.Error("[crd] [watch]cannot convert unstructured to structured", zap.Error(err))
continue
}
if job.Namespace != namespace {
continue continue
} }
ch <- rxgo.Item{ go func() {
V: &FlinkJobCrdEvent{ ch <- FlinkJobCrdEvent{
EventType: event.Type, EventType: event.Type,
Job: job, Job: job,
},
} }
}()
pkg.Logger.Debug("[crd] [watch] change in", zap.String("name", job.Name), zap.String("operation", string(event.Type)))
switch event.Type { switch event.Type {
case watch.Bookmark: case watch.Bookmark:
case watch.Modified: case watch.Modified:
@@ -55,10 +59,12 @@ func (crd Crd) watchFlinkJobs() rxgo.Observable {
//pkg.Logger.Info("[crd] [watch] new flink job created") //pkg.Logger.Info("[crd] [watch] new flink job created")
crd.repsert(job) crd.repsert(job)
case watch.Deleted: case watch.Deleted:
crd.remove(job.UID)
} }
} }
defer watcher.Stop()
pkg.Logger.Warn("[crd] [watch] Watcher stopped, restarting...")
}
}() }()
return rxgo.FromChannel(ch)
} }

View File

@@ -1,7 +1,10 @@
package jar package jar
import ( import (
"crypto/rand"
"encoding/hex"
"errors" "errors"
"fmt"
"io" "io"
"net/http" "net/http"
"os" "os"
@@ -9,19 +12,43 @@ import (
"flink-kube-operator/pkg" "flink-kube-operator/pkg"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/google/go-containerregistry/pkg/v1/types"
api "github.com/logi-camp/go-flink-client" api "github.com/logi-camp/go-flink-client"
gonanoid "github.com/matoous/go-nanoid/v2"
"go.uber.org/zap" "go.uber.org/zap"
) )
type JarFile struct { type JarFile struct {
uri string ref string
filePath string filePath string
digest string
basicAuthUsername *string
basicAuthPassword *string
auth authn.Authenticator
} }
func NewJarFile(URI string) (*JarFile, error) { // NewJarFile creates a JarFile from an OCI reference, pulling the artifact.
// auth is the OCI authenticator (from pull secret or env vars). Use authn.Anonymous for public registries.
func NewJarFile(ref string, auth authn.Authenticator) (*JarFile, error) {
jarFile := &JarFile{ jarFile := &JarFile{
uri: URI, ref: ref,
auth: auth,
}
err := jarFile.Pull()
if err != nil {
return nil, err
}
return jarFile, nil
}
// NewJarFileFromURI creates a JarFile from an HTTP URI (deprecated path, kept for backward compat).
func NewJarFileFromURI(uri string, basicAuthUsername *string, basicAuthPassword *string) (*JarFile, error) {
jarFile := &JarFile{
ref: uri,
basicAuthUsername: basicAuthUsername,
basicAuthPassword: basicAuthPassword,
} }
err := jarFile.Download() err := jarFile.Download()
if err != nil { if err != nil {
@@ -31,10 +58,10 @@ func NewJarFile(URI string) (*JarFile, error) {
} }
func (jarFile *JarFile) Upload(flinkClient *api.Client) (fileName string, err error) { func (jarFile *JarFile) Upload(flinkClient *api.Client) (fileName string, err error) {
resp, err := flinkClient.UploadJar(jarFile.filePath) resp, err := flinkClient.UploadJar(jarFile.filePath)
if err != nil { if err != nil {
pkg.Logger.Error("[main] error uploading jar", zap.Error(err)) pkg.Logger.Error("[jar] error uploading jar", zap.Error(err))
return "", err
} }
filePathParts := strings.Split(resp.FileName, "/") filePathParts := strings.Split(resp.FileName, "/")
fileName = filePathParts[len(filePathParts)-1] fileName = filePathParts[len(filePathParts)-1]
@@ -45,8 +72,82 @@ func (jarFile *JarFile) Upload(flinkClient *api.Client) (fileName string, err er
return return
} }
func (jarFile *JarFile) Digest() string {
return jarFile.digest
}
func (jarFile *JarFile) Pull() error {
randBytes := make([]byte, 16)
rand.Read(randBytes)
fileName := hex.EncodeToString(randBytes)
jarFile.filePath = "/tmp/" + fileName + ".jar"
ref, err := name.ParseReference(jarFile.ref)
if err != nil {
return fmt.Errorf("parsing OCI reference %q: %w", jarFile.ref, err)
}
img, err := remote.Image(ref, remote.WithAuth(jarFile.auth))
if err != nil {
return fmt.Errorf("fetching OCI image %q: %w", jarFile.ref, err)
}
d, err := img.Digest()
if err != nil {
return fmt.Errorf("getting image digest: %w", err)
}
jarFile.digest = d.String()
layers, err := img.Layers()
if err != nil {
return fmt.Errorf("getting image layers: %w", err)
}
if len(layers) == 0 {
return fmt.Errorf("OCI image %q has no layers", jarFile.ref)
}
layer := layers[0]
mediaType, err := layer.MediaType()
if err != nil {
return fmt.Errorf("getting layer media type: %w", err)
}
if mediaType != types.OCILayer && mediaType != types.OCIUncompressedLayer && mediaType != types.DockerLayer && mediaType != types.DockerUncompressedLayer && string(mediaType) != "application/java-archive" {
pkg.Logger.Warn("[jar] unexpected layer media type, expected application/java-archive or standard OCI layer",
zap.String("got", string(mediaType)),
zap.String("ref", jarFile.ref),
)
}
blob, err := layer.Compressed()
if err != nil {
return fmt.Errorf("reading layer blob: %w", err)
}
defer blob.Close()
out, err := os.Create(jarFile.filePath)
if err != nil {
return fmt.Errorf("creating temp file: %w", err)
}
defer out.Close()
_, err = io.Copy(out, blob)
if err != nil {
jarFile.delete()
return fmt.Errorf("writing layer to disk: %w", err)
}
pkg.Logger.Info("[jar] pulled OCI artifact",
zap.String("ref", jarFile.ref),
zap.String("digest", jarFile.digest),
zap.String("path", jarFile.filePath),
)
return nil
}
func (jarFile *JarFile) Download() error { func (jarFile *JarFile) Download() error {
fileName, _ := gonanoid.New() randBytes := make([]byte, 16)
rand.Read(randBytes)
fileName := hex.EncodeToString(randBytes)
jarFile.filePath = "/tmp/" + fileName + ".jar" jarFile.filePath = "/tmp/" + fileName + ".jar"
out, err := os.Create(jarFile.filePath) out, err := os.Create(jarFile.filePath)
if err != nil { if err != nil {
@@ -54,11 +155,29 @@ func (jarFile *JarFile) Download() error {
} }
defer out.Close() defer out.Close()
resp, err := http.Get(jarFile.uri)
if err != nil || resp.StatusCode > 299 { var resp *http.Response
if jarFile.basicAuthPassword != nil && jarFile.basicAuthUsername != nil {
req, err := http.NewRequest("GET", jarFile.ref, nil)
if err != nil {
jarFile.delete() jarFile.delete()
return err return err
} }
req.SetBasicAuth(*jarFile.basicAuthUsername, *jarFile.basicAuthPassword)
resp, err = http.DefaultClient.Do(req)
} else {
resp, err = http.Get(jarFile.ref)
}
if err != nil {
jarFile.delete()
pkg.Logger.Error("error in downloading jar", zap.Error(err))
return err
}
if resp.StatusCode > 299 {
err = fmt.Errorf("download failed: status %s", resp.Status)
pkg.Logger.Error("error in downloading jar", zap.Error(err))
return err
}
defer resp.Body.Close() defer resp.Body.Close()
_, err = io.Copy(out, resp.Body) _, err = io.Copy(out, resp.Body)

View File

@@ -10,16 +10,9 @@ import (
) )
func (job *ManagedJob) Cycle() { func (job *ManagedJob) Cycle() {
pkg.Logger.Debug("[managed-job] [new] check cycle", zap.String("jobName", job.def.GetName()))
// Init job // Init job
if job.def.Status.LifeCycleStatus == "" && job.def.Status.JobStatus == "" { if job.def.Status.LifeCycleStatus == "" && (job.def.Status.JobStatus == "" || job.def.Status.JobStatus == v1alpha1.JobStatusFinished) {
job.run(false) job.Run(false)
return
}
if job.def.Status.JobStatus == v1alpha1.JobStatusFinished && job.def.Status.LifeCycleStatus == v1alpha1.LifeCycleStatusGracefullyPaused {
job.run(true)
return return
} }
@@ -31,7 +24,8 @@ func (job *ManagedJob) Cycle() {
job.trackSavepoint() job.trackSavepoint()
} }
} }
if job.def.Status.RunningJarURI != nil && job.def.Spec.JarURI != *job.def.Status.RunningJarURI {
if job.def.Spec.NeedsUpgrade(job.def.Status) {
job.upgrade() job.upgrade()
} }
@@ -47,14 +41,6 @@ func (job *ManagedJob) Cycle() {
job.crd.SetJobStatus(job.def.UID, job.def.Status) job.crd.SetJobStatus(job.def.UID, job.def.Status)
return return
} }
// if job.def.Status.JobStatus == v1alpha1.JobStatusFailed && job.def.Status.LastSavepointPath != nil {
// //job.restore()
// return
// }
// if job.def.Status.JobStatus == v1alpha1.JobStatusFailed && job.def.Status.LastSavepointPath == nil {
// //job.restore()
// return
// }
pkg.Logger.Warn("[managed-job] [cycle]", zap.String("unhanded job status", string(job.def.Status.JobStatus))) pkg.Logger.Warn("[managed-job] [cycle] unhandled job status", zap.String("name", job.def.Name), zap.String("status", string(job.def.Status.JobStatus)), zap.String("namespace", job.def.Namespace))
} }

View File

@@ -0,0 +1,93 @@
package managed_job
import (
"flink-kube-operator/internal/jar"
"flink-kube-operator/pkg"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/name"
"go.uber.org/zap"
)
// upload pulls/downloads the jar and uploads it to Flink, then patches the CRD status.
func (job *ManagedJob) upload() error {
spec := job.def.Spec
var jarFile *jar.JarFile
var err error
if spec.IsOCISource() {
auth, authErr := job.resolveAuth()
if authErr != nil {
pkg.Logger.Error("[managed-job] [upload] auth resolution failed", zap.Error(authErr))
return authErr
}
jarFile, err = jar.NewJarFile(spec.EffectiveJarRef(), auth)
} else {
jarFile, err = jar.NewJarFileFromURI(spec.JarURI, spec.JarURIBasicAuthUsername, spec.JarURIBasicAuthPassword)
}
if err != nil {
pkg.Logger.Debug("[managed-job] [upload] error on pull/download jar", zap.Error(err))
return err
}
jarId, err := jarFile.Upload(job.client)
if err != nil {
pkg.Logger.Debug("[managed-job] [upload] error on upload jar", zap.Error(err))
return err
}
pkg.Logger.Info("[managed-job] [upload] uploaded", zap.String("jarId", jarId))
patchData := map[string]interface{}{
"status": map[string]interface{}{
"jarId": &jarId,
},
}
if spec.IsOCISource() {
digest := jarFile.Digest()
patchData["status"].(map[string]interface{})["runningJarDigest"] = &digest
}
job.crd.Patch(job.def.UID, patchData)
return nil
}
func (job *ManagedJob) resolveAuth() (authn.Authenticator, error) {
s := job.def.Spec
server := ""
ref := s.EffectiveJarRef()
if reg, err := extractRegistry(ref); err == nil {
server = reg
}
return job.crd.ResolveAuth(s.JarPullSecret, server)
}
func extractRegistry(ref string) (string, error) {
r, err := name.NewRegistry(ref)
if err != nil {
t, err2 := name.NewTag(ref)
if err2 != nil {
return "", err
}
return t.RegistryStr(), nil
}
return r.RegistryStr(), nil
}
func (job *ManagedJob) RemoveJar() {
if job.def.Status.JarId != nil {
err := job.client.DeleteJar(*job.def.Status.JarId)
if err != nil {
pkg.Logger.Error("[managed-job] [jar] delete jar error", zap.Error(err))
}
err = job.crd.Patch(job.def.UID, map[string]interface{}{
"status": map[string]interface{}{
"jarId": nil,
},
})
if err != nil {
pkg.Logger.Error("[managed-job] [jar] patch jarId error", zap.Error(err))
}
}
}

View File

@@ -1,9 +1,9 @@
package manager package managed_job
import ( import (
"flink-kube-operator/internal/crd" "flink-kube-operator/internal/crd"
"flink-kube-operator/internal/crd/v1alpha1" "flink-kube-operator/internal/crd/v1alpha1"
"flink-kube-operator/internal/managed_job" "os"
"time" "time"
"flink-kube-operator/pkg" "flink-kube-operator/pkg"
@@ -16,16 +16,22 @@ import (
type Manager struct { type Manager struct {
client *api.Client client *api.Client
managedJobs map[types.UID]managed_job.ManagedJob managedJobs map[types.UID]ManagedJob
processingJobsIds []types.UID processingJobsIds []types.UID
} }
var mgr Manager
func GetManager() Manager {
return mgr
}
func NewManager(client *api.Client, crdInstance *crd.Crd) Manager { func NewManager(client *api.Client, crdInstance *crd.Crd) Manager {
ticker := time.NewTicker(5 * time.Second) ticker := time.NewTicker(5 * time.Second)
quit := make(chan struct{}) quit := make(chan struct{})
mgr := Manager{ mgr = Manager{
client: client, client: client,
managedJobs: map[types.UID]managed_job.ManagedJob{}, managedJobs: map[types.UID]ManagedJob{},
processingJobsIds: []types.UID{}, processingJobsIds: []types.UID{},
} }
@@ -41,6 +47,18 @@ func NewManager(client *api.Client, crdInstance *crd.Crd) Manager {
} }
} }
}() }()
go func() {
for event := range crd.FinalizerChannel {
manager := mgr.GetJob(event)
if manager != nil {
err := manager.Stop()
pkg.Logger.Info("[finalizer]", zap.Error(err))
delete(mgr.managedJobs, event)
}
}
}()
return mgr return mgr
} }
@@ -54,11 +72,12 @@ func (mgr *Manager) cycle(client *api.Client, crdInstance *crd.Crd) {
}, },
}) })
} }
//pkg.Logger.Debug("[manager] [cycle] overviews", zap.Any("overviews", jobsOverviews)) pkg.Logger.Debug("[manager] [cycle] overviews", zap.Any("overviews", jobManagerJobOverviews))
operatorId := os.Getenv("OPERATOR_ID")
// Loop over job definitions as Kubernetes CRD // Loop over job definitions as Kubernetes CRD
for _, uid := range crd.GetAllJobKeys() { for _, uid := range crd.GetAllJobKeys() {
pkg.Logger.Debug("mgr.processingJobsIds", zap.Any("processingJobIds", mgr.processingJobsIds))
if lo.Contains(mgr.processingJobsIds, uid) { if lo.Contains(mgr.processingJobsIds, uid) {
pkg.Logger.Warn("[manager] already in process", zap.Any("uid", uid)) pkg.Logger.Warn("[manager] already in process", zap.Any("uid", uid))
continue continue
@@ -66,29 +85,50 @@ func (mgr *Manager) cycle(client *api.Client, crdInstance *crd.Crd) {
// Get job definition from Kubernetes CRD // Get job definition from Kubernetes CRD
def := crd.GetJob(uid) def := crd.GetJob(uid)
// Auto-claim: if flinkCluster is empty, set it to this operator's ID
if def.Spec.FlinkCluster == "" {
pkg.Logger.Info("[manager] auto-claiming unowned job", zap.String("name", def.GetName()))
crdInstance.Patch(def.GetUID(), map[string]interface{}{
"spec": map[string]interface{}{
"flinkCluster": operatorId,
},
})
def.Spec.FlinkCluster = operatorId
}
// Skip CRs that belong to another operator
if def.Spec.FlinkCluster != operatorId {
pkg.Logger.Debug("[manager] skipping job owned by another operator",
zap.String("name", def.GetName()),
zap.String("flinkCluster", def.Spec.FlinkCluster),
zap.String("operatorId", operatorId),
)
continue
}
mgr.processingJobsIds = append(mgr.processingJobsIds, uid) mgr.processingJobsIds = append(mgr.processingJobsIds, uid)
// Check if job exists in manager managed jobs // Check if job exists in manager managed jobs
managedJob, ok := mgr.managedJobs[uid] managedJob, jobFound := mgr.managedJobs[uid]
if ok { if jobFound {
managedJob.Update(def) managedJob.Update(def)
} else { } else {
// Add job to manager managed job // Add job to manager managed job
managedJob = *managed_job.NewManagedJob(client, def, crdInstance) managedJob = *NewManagedJob(client, def, crdInstance)
} }
if jobManagerJobStatusError != nil { if jobManagerJobStatusError != nil {
} }
jobManagerJobOverview, ok := lo.Find(jobManagerJobOverviews.Jobs, func(job api.JobOverview) bool { jobManagerJobOverview, jobFound := lo.Find(jobManagerJobOverviews.Jobs, func(job api.JobOverview) bool {
jobId := managedJob.GetJobId() jobId := managedJob.GetJobId()
if jobId != nil { if jobId != nil {
return job.ID == *jobId return job.ID == *jobId
} }
return false return false
}) })
if ok { if jobFound {
pkg.Logger.Debug("[manager] read status from flink", zap.String("name", jobManagerJobOverview.Name), zap.String("state", jobManagerJobOverview.State)) // pkg.Logger.Debug("[manager] read status from flink", zap.String("name", jobManagerJobOverview.Name), zap.String("state", jobManagerJobOverview.State))
patchStatusObj := map[string]interface{}{ patchStatusObj := map[string]interface{}{
"jobStatus": v1alpha1.JobStatus(jobManagerJobOverview.State), "jobStatus": v1alpha1.JobStatus(jobManagerJobOverview.State),
} }
@@ -100,6 +140,16 @@ func (mgr *Manager) cycle(client *api.Client, crdInstance *crd.Crd) {
crdInstance.Patch(uid, map[string]interface{}{ crdInstance.Patch(uid, map[string]interface{}{
"status": patchStatusObj, "status": patchStatusObj,
}) })
} else {
// TODO handle job not found status
// patchStatusObj := map[string]interface{}{
// "jobStatus": "",
// "lifeCycleStatus": string(v1alpha1.LifeCycleStatusFailed),
// }
// crdInstance.Patch(uid, map[string]interface{}{
// "status": patchStatusObj,
// })
} }
managedJob.Cycle() managedJob.Cycle()
@@ -110,3 +160,12 @@ func (mgr *Manager) cycle(client *api.Client, crdInstance *crd.Crd) {
}) })
} }
} }
func (mgr *Manager) GetJobs() map[types.UID]ManagedJob {
return mgr.managedJobs
}
func (mgr *Manager) GetJob(id types.UID) *ManagedJob {
job := mgr.managedJobs[id]
return &job
}

View File

@@ -10,7 +10,7 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
func (job *ManagedJob) pause() error { func (job *ManagedJob) Pause() error {
var err error var err error
if job.def.Status.JobId != nil { if job.def.Status.JobId != nil {
result, stopJobErr := job.client.StopJobWithSavepoint(*job.def.Status.JobId, os.Getenv("SAVEPOINT_PATH"), false) result, stopJobErr := job.client.StopJobWithSavepoint(*job.def.Status.JobId, os.Getenv("SAVEPOINT_PATH"), false)
@@ -33,7 +33,7 @@ func (job *ManagedJob) pause() error {
if savepointPath != "" { if savepointPath != "" {
job.def.Status.LastSavepointPath = &savepointPath job.def.Status.LastSavepointPath = &savepointPath
job.def.Status.PauseSavepointTriggerId = nil job.def.Status.PauseSavepointTriggerId = nil
job.def.Status.JobStatus = "" job.def.Status.JobStatus = "FINISHED"
job.def.Status.LastSavepointPath = &savepointPath job.def.Status.LastSavepointPath = &savepointPath
lastSavepointDate := time.Now() lastSavepointDate := time.Now()
job.def.Status.LastSavepointDate = &lastSavepointDate job.def.Status.LastSavepointDate = &lastSavepointDate

View File

@@ -1,5 +1,11 @@
package managed_job package managed_job
func (job *ManagedJob) Stop() { import "errors"
job.client.StopJob(*job.def.Status.JobId)
func (job *ManagedJob) Stop() error {
if job.def.Status.JobId != nil {
return job.client.StopJob(*job.def.Status.JobId)
} else {
return errors.New("job Id not found")
}
} }

View File

@@ -11,8 +11,8 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
// run the job from savepoint and jarId in managedJob // Run the job from savepoint and jarId in managedJob
func (job *ManagedJob) run(restoreMode bool) error { func (job *ManagedJob) Run(restoreMode bool) error {
var savepointPath string var savepointPath string
if job.def.Status.LastSavepointPath == nil { if job.def.Status.LastSavepointPath == nil {
pkg.Logger.Error("[managed-job] [restore]", zap.Error(v1alpha1.ErrNoSavepointPath)) pkg.Logger.Error("[managed-job] [restore]", zap.Error(v1alpha1.ErrNoSavepointPath))
@@ -43,7 +43,7 @@ func (job *ManagedJob) run(restoreMode bool) error {
EntryClass: job.def.Spec.EntryClass, EntryClass: job.def.Spec.EntryClass,
SavepointPath: savepointPath, SavepointPath: savepointPath,
Parallelism: job.def.Spec.Parallelism, Parallelism: job.def.Spec.Parallelism,
ProgramArg: job.def.Spec.Args, ProgramArgsList: job.def.Spec.Args,
}) })
if err == nil { if err == nil {
pkg.Logger.Info("[managed-job] [run] jar successfully ran", zap.Any("run-jar-resp", runJarResp)) pkg.Logger.Info("[managed-job] [run] jar successfully ran", zap.Any("run-jar-resp", runJarResp))
@@ -78,23 +78,26 @@ func (job *ManagedJob) run(restoreMode bool) error {
shouldUpload = false shouldUpload = false
continue continue
} }
return nil
} }
// job.def.Status.JobId = &runJarResp.JobId // job.def.Status.JobId = &runJarResp.JobId
// job.def.Status.JobStatus = v1alpha1.JobStatusCreating // job.def.Status.JobStatus = v1alpha1.JobStatusCreating
// job.def.Status.Error = nil // job.def.Status.Error = nil
job.crd.Patch(job.def.UID, map[string]interface{}{ statusPatch := map[string]interface{}{
"status": map[string]interface{}{
"jobId": jobId, "jobId": jobId,
"runningJarURI": job.def.Spec.JarURI,
"jobStatus": v1alpha1.JobStatusCreating, "jobStatus": v1alpha1.JobStatusCreating,
"lifeCycleStatus": v1alpha1.LifeCycleStatusRestoring, "lifeCycleStatus": v1alpha1.LifeCycleStatusRestoring,
"lastRestoredSavepointDate": job.def.Status.LastSavepointDate, "lastRestoredSavepointDate": job.def.Status.LastSavepointDate,
"restoredCount": job.def.Status.RestoredCount + 1, "restoredCount": job.def.Status.RestoredCount + 1,
"lastRestoredSavepointRestoredDate": time.Now().Format(time.RFC3339), "lastRestoredSavepointRestoredDate": time.Now().Format(time.RFC3339),
"error": nil, "error": nil,
}, }
refKey, refVal := job.def.Spec.RunningRefPatchData()
statusPatch[refKey] = refVal
job.crd.Patch(job.def.UID, map[string]interface{}{
"status": statusPatch,
}) })
return nil return nil

View File

@@ -17,14 +17,14 @@ func (job ManagedJob) createSavepoint() error {
pkg.Logger.Debug("[managed-job] [savepoint] no job id") pkg.Logger.Debug("[managed-job] [savepoint] no job id")
return v1alpha1.ErrNoJobId return v1alpha1.ErrNoJobId
} }
pkg.Logger.Info("[managed-job] [savepoint] creating savepoint", zap.String("interval", job.def.Spec.SavepointInterval.String())) pkg.Logger.Info("[managed-job] [savepoint] creating savepoint", zap.String("name", job.def.GetName()), zap.String("interval", job.def.Spec.SavepointInterval.String()))
resp, err := job.client.SavePoints(*job.def.Status.JobId, os.Getenv("SAVEPOINT_PATH"), false) resp, err := job.client.SavePoints(*job.def.Status.JobId, os.Getenv("SAVEPOINT_PATH"), false)
if err != nil { if err != nil {
pkg.Logger.Error("[managed-job] [savepoint] error in creating savepoint", zap.Error(err)) pkg.Logger.Error("[managed-job] [savepoint] error in creating savepoint", zap.Error(err))
return err return err
} }
pkg.Logger.Debug("[managed-job] [savepoint] savepoint created successfully", zap.String("trigger-id", resp.RequestID)) pkg.Logger.Debug("[managed-job] [savepoint] savepoint created successfully", zap.String("trigger-id", resp.RequestID))
job.def.Status.SavepointTriggerId = &resp.RequestID
job.crd.Patch(job.def.UID, map[string]interface{}{ job.crd.Patch(job.def.UID, map[string]interface{}{
"status": map[string]interface{}{ "status": map[string]interface{}{
"savepointTriggerId": resp.RequestID, "savepointTriggerId": resp.RequestID,
@@ -80,3 +80,16 @@ func (job ManagedJob) trackSavepoint() error {
return nil return nil
} }
func (job ManagedJob) TriggerSavepoint() error {
err := job.createSavepoint()
if err != nil {
return err
}
err = job.trackSavepoint()
return err
}
func (job ManagedJob) GetLastSavepointPath() *string {
return job.def.Status.LastSavepointPath
}

View File

@@ -7,10 +7,13 @@ import (
) )
func (job *ManagedJob) upgrade() { func (job *ManagedJob) upgrade() {
effectiveRef := job.def.Spec.EffectiveJarRef()
prevRef := job.def.Status.RunningRef()
pkg.Logger.Info("[managed-job] [upgrade] pausing...", pkg.Logger.Info("[managed-job] [upgrade] pausing...",
zap.String("jobName", job.def.GetName()), zap.String("jobName", job.def.GetName()),
zap.String("currentJarURI", job.def.Spec.JarURI), zap.String("currentRef", effectiveRef),
zap.String("prevJarURI", *job.def.Status.RunningJarURI), zap.String("prevRef", prevRef),
) )
job.def.Status.JarId = nil job.def.Status.JarId = nil
job.crd.Patch(job.def.UID, map[string]interface{}{ job.crd.Patch(job.def.UID, map[string]interface{}{
@@ -18,19 +21,18 @@ func (job *ManagedJob) upgrade() {
"jarId": job.def.Status.JarId, "jarId": job.def.Status.JarId,
}, },
}) })
err := job.pause() err := job.Pause()
if err != nil { if err != nil {
pkg.Logger.Error("[managed-job] [upgrade] error in pausing", zap.Error(err)) pkg.Logger.Error("[managed-job] [upgrade] error in pausing", zap.Error(err))
return return
} }
pkg.Logger.Info("[managed-job] [upgrade] restoring...", pkg.Logger.Info("[managed-job] [upgrade] restoring...",
zap.String("jobName", job.def.GetName()), zap.String("jobName", job.def.GetName()),
zap.String("currentJarURI", job.def.Spec.JarURI), zap.String("currentRef", effectiveRef),
zap.String("prevJarURI", *job.def.Status.RunningJarURI), zap.String("prevRef", prevRef),
zap.Error(err),
) )
err = job.run(true) err = job.Run(true)
if err != nil { if err != nil {
pkg.Logger.Error("[managed-job] [upgrade] error in running", zap.Error(err)) pkg.Logger.Error("[managed-job] [upgrade] error in running", zap.Error(err))
return return

View File

@@ -1,32 +0,0 @@
package managed_job
import (
"flink-kube-operator/internal/jar"
"flink-kube-operator/pkg"
"go.uber.org/zap"
)
// upload jar file and set the jarId for later usages
func (job *ManagedJob) upload() error {
jarFile, err := jar.NewJarFile(job.def.Spec.JarURI)
if err != nil {
pkg.Logger.Debug("[manage-job] [upload] error on download jar", zap.Error(err))
return err
}
jarId, err := jarFile.Upload(job.client)
if err != nil {
pkg.Logger.Debug("[manage-job] [upload] error on upload jar", zap.Error(err))
return err
}
pkg.Logger.Info("[manage-job] [upload] uploaded", zap.Any("upload-jar-resp", jarId))
job.def.Status.JarId = &jarId
job.crd.Patch(job.def.UID, map[string]interface{}{
"status": map[string]interface{}{
"jarId": job.def.Status.JarId,
},
})
return nil
}

46
internal/rest/base.go Normal file
View File

@@ -0,0 +1,46 @@
package rest
import (
"fmt"
"log"
"github.com/danielgtaylor/huma/v2"
humaFiber "github.com/danielgtaylor/huma/v2/adapters/humafiber"
"github.com/gofiber/fiber/v2"
)
func Init() {
app := fiber.New()
app.Use(func(c *fiber.Ctx) error {
// Logic to execute before the next handler
fmt.Printf("Request Method: %s, URL: %s\n", c.Method(), c.OriginalURL())
// Call the next handler in the stack
err := c.Next()
// Logic to execute after the next handler
fmt.Println("Request completed")
return err
})
config := huma.DefaultConfig("Go API", "1.0.0")
config.Servers = []*huma.Server{{}}
config.Components.SecuritySchemes = map[string]*huma.SecurityScheme{
"auth": {
Type: "http",
Scheme: "bearer",
BearerFormat: "JWT",
},
}
api := humaFiber.New(app, config)
api.UseMiddleware(
func(ctx huma.Context, next func(huma.Context)) {
ctx = huma.WithValue(ctx, "humaContext", ctx)
next(ctx)
},
)
initRouter(api)
log.Fatal(app.Listen(fmt.Sprintf(":%s", "3000")))
}

View File

@@ -0,0 +1,253 @@
package controller
import (
"archive/zip"
"bufio"
"context"
"errors"
"flink-kube-operator/internal/crd"
"flink-kube-operator/internal/crd/v1alpha1"
"flink-kube-operator/internal/managed_job"
"flink-kube-operator/internal/rest/types"
"flink-kube-operator/pkg"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/danielgtaylor/huma/v2"
"go.uber.org/zap"
k8sTypes "k8s.io/apimachinery/pkg/types"
)
type GetJobsReq struct {
}
type GetJobsResp struct {
Body []v1alpha1.FlinkJob
}
func GetJobs(ctx context.Context, req *GetJobsReq) (*GetJobsResp, error) {
jobs := []v1alpha1.FlinkJob{}
for _, key := range crd.GetAllJobKeys() {
job := crd.GetJob(key)
job.ManagedFields = nil
jobs = append(jobs, job)
}
return &GetJobsResp{Body: jobs}, nil
}
type StopJobReq struct {
JobUId string `path:"uid"`
}
type StopJobRespBody struct {
Success bool `json:"success"`
}
type StopJobResp struct {
Body StopJobRespBody
}
func StopJob(ctx context.Context, req *StopJobReq) (*StopJobResp, error) {
mgr := managed_job.GetManager()
job := mgr.GetJob(k8sTypes.UID(req.JobUId))
err := job.Stop()
if err != nil {
return nil, err
}
return &StopJobResp{Body: StopJobRespBody{
Success: true,
}}, nil
}
func StartJob(ctx context.Context, req *StopJobReq) (*StopJobResp, error) {
mgr := managed_job.GetManager()
job := mgr.GetJob(k8sTypes.UID(req.JobUId))
err := job.Run(false)
if err != nil {
return nil, err
}
return &StopJobResp{Body: StopJobRespBody{
Success: true,
}}, nil
}
func ResumeJob(ctx context.Context, req *StopJobReq) (*StopJobResp, error) {
mgr := managed_job.GetManager()
job := mgr.GetJob(k8sTypes.UID(req.JobUId))
err := job.Run(true)
if err != nil {
return nil, err
}
return &StopJobResp{Body: StopJobRespBody{
Success: true,
}}, nil
}
func RemoveJobJar(ctx context.Context, req *StopJobReq) (*StopJobResp, error) {
mgr := managed_job.GetManager()
job := mgr.GetJob(k8sTypes.UID(req.JobUId))
job.RemoveJar()
return &StopJobResp{Body: StopJobRespBody{
Success: true,
}}, nil
}
func PauseJob(ctx context.Context, req *StopJobReq) (*StopJobResp, error) {
mgr := managed_job.GetManager()
job := mgr.GetJob(k8sTypes.UID(req.JobUId))
job.Pause()
return &StopJobResp{Body: StopJobRespBody{
Success: true,
}}, nil
}
type JobTriggerSavepointReq struct {
JobUId string `path:"uid"`
}
type JobTriggerSavepointRespBody struct {
Success bool `json:"success"`
}
type JobTriggerSavepointResp struct {
Body JobTriggerSavepointRespBody
}
func TriggerSavepoint(ctx context.Context, req *JobTriggerSavepointReq) (*JobTriggerSavepointResp, error) {
mgr := managed_job.GetManager()
job := mgr.GetJob(k8sTypes.UID(req.JobUId))
err := job.TriggerSavepoint()
if err != nil {
return nil, err
}
return &JobTriggerSavepointResp{Body: JobTriggerSavepointRespBody{
Success: true,
}}, nil
}
func DownloadSavepoint(ctx context.Context, req *types.SavepointDownloadReq) (*huma.StreamResponse, error) {
mgr := managed_job.GetManager()
job := mgr.GetJob(k8sTypes.UID(req.JobUID))
lastSavepointPath := job.GetLastSavepointPath()
if lastSavepointPath == nil {
return nil, huma.Error404NotFound("there is no savepoint path is registered for the job")
}
folderPath := strings.TrimLeft(*lastSavepointPath, "file:") // Change this to your folder path
// Create a temporary zip file
zipFilePath, err := filepath.Abs("./savepoint.zip")
pkg.Logger.Debug("[controller] [savepoint]",
zap.String("zipFileName", zipFilePath),
zap.String("folderPath", folderPath),
)
// Create the zip file
zipFile, err := os.Create(zipFilePath)
if err != nil {
fmt.Println("Error creating zip file:", err)
return nil, nil
}
defer zipFile.Close()
// Create a new zip writer
zipWriter := zip.NewWriter(zipFile)
defer zipWriter.Close()
// Walk through the source directory and add files to the zip
err = filepath.Walk(folderPath, func(filePath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Create a new file header
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
// Set the header name to the relative path
header.Name, err = filepath.Rel(folderPath, filePath)
if err != nil {
return err
}
// If it's a directory, add a trailing slash
if info.IsDir() {
header.Name += "/"
} else {
// Set the compression method
header.Method = zip.Deflate
}
// Create a new writer for the file
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
// If it's a directory, we're done
if info.IsDir() {
return nil
}
// Open the file to be zipped
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
// Copy the file content to the zip writer
_, err = io.Copy(writer, file)
return err
})
// Open the zip file for reading
zipFileReader, err := os.Open(zipFilePath)
if err != nil {
return nil, fmt.Errorf("failed to open zip file: %w", err)
}
//defer zipFileReader.Close()
fileInfo, err := zipFileReader.Stat()
if err != nil {
return nil, fmt.Errorf("failed to get info of zipped file: %w", err)
}
resp := &huma.StreamResponse{
Body: func(ctx huma.Context) {
ctx.SetHeader("Content-Type", "application/zip")
ctx.SetHeader("Content-Length", fmt.Sprintf("%d", fileInfo.Size()))
ctx.SetHeader("Content-Disposition", fmt.Sprintf("attachment; filename=%s", zipFilePath))
writer := ctx.BodyWriter()
br := bufio.NewReader(zipFileReader)
for {
b, err := br.ReadByte()
if err != nil && !errors.Is(err, io.EOF) {
fmt.Println(err)
break
}
// process the one byte b
if err != nil {
// end of file
break
}
writer.Write([]byte{b})
}
os.Remove(zipFilePath)
},
}
return resp, nil
}

82
internal/rest/router.go Normal file
View File

@@ -0,0 +1,82 @@
package rest
import (
"flink-kube-operator/internal/rest/controller"
"net/http"
"github.com/danielgtaylor/huma/v2"
)
func initRouter(api huma.API) {
huma.Register(api, huma.Operation{
OperationID: "get-jobs",
Method: http.MethodGet,
Path: "/jobs",
Summary: "Get Jobs",
Description: "Get Flink Jobs",
Tags: []string{"Job"},
}, controller.GetJobs)
huma.Register(api, huma.Operation{
OperationID: "stop-job",
Method: http.MethodPost,
Path: "/jobs/{uid}/stop",
Summary: "Stop Job",
Description: "Stop Flink Job",
Tags: []string{"Job"},
}, controller.StopJob)
huma.Register(api, huma.Operation{
OperationID: "start-job",
Method: http.MethodPost,
Path: "/jobs/{uid}/start",
Summary: "Start Job",
Description: "Start Flink Job",
Tags: []string{"Job"},
}, controller.StartJob)
huma.Register(api, huma.Operation{
OperationID: "resume-job",
Method: http.MethodPost,
Path: "/jobs/{uid}/resume",
Summary: "Resume Job",
Description: "Resume Flink Job",
Tags: []string{"Job"},
}, controller.ResumeJob)
huma.Register(api, huma.Operation{
OperationID: "remove-jar",
Method: http.MethodPost,
Path: "/jobs/{uid}/remove-jar",
Summary: "Remove Job Jar",
Description: "Remove Flink Job Jar",
Tags: []string{"Job"},
}, controller.RemoveJobJar)
huma.Register(api, huma.Operation{
OperationID: "pause-job",
Method: http.MethodPost,
Path: "/jobs/{uid}/pause",
Summary: "Pause Job",
Description: "Pause Flink Job",
Tags: []string{"Job"},
}, controller.PauseJob)
huma.Register(api, huma.Operation{
OperationID: "download-savepoint",
Method: http.MethodGet,
Path: "/savepoint/download",
Summary: "Download Savepoint",
Description: "Download Savepoint",
Tags: []string{"Savepoint"},
}, controller.DownloadSavepoint)
huma.Register(api, huma.Operation{
OperationID: "trigger-savepoint",
Method: http.MethodPost,
Path: "/jobs/{uid}/trigger-savepoint",
Summary: "Trigger Savepoint",
Description: "Trigger Savepoint",
Tags: []string{"Savepoint"},
}, controller.TriggerSavepoint)
}

View File

@@ -0,0 +1,12 @@
package types
type SavepointDownloadReq struct {
JobUID string `query:"jobUID"`
}
type SavepointDownloadResp struct {
ContentType string `header:"Content-Type"`
ContentDisposition string `header:"Content-Disposition"`
ContentLength string `header:"Content-Length"`
Body []byte
}

View File

@@ -2,6 +2,8 @@ package pkg
import ( import (
"context" "context"
"os"
"strconv"
"github.com/mattn/go-colorable" "github.com/mattn/go-colorable"
"go.uber.org/zap" "go.uber.org/zap"
@@ -82,8 +84,10 @@ func OverrideLoggerConfig(config LoggerConfig) {
Logger = createOrUpdateInstance(config) Logger = createOrUpdateInstance(config)
} }
var level, err = strconv.Atoi(os.Getenv("LOG_LEVEL"))
var Logger = GetLogger(context.Background(), LoggerConfig{ var Logger = GetLogger(context.Background(), LoggerConfig{
Level: zap.DebugLevel, Level: zapcore.Level(level),
Filename: "./tmp/error.log", Filename: "./tmp/error.log",
MaxSize: 100, MaxSize: 100,
MaxAge: 90, MaxAge: 90,

58
scripts/build-flink-images.sh Executable file
View File

@@ -0,0 +1,58 @@
#!/bin/bash
set -euo pipefail
REGISTRY="${REGISTRY:-lcr.logicamp.tech/library/flink}"
echo "=== Building Flink 2.3.0 image (default) ==="
docker build \
--build-arg FLINK_VERSION=2.3.0 \
--build-arg JAVA_VERSION=21 \
--build-arg KAFKA_CONNECTOR_VERSION=5.0.0-2.2 \
--build-arg AVRO_VERSION=2.2.1 \
--build-arg AVRO_REGISTRY_VERSION=2.2.1 \
--build-arg PG_CDC_VERSION=3.6.0-2.2 \
--build-arg JDBC_DRIVER_VERSION=2.2.1 \
--build-arg JDBC_CONNECTOR_VERSION=3.3.0-1.20 \
--build-arg S3_FS_VERSION=2.2.1 \
-f Dockerfile.flink \
-t "${REGISTRY}:2.3.0-scala_2.12-java21-minicluster" \
.
echo ""
echo "=== Building Flink 2.2.1 image ==="
docker build \
--build-arg FLINK_VERSION=2.2.1 \
--build-arg JAVA_VERSION=17 \
--build-arg KAFKA_CONNECTOR_VERSION=5.0.0-2.2 \
--build-arg AVRO_VERSION=2.2.1 \
--build-arg AVRO_REGISTRY_VERSION=2.2.1 \
--build-arg PG_CDC_VERSION=3.6.0-2.2 \
--build-arg JDBC_DRIVER_VERSION=2.2.1 \
--build-arg JDBC_CONNECTOR_VERSION=3.3.0-1.20 \
--build-arg S3_FS_VERSION=2.2.1 \
-f Dockerfile.flink \
-t "${REGISTRY}:2.2.1-scala_2.12-java17-minicluster" \
.
echo ""
echo "=== Building Flink 1.20.1 image (backward compat) ==="
docker build \
--build-arg FLINK_VERSION=1.20.1 \
--build-arg JAVA_VERSION=17 \
--build-arg KAFKA_CONNECTOR_VERSION=3.4.0-1.20 \
--build-arg AVRO_VERSION=1.20.1 \
--build-arg AVRO_REGISTRY_VERSION=1.20.1 \
--build-arg PG_CDC_VERSION=3.2.1 \
--build-arg JDBC_DRIVER_VERSION=1.20.1 \
--build-arg JDBC_CONNECTOR_VERSION=3.2.0-1.19 \
--build-arg S3_FS_VERSION=1.20.1 \
-f Dockerfile.flink \
-t "${REGISTRY}:1.20.1-scala_2.12-java17-minicluster" \
.
echo ""
echo "=== Done ==="
echo "Images built:"
echo " ${REGISTRY}:2.3.0-scala_2.12-java21-minicluster"
echo " ${REGISTRY}:2.2.1-scala_2.12-java17-minicluster"
echo " ${REGISTRY}:1.20.1-scala_2.12-java17-minicluster"

0
start-cluster.sh Normal file → Executable file
View File