diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b203912150..5c0605da2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -236,6 +236,15 @@ jobs: - uses: cashapp/activate-hermit@v1.1.3 - uses: ./.github/actions/build-cache - run: just build-docker provisioner + docker-build-cron: + name: Build Cron Docker Image + # if: github.event_name != 'pull_request' || github.event.action == 'enqueued' || contains( github.event.pull_request.labels.*.name, 'run-all') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: cashapp/activate-hermit@v1.1.3 + - uses: ./.github/actions/build-cache + - run: just build-docker cron docker-build-runners: name: Build Runner Docker Images # if: github.event_name != 'pull_request' || github.event.action == 'enqueued' || contains( github.event.pull_request.labels.*.name, 'run-all') diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2b4c714be6..0b8bd4b4dd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -72,6 +72,25 @@ jobs: name: docker-provisioner-artifact path: artifacts/ftl-provisioner retention-days: 1 + build-cron: + name: Build Cron Docker Image + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Init Hermit + uses: cashapp/activate-hermit@v1.1.3 + - name: Build + run: | + just build-docker cron + mkdir -p artifacts/ftl-provisioner + docker save -o artifacts/ftl-cron/ftl-cron.tar ftl0/ftl-cron:latest + - name: Temporarily save Docker image + uses: actions/upload-artifact@v4 + with: + name: docker-cron-artifact + path: artifacts/ftl-cron + retention-days: 1 build-box: name: Build FTL-in-a-box Docker Image runs-on: ubuntu-latest @@ -125,6 +144,11 @@ jobs: with: name: docker-provisioner-artifact path: artifacts/ftl-provisioner + - name: Retrieve Cron Docker image + uses: actions/download-artifact@v4 + with: + name: docker-cron-artifact + path: artifacts/ftl-cron - name: Retrieve FTL-in-a-box Docker image uses: actions/download-artifact@v4 with: @@ -138,6 +162,8 @@ jobs: run: docker load -i artifacts/ftl-controller/ftl-controller.tar - name: Load Provisioner Docker image run: docker load -i artifacts/ftl-provisioner/ftl-provisioner.tar + - name: Load Cron Docker image + run: docker load -i artifacts/ftl-cron/ftl-cron.tar - name: Load FTL-in-a-box Docker image run: docker load -i artifacts/ftl-box/ftl-box.tar - name: Log in to the Container registry @@ -160,6 +186,9 @@ jobs: docker tag ftl0/ftl-provisioner:latest ftl0/ftl-provisioner:"$GITHUB_SHA" docker tag ftl0/ftl-provisioner:latest ftl0/ftl-provisioner:"$version" docker push -a ftl0/ftl-provisioner + docker tag ftl0/ftl-cron:latest ftl0/ftl-cron:"$GITHUB_SHA" + docker tag ftl0/ftl-cron:latest ftl0/ftl-cron:"$version" + docker push -a ftl0/ftl-cron docker tag ftl0/ftl-box:latest ftl0/ftl-box:"$GITHUB_SHA" docker tag ftl0/ftl-box:latest ftl0/ftl-box:"$version" docker push -a ftl0/ftl-box diff --git a/Dockerfile.cron b/Dockerfile.cron new file mode 100644 index 0000000000..674900415e --- /dev/null +++ b/Dockerfile.cron @@ -0,0 +1,44 @@ +FROM ubuntu:24.04 AS builder +RUN apt-get update +RUN apt-get install -y curl git zip + +# Copy Hermit bin stubs and install all packages. This is done +# separately so that Docker will cache the tools correctly. +COPY ./bin /src/bin +ENV PATH="/src/bin:$PATH" +WORKDIR /src + +# Seed some of the most common tools - this will be cached +RUN go version +RUN node --version + +# Download Go dependencies separately so Docker will cache them +COPY go.mod go.sum ./ +RUN go mod download -x + +# Download PNPM dependencies separately so Docker will cache them +COPY frontend/console/package.json ./frontend/console/ +COPY frontend/vscode/package.json ./frontend/vscode/ +COPY pnpm-workspace.yaml pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +# Build +COPY . /src/ +RUN just errtrace +# Reset timestamps so that the build state is reset +RUN git ls-files -z | xargs -0 touch -r go.mod +RUN just build ftl-cron + +# Finally create the runtime image. +FROM scratch + +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +WORKDIR /plugins/ + +WORKDIR /service/ +COPY --from=builder /src/build/release/ftl-cron . + +EXPOSE 8893 + +CMD ["/service/ftl-cron"] diff --git a/backend/cron/service.go b/backend/cron/service.go index 8ce60c5473..6ac91c19e0 100644 --- a/backend/cron/service.go +++ b/backend/cron/service.go @@ -3,6 +3,7 @@ package cron import ( "context" "fmt" + "net/url" "sort" "time" @@ -37,6 +38,10 @@ type cronJob struct { next time.Time } +type Config struct { + ControllerEndpoint *url.URL `name:"ftl-endpoint" help:"Controller endpoint." env:"FTL_ENDPOINT" default:"http://127.0.0.1:8892"` +} + func (c cronJob) String() string { desc := fmt.Sprintf("%s.%s (%s)", c.module, c.verb.Name, c.pattern) var next string diff --git a/charts/ftl/templates/_helpers.tpl b/charts/ftl/templates/_helpers.tpl index f4479221ac..6cd2d181d5 100644 --- a/charts/ftl/templates/_helpers.tpl +++ b/charts/ftl/templates/_helpers.tpl @@ -46,4 +46,9 @@ app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/name: {{ include "ftl.fullname" . }} app.kubernetes.io/component: provisioner app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} +{{- define "ftl-cron.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ftl.fullname" . }} +app.kubernetes.io/component: cron +app.kubernetes.io/instance: {{ .Release.Name }} {{- end -}} \ No newline at end of file diff --git a/charts/ftl/templates/cron-role.yaml b/charts/ftl/templates/cron-role.yaml new file mode 100644 index 0000000000..5ba7627455 --- /dev/null +++ b/charts/ftl/templates/cron-role.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Values.cron.serviceAccountName }} + namespace: {{ .Release.Namespace }} + {{- if .Values.cron.cronsRoleArn }} + annotations: + eks.amazonaws.com/role-arn: {{ .Values.cron.cronsRoleArn }} + {{- end }} diff --git a/charts/ftl/templates/cron.yaml b/charts/ftl/templates/cron.yaml new file mode 100644 index 0000000000..5c5eddc16e --- /dev/null +++ b/charts/ftl/templates/cron.yaml @@ -0,0 +1,53 @@ +{{ $version := printf "v%s" .Chart.Version -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ftl.fullname" . }}-cron + labels: + {{- include "ftl.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.cron.replicas }} + revisionHistoryLimit: {{ .Values.cron.revisionHistoryLimit }} + selector: + matchLabels: + {{- include "ftl-cron.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "ftl-cron.selectorLabels" . | nindent 8 }} + {{- if .Values.cron.podAnnotations }} + annotations: + {{- toYaml .Values.cron.podAnnotations | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ .Values.cron.serviceAccountName }} + containers: + - name: app + image: "{{ .Values.cron.image.repository }}:{{ .Values.cron.image.tag | default $version }}" + imagePullPolicy: {{ .Values.cron.image.pullPolicy }} + {{- if .Values.cron.envFrom }} + envFrom: + {{- if .Values.cron.envFrom }} + {{- toYaml .Values.cron.envFrom | nindent 12 }} + {{- end }} + {{- end }} + env: + {{- if .Values.cron.env }} + {{- toYaml .Values.cron.env | nindent 12 }} + {{- end }} + {{- if .Values.cron.nodeSelector }} + nodeSelector: + {{- toYaml .Values.cron.nodeSelector | nindent 8 }} + {{- end }} + {{- if .Values.cron.affinity }} + affinity: + {{- toYaml .Values.cron.affinity | nindent 8 }} + {{- end }} + {{- if .Values.cron.topologySpreadConstraints }} + topologySpreadConstraints: + {{- toYaml .Values.cron.topologySpreadConstraints | nindent 8 }} + {{- end }} + {{- if .Values.cron.tolerations }} + tolerations: + {{- toYaml .Values.cron.tolerations | nindent 8 }} + {{- end }} \ No newline at end of file diff --git a/charts/ftl/templates/ingress.yaml b/charts/ftl/templates/ingress.yaml index d204a35c3c..715ed42dad 100644 --- a/charts/ftl/templates/ingress.yaml +++ b/charts/ftl/templates/ingress.yaml @@ -24,7 +24,6 @@ spec: name: ftl-controller-ingress port: number: 8891 - {{- if .Values.provisioner.enabled }} - path: /xyz.block.ftl.v1beta1.provisioner.ProvisionerService/ pathType: Prefix backend: @@ -32,7 +31,6 @@ spec: name: ftl-provisioner port: number: 8893 - {{- end }} {{- range $host := .Values.ingress.hosts }} - host: "{{ $host.host }}" http: diff --git a/charts/ftl/templates/provisioner-config-map.yaml b/charts/ftl/templates/provisioner-config-map.yaml index 50faeda7e9..8994254d5e 100644 --- a/charts/ftl/templates/provisioner-config-map.yaml +++ b/charts/ftl/templates/provisioner-config-map.yaml @@ -1,4 +1,3 @@ -{{- if .Values.provisioner.enabled }} {{- if eq .Values.provisioner.configMap "ftl-provisioner-default-config" }} apiVersion: v1 kind: ConfigMap @@ -12,5 +11,4 @@ data: { id = "cloudformation", resources = ["postgres"] }, { id = "controller", resources = ["module"] }, ] -{{- end}} {{- end}} \ No newline at end of file diff --git a/charts/ftl/templates/provisioner-services.yaml b/charts/ftl/templates/provisioner-services.yaml index 090e356040..a734fe60b7 100644 --- a/charts/ftl/templates/provisioner-services.yaml +++ b/charts/ftl/templates/provisioner-services.yaml @@ -1,4 +1,3 @@ -{{- if .Values.provisioner.enabled }} apiVersion: v1 kind: Service metadata: @@ -22,5 +21,4 @@ spec: {{- end }} selector: {{- include "ftl-provisioner.selectorLabels" . | nindent 4 }} - type: {{ .Values.provisioner.service.type | default "ClusterIP" }} -{{- end }} \ No newline at end of file + type: {{ .Values.provisioner.service.type | default "ClusterIP" }} \ No newline at end of file diff --git a/charts/ftl/templates/provisioner.yaml b/charts/ftl/templates/provisioner.yaml index fc970390c5..c0fa72c6c0 100644 --- a/charts/ftl/templates/provisioner.yaml +++ b/charts/ftl/templates/provisioner.yaml @@ -1,4 +1,3 @@ -{{- if .Values.provisioner.enabled }} {{ $version := printf "v%s" .Chart.Version -}} apiVersion: apps/v1 kind: Deployment @@ -81,5 +80,4 @@ spec: {{- if .Values.provisioner.tolerations }} tolerations: {{- toYaml .Values.provisioner.tolerations | nindent 8 }} - {{- end }} -{{- end }} \ No newline at end of file + {{- end }} \ No newline at end of file diff --git a/charts/ftl/values.yaml b/charts/ftl/values.yaml index 1cc7be4b44..422544f371 100644 --- a/charts/ftl/values.yaml +++ b/charts/ftl/values.yaml @@ -103,7 +103,6 @@ controller: provisioner: provisionersRoleArn: arn:aws:iam::ftl-provisioners-irsa-role - enabled: false replicas: 1 revisionHistoryLimit: 0 configMap: "ftl-provisioner-default-config" @@ -131,7 +130,6 @@ provisioner: valueFrom: fieldRef: fieldPath: status.hostIP - ports: - name: http containerPort: 8893 @@ -211,6 +209,25 @@ runner: topologySpreadConstraints: null tolerations: null + +cron: + replicas: 1 + revisionHistoryLimit: 0 + image: + repository: "ftl0/ftl-cron" + pullPolicy: IfNotPresent + + envFrom: null + serviceAccountName: ftl-cron + + env: + - name: FTL_ENDPOINT + value: "http://ftl-controller:8892" + - name: LOG_LEVEL + value: "debug" + - name: LOG_JSON + value: "true" + postgresql: enabled: true architecture: standalone diff --git a/cmd/ftl-cron/main.go b/cmd/ftl-cron/main.go new file mode 100644 index 0000000000..7e26d76e8d --- /dev/null +++ b/cmd/ftl-cron/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "context" + "fmt" + "os" + "strconv" + "time" + + "github.com/alecthomas/kong" + + "github.com/TBD54566975/ftl" + "github.com/TBD54566975/ftl/backend/cron" + "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect" + _ "github.com/TBD54566975/ftl/internal/automaxprocs" // Set GOMAXPROCS to match Linux container CPU quota. + "github.com/TBD54566975/ftl/internal/log" + "github.com/TBD54566975/ftl/internal/observability" + "github.com/TBD54566975/ftl/internal/rpc" +) + +var cli struct { + Version kong.VersionFlag `help:"Show version."` + ObservabilityConfig observability.Config `embed:"" prefix:"o11y-"` + LogConfig log.Config `embed:"" prefix:"log-"` + CronConfig cron.Config `embed:""` + ConfigFlag string `name:"config" short:"C" help:"Path to FTL project cf file." env:"FTL_CONFIG" placeholder:"FILE"` +} + +func main() { + t, err := strconv.ParseInt(ftl.Timestamp, 10, 64) + if err != nil { + panic(fmt.Sprintf("invalid timestamp %q: %s", ftl.Timestamp, err)) + } + kctx := kong.Parse(&cli, + kong.Description(`FTL - Cron`), + kong.UsageOnError(), + kong.Vars{"version": ftl.Version, "timestamp": time.Unix(t, 0).Format(time.RFC3339)}, + ) + + ctx := log.ContextWithLogger(context.Background(), log.Configure(os.Stderr, cli.LogConfig)) + err = observability.Init(ctx, false, "", "ftl-cron", ftl.Version, cli.ObservabilityConfig) + kctx.FatalIfErrorf(err, "failed to initialize observability") + + verbClient := rpc.Dial(ftlv1connect.NewVerbServiceClient, cli.CronConfig.ControllerEndpoint.String(), log.Error) + schemaClient := rpc.Dial(ftlv1connect.NewSchemaServiceClient, cli.CronConfig.ControllerEndpoint.String(), log.Error) + + err = cron.Start(ctx, schemaClient, verbClient) + kctx.FatalIfErrorf(err, "failed to start provisioner") +} diff --git a/deployment/Dockerfile.cron.test b/deployment/Dockerfile.cron.test new file mode 100644 index 0000000000..7dacd4e876 --- /dev/null +++ b/deployment/Dockerfile.cron.test @@ -0,0 +1,8 @@ +FROM ubuntu:24.04 + +WORKDIR /root/ + +COPY docker-build/ftl-cron . +EXPOSE 8893 + +CMD ["/root/ftl-cron"] diff --git a/deployment/Justfile b/deployment/Justfile index 69f9f180fe..4fc98e8312 100755 --- a/deployment/Justfile +++ b/deployment/Justfile @@ -17,7 +17,7 @@ start: setup full-deploy rm: teardown -full-deploy: build-controller build-runners build-provisioner setup-istio-cluster +full-deploy: build-controller build-runners build-provisioner setup-istio-cluster build-cron #!/bin/bash kubectl rollout restart deployment ftl-controller || true # if this exists already restart it to get the latest image just apply || sleep 5 # wait for CRDs to be created, the initial apply will usually fail @@ -116,7 +116,7 @@ build-executables: # it is way faster than building in the docker files java -version #make sure hermit has downloaded Java mkdir -p "docker-build" - cd ../ && GOARCH=amd64 GOOS=linux CGO_ENABLED=0 just build ftl-controller ftl-runner ftl-initdb ftl ftl-provisioner ftl-provisioner-cloudformation + cd ../ && GOARCH=amd64 GOOS=linux CGO_ENABLED=0 just build ftl-controller ftl-runner ftl-initdb ftl ftl-provisioner ftl-provisioner-cloudformation ftl-cron cp ../build/release/* ./docker-build/ build-controller: build-executables setup-registry setup-istio-cluster @@ -138,7 +138,12 @@ build-provisioner: build-executables setup-registry setup-istio-cluster docker tag ftl-provisioner:latest {{registry_local}}/ftl-provisioner:latest docker push {{registry_local}}/ftl-provisioner:latest -build: build-controller build-runners build-provisioner +build-cron: build-executables setup-registry setup-istio-cluster + docker build --platform linux/amd64 -t ftl-cron:latest -f Dockerfile.cron.test . + docker tag ftl-cron:latest {{registry_local}}/ftl-cron:latest + docker push {{registry_local}}/ftl-cron:latest + +build: build-controller build-runners build-provisioner build-cron deploy path: #!/usr/bin/env bash diff --git a/deployment/values.yaml b/deployment/values.yaml index 8e4230e593..d64c7390cf 100644 --- a/deployment/values.yaml +++ b/deployment/values.yaml @@ -29,7 +29,6 @@ runner: sidecar.istio.io/logLevel: "debug" provisioner: - enabled: true image: repository: "ftl:5000/ftl-provisioner" tag: "latest" @@ -38,6 +37,11 @@ provisioner: ports: - name: "http-8893" port: 8893 +cron: + image: + repository: "ftl:5000/ftl-cron" + tag: "latest" + pullPolicy: Always istio: enabled: true diff --git a/frontend/cli/cmd_dev.go b/frontend/cli/cmd_dev.go index a8b3ee6efd..2127a49d71 100644 --- a/frontend/cli/cmd_dev.go +++ b/frontend/cli/cmd_dev.go @@ -47,6 +47,7 @@ func (d *devCmd) Run( schemaClient ftlv1connect.SchemaServiceClient, controllerClient ftlv1connect.ControllerServiceClient, provisionerClient provisionerconnect.ProvisionerServiceClient, + verbClient ftlv1connect.VerbServiceClient, ) error { startTime := time.Now() if len(d.Build.Dirs) == 0 { @@ -94,7 +95,7 @@ func (d *devCmd) Run( controllerReady := make(chan bool, 1) if !d.NoServe { if d.ServeCmd.Stop { - err := d.ServeCmd.run(ctx, projConfig, cm, sm, optional.Some(controllerReady), true, bindAllocator, controllerClient, provisionerClient, schemaClient, true, nil) + err := d.ServeCmd.run(ctx, projConfig, cm, sm, optional.Some(controllerReady), true, bindAllocator, controllerClient, provisionerClient, schemaClient, verbClient, true, nil) if err != nil { return fmt.Errorf("failed to stop server: %w", err) } @@ -102,7 +103,7 @@ func (d *devCmd) Run( } g.Go(func() error { - return d.ServeCmd.run(ctx, projConfig, cm, sm, optional.Some(controllerReady), true, bindAllocator, controllerClient, provisionerClient, schemaClient, true, devModeEndpointUpdates) + return d.ServeCmd.run(ctx, projConfig, cm, sm, optional.Some(controllerReady), true, bindAllocator, controllerClient, provisionerClient, schemaClient, verbClient, true, devModeEndpointUpdates) }) } diff --git a/frontend/cli/cmd_serve.go b/frontend/cli/cmd_serve.go index e1eb0ddd4a..b1b2100076 100644 --- a/frontend/cli/cmd_serve.go +++ b/frontend/cli/cmd_serve.go @@ -23,6 +23,7 @@ import ( "github.com/TBD54566975/ftl/backend/controller/artefacts" "github.com/TBD54566975/ftl/backend/controller/scaling" "github.com/TBD54566975/ftl/backend/controller/scaling/localscaling" + "github.com/TBD54566975/ftl/backend/cron" ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1" "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect" "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1beta1/provisioner/provisionerconnect" @@ -73,12 +74,13 @@ func (s *serveCmd) Run( controllerClient ftlv1connect.ControllerServiceClient, provisionerClient provisionerconnect.ProvisionerServiceClient, schemaClient ftlv1connect.SchemaServiceClient, + verbClient ftlv1connect.VerbServiceClient, ) error { bindAllocator, err := bind.NewBindAllocator(s.Bind, 2) if err != nil { return fmt.Errorf("could not create bind allocator: %w", err) } - return s.run(ctx, projConfig, cm, sm, optional.None[chan bool](), false, bindAllocator, controllerClient, provisionerClient, schemaClient, s.Recreate, nil) + return s.run(ctx, projConfig, cm, sm, optional.None[chan bool](), false, bindAllocator, controllerClient, provisionerClient, schemaClient, verbClient, s.Recreate, nil) } //nolint:maintidx @@ -93,6 +95,7 @@ func (s *serveCommonConfig) run( controllerClient ftlv1connect.ControllerServiceClient, provisionerClient provisionerconnect.ProvisionerServiceClient, schemaClient ftlv1connect.SchemaServiceClient, + vervClient ftlv1connect.VerbServiceClient, recreate bool, devModeEndpoints <-chan scaling.DevModeEndpoints, ) error { @@ -273,6 +276,14 @@ func (s *serveCommonConfig) run( }) } + // Start Cron + wg.Go(func() error { + err := cron.Start(ctx, schemaClient, vervClient) + if err != nil { + return fmt.Errorf("cron failed: %w", err) + } + return nil + }) // Wait for controller to start, then run startup commands. start := time.Now() if err := waitForControllerOnline(ctx, s.StartupTimeout, controllerClient); err != nil {