diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..37c614c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,90 @@ +name: Release + +# Tag-driven: the tag IS the version. Nothing in the repo pins a release version, so a release +# needs no prior bump commit -- the tag is stamped into the images, the binary (VERSION -> +# pkg/version) and dist/install.yaml below. workflow_dispatch exists to re-publish a tag whose +# first run failed halfway, without moving the tag. +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: Existing tag to publish + required: true + +permissions: + contents: read + +env: + CONTROLLER_IMAGE: inftyai/nebula-controller + VERSION: ${{ inputs.version || github.ref_name }} + +jobs: + # Both images ship under the same tag. Their names are repeated here rather than taken from env + # because the matrix is evaluated before env exists. + images: + name: Push ${{ matrix.image }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - image: inftyai/nebula-controller + dir: . + target: docker-buildx + - image: inftyai/nebula-logship + dir: components/logship + target: docker-push + steps: + - name: Clone the code + uses: actions/checkout@v5 + + # PLATFORMS carries linux/arm64, which an amd64 runner can only build under emulation. + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # VERSION is passed explicitly: its default is `git describe`, which needs the tag history + # that checkout does not fetch, and would silently stamp a bare commit instead. + - name: Build and push + working-directory: ${{ matrix.dir }} + run: make ${{ matrix.target }} IMG=${{ matrix.image }}:${{ env.VERSION }} VERSION=${{ env.VERSION }} + + release: + name: Publish release + runs-on: ubuntu-latest + needs: images + permissions: + contents: write + steps: + - name: Clone the code + uses: actions/checkout@v5 + + # build-installer runs controller-gen and kustomize through `go install`. + - name: Setup Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + + # Pins the released tag into the manifest, so applying it cannot drift onto a different + # image than the release it came from. + - name: Generate install manifest + run: make build-installer IMG=${{ env.CONTROLLER_IMAGE }}:${{ env.VERSION }} + + # Runs after the images so a published release never points at a tag nobody can pull. + # A pre-release suffix (v0.2.0-rc.1) must not become the repo's "latest" release. + - name: Create release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + prerelease="" + case "$VERSION" in *-*) prerelease="--prerelease" ;; esac + gh release create "$VERSION" dist/install.yaml \ + --title "$VERSION" --generate-notes $prerelease diff --git a/.gitignore b/.gitignore index df2cd4c..3b5f33c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ components/*/bin/ # exactly the kind of thing that sneaks into a commit unnoticed. /manager +# `make build-installer` output, the release artifact — generated, never reviewed. +/dist + # Test binary, build with `go test -c` *.test diff --git a/Makefile b/Makefile index 5c5fcb7..3131973 100644 --- a/Makefile +++ b/Makefile @@ -72,6 +72,20 @@ generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and # the catalog pass LoadRestrictionsNone. KUSTOMIZE_BUILD_FLAGS ?= --load-restrictor=LoadRestrictionsNone +# render-manifests builds an overlay with the manager image pinned to IMG. +# +# `kustomize build` has no image-override flag, so pinning it means EDITING +# config/manager/kustomization.yaml, whose newTag is committed. The trap restores the file +# however the shell exits, so a local deploy leaves no stray version diff to commit by +# accident. One shell for the whole body, or the trap would fire on the first line. +KUSTOMIZATION := $(CURDIR)/config/manager/kustomization.yaml +define render-manifests + cp $(KUSTOMIZATION) $(KUSTOMIZATION).bak && \ + trap 'mv $(KUSTOMIZATION).bak $(KUSTOMIZATION)' EXIT && \ + (cd $(CURDIR)/config/manager && $(KUSTOMIZE) edit set image controller=$(IMG)) && \ + $(KUSTOMIZE) build $(KUSTOMIZE_BUILD_FLAGS) $(1) +endef + .PHONY: verify-catalog verify-catalog: kustomize ## Verify the price catalog CSVs parse and the catalog ConfigMap renders. go test ./pkg/provider/catalog/... @@ -205,8 +219,7 @@ docker-buildx: buildx-builder ## Build and push docker image for the manager for .PHONY: build-installer build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. mkdir -p dist - cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} - $(KUSTOMIZE) build $(KUSTOMIZE_BUILD_FLAGS) config/default > dist/install.yaml + $(call render-manifests,config/default) > dist/install.yaml ##@ Deployment @@ -224,13 +237,11 @@ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified .PHONY: deploy deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. - cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} - $(KUSTOMIZE) build $(KUSTOMIZE_BUILD_FLAGS) config/default | $(KUBECTL) apply -f - + $(call render-manifests,config/default) | $(KUBECTL) apply -f - .PHONY: deploy-e2e deploy-e2e: manifests kustomize ## Deploy for e2e: config/default plus the fake-provider env var (baked in at deploy time, not via a post-deploy rollout). - cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} - $(KUSTOMIZE) build $(KUSTOMIZE_BUILD_FLAGS) config/e2e | $(KUBECTL) apply -f - + $(call render-manifests,config/e2e) | $(KUBECTL) apply -f - .PHONY: undeploy undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. diff --git a/README.md b/README.md index 6d2d969..080c659 100644 --- a/README.md +++ b/README.md @@ -105,9 +105,31 @@ the standard `nvidia.com/gpu` resource limit, so scheduling and provisioning rea the same number. Do not set `nodeName` or a provider `nodeSelector` yourself — the placement controller owns those. -## Getting started +## Quick start + +```bash +# 1. Namespace and provider credentials. Create them first — the manager reads them as env +# on startup. MODAL_ENVIRONMENT is optional; see docs/deploy.md for the other providers. +kubectl create namespace nebula-system +kubectl create secret generic nebula-modal-credentials -n nebula-system \ + --from-literal=MODAL_TOKEN_ID=ak-... \ + --from-literal=MODAL_TOKEN_SECRET=as-... + +# 2. CRDs, manager and webhook. Server-side apply, because the CRDs are too large for the +# last-applied annotation. The manager provisions its own webhook cert — no cert-manager. +kubectl apply --server-side -f https://github.com/InftyAI/Nebula/releases/download/v0.1.0/install.yaml +``` + +One virtual node appears per provider whose credentials are present: + +```bash +kubectl get nodes -l nebula.inftyai.com/provider +``` + +Then define a [NodePool](#defining-a-nodepool) and [opt a workload in](#opting-a-workload-in). -See [docs](docs/README.md) for an overview of Nebula. +To build and deploy from source instead, see [docs/deploy.md](docs/deploy.md). The +[docs](docs/README.md) cover the rest. ## License diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 086bc1e..d2d9ff3 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -5,4 +5,4 @@ kind: Kustomization images: - name: controller newName: inftyai/nebula-controller - newTag: latest + newTag: v0.1.0 diff --git a/docs/deploy.md b/docs/deploy.md index 1de3385..807adb4 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -3,7 +3,7 @@ This guide covers building and deploying the Nebula manager to a Kubernetes cluster, including wiring provider credentials. -- [Quick start](#quick-start) +- [From source](#from-source) - [How credentials are handled](#how-credentials-are-handled) - [Webhook TLS (no cert-manager)](#webhook-tls-no-cert-manager) - [What `deploy-all` does](#what-deploy-all-does) @@ -15,7 +15,10 @@ cluster, including wiring provider credentials. --- -## Quick start +## From source + +To install a release instead of building, see the quick start in the +[README](../README.md#quick-start). ```bash # 1. Put provider credentials in .env (secrets only — gitignored).