diff --git a/.github/.copilot/breadcrumbs/2026-09-01-1119-kubefleet-main-backport.md b/.github/.copilot/breadcrumbs/2026-09-01-1119-kubefleet-main-backport.md new file mode 100644 index 000000000..0daf1f970 --- /dev/null +++ b/.github/.copilot/breadcrumbs/2026-09-01-1119-kubefleet-main-backport.md @@ -0,0 +1,36 @@ +# Implementation: September 2026 KubeFleet Main Backport + +## Overview + +Merge the latest `kubefleet-dev/kubefleet` main branch into `Azure/fleet` main while preserving Fleet-specific module paths, build settings, chart layout, and link-check configuration. + +## Plan + +1. Confirm `cncf` and `upstream` remotes, fetch both main branches, and fast-forward the session branch to `upstream/main`. +2. Merge `cncf/main` with a merge commit, preferring incoming conflict resolutions and retaining incoming modify/delete files. +3. Restore Fleet conventions for Go module/import paths, repository links, Docker builder versions, CRD chart symlinks, and authenticated Slack link checking. +4. Run `make reviewable` in WSL and fix backport-related failures. +5. Commit any post-merge fixes, push the branch, and open a new pull request against `Azure/fleet:main`. + +## Success Criteria + +- [x] The branch begins at the latest `upstream/main`. +- [x] `cncf/main` is merged with a merge commit. +- [x] Go module and import references use `go.goms.io/fleet`. +- [x] Dockerfiles use the latest incoming Microsoft Go builder consistently. +- [x] No new CRD template symlinks remain. +- [x] `make reviewable` passes in WSL. +- [x] A new pull request targets `Azure/fleet:main` from the pushed fork branch. + +## Implementation Notes + +- Base: `upstream/main` at `f21dbd0cfd3cb10923472fa8557e4c9717ba3597`. +- Initial incoming: `cncf/main` at `48bde0d8`, merged as `533a4a8a`. +- Refreshed incoming: `cncf/main` at `81194d7d`, merged as `ccd19190`. +- Restored incoming Go imports in `test/e2e/join_and_leave_test.go` to `go.goms.io/fleet`. +- Confirmed all four Dockerfiles use `mcr.microsoft.com/oss/go/microsoft/golang:1.26.6-1`. +- Confirmed the Slack archives URL remains covered by `ignorePatterns`. +- Confirmed no new CRD template symlinks were introduced relative to the first merge parent. +- Ran `make reviewable` successfully in WSL with `GOTOOLCHAIN=go1.26.6`; shell scripts were normalized only in the working tree for validation and restored to the checkout line-ending convention afterward. +- Refetched `cncf/main` after PR #858 landed, merged its cross-compilation changes, and reran `make reviewable` successfully. +- Published the backport as `Azure/fleet` PR #1338. diff --git a/.github/workflows/code-lint.yml b/.github/workflows/code-lint.yml index d96ee1cf1..717439c85 100644 --- a/.github/workflows/code-lint.yml +++ b/.github/workflows/code-lint.yml @@ -79,7 +79,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Helm - uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5 + uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5 with: version: v3.17.0 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index edaa4ff37..23b855f38 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 + uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 + uses: github/codeql-action/autobuild@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4 # â„šī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -69,4 +69,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4 + uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4 diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml index 27b6f5379..35f0d7383 100644 --- a/.github/workflows/codespell.yml +++ b/.github/workflows/codespell.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 with: egress-policy: audit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 07762796d..030e9b91b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,39 +60,47 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + # A single multi-arch `docker buildx build` per image publishes the full + # tag (e.g. "v0.4.0") and, for stable releases, the short alias + # (e.g. "0.4.0") in the same push - no separate retag step. RC images are + # published under the long form ("v0.4.0-rc.1") for testers only: the + # short-tag namespace is deliberately reserved for stable releases that + # consumers can safely pin to, so RC tags get no short alias. - name: Build and push images with tag ${{ env.TAG }} - run: | - make push - - # The short-tag (e.g. ":0.4.0") aliases the long form for stable releases - # only. RC images are published under the long form (":v0.4.0-rc.1") for - # testers, but we deliberately do NOT alias them to a short tag - the - # short-tag namespace is reserved for stable releases that consumers can - # safely pin to (and "imagetools create" with an RC alias would publish - # "0.4.0-rc.1" into that namespace, muddying it). - - name: Tag and push images without v prefix - if: ${{ !contains(needs.export-registry.outputs.tag, '-rc.') }} env: VERSION: ${{ needs.export-registry.outputs.version }} run: | set -euo pipefail - for IMAGE in ${{ env.HUB_AGENT_IMAGE_NAME }} ${{ env.MEMBER_AGENT_IMAGE_NAME }} ${{ env.REFRESH_TOKEN_IMAGE_NAME }}; do - docker buildx imagetools create \ - --tag "${{ env.REGISTRY }}/${IMAGE}:${VERSION}" \ - "${{ env.REGISTRY }}/${IMAGE}:${{ env.TAG }}" - done + if [[ "${TAG}" == *-rc.* ]]; then + make push + else + make push IMAGE_EXTRA_TAG="${VERSION}" + fi - - name: Verify images + # Confirm every published tag is a real multi-arch manifest list rather + # than a silently degraded single-arch image: a build that pushed only one + # architecture would otherwise go unnoticed until a consumer on the other + # architecture failed to pull. Stable releases also carry the short alias. + - name: Verify images are multi-arch env: VERSION: ${{ needs.export-registry.outputs.version }} run: | set -euo pipefail - echo "✅ Published images:" + tags="${TAG}" + if [[ "${TAG}" != *-rc.* ]]; then + tags="${tags} ${VERSION}" + fi + echo "✅ Verifying published images:" for IMAGE in ${{ env.HUB_AGENT_IMAGE_NAME }} ${{ env.MEMBER_AGENT_IMAGE_NAME }} ${{ env.REFRESH_TOKEN_IMAGE_NAME }}; do - echo " - ${{ env.REGISTRY }}/${IMAGE}:${{ env.TAG }}" - if [[ "${TAG}" != *-rc.* ]]; then - echo " - ${{ env.REGISTRY }}/${IMAGE}:${VERSION}" - fi + for tag in ${tags}; do + ref="${{ env.REGISTRY }}/${IMAGE}:${tag}" + echo " - ${ref}" + manifest="$(docker buildx imagetools inspect "${ref}")" + for platform in linux/amd64 linux/arm64; do + grep -q "Platform:.*${platform}" <<<"${manifest}" \ + || { echo "::error::${ref} is missing platform ${platform}"; exit 1; } + done + done done # Publish the raw CRDs as a standalone release asset so consumers can install diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml index 67febc7a6..87423fa1c 100644 --- a/.github/workflows/workflow-lint.yml +++ b/.github/workflows/workflow-lint.yml @@ -22,7 +22,7 @@ jobs: timeout-minutes: 10 steps: - name: Harden Runner - uses: step-security/harden-runner@9ca718d3bf646d6534007c269a635b3e54cadf99 # v2.19.2 + uses: step-security/harden-runner@05e31511f85b41b11d1cf0ef85d0992719546e2c # v2.21.0 with: egress-policy: audit diff --git a/.golangci.yml b/.golangci.yml index c1e6993c4..aff61ff82 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,30 +1,28 @@ +version: "2" + run: timeout: 15m go: '1.26.6' -linters-settings: - stylecheck: - checks: [ "all", "-ST1001" ] # Disables dot-import warnings - revive: - rules: - - name: dot-imports - disabled: true - gosec: - excludes: - - G404 #Use of weak random number generator (math/rand or math/rand/v2). It is only used in tests. - linters: - disable-all: true + default: none + settings: + staticcheck: + checks: [ "all", "-ST1001" ] # Disables dot-import warnings + revive: + rules: + - name: dot-imports + disabled: true + gosec: + excludes: + - G404 #Use of weak random number generator (math/rand or math/rand/v2). It is only used in tests. enable: - decorder - errcheck - errorlint - goconst - gocyclo - - gofmt - - goimports - gosec - - gosimple - govet - ineffassign - misspell @@ -34,9 +32,11 @@ linters: - revive - staticcheck - tparallel - - typecheck - unconvert - unused - whitespace - # Run with --fast=false for more extensive checks - fast: true + +formatters: + enable: + - gofmt + - goimports diff --git a/Makefile b/Makefile index e6f60961a..7fd7a9bf4 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,12 @@ MEMBER_AGENT_IMAGE_VERSION ?= $(TAG) REFRESH_TOKEN_IMAGE_VERSION ?= $(TAG) CRD_INSTALLER_IMAGE_VERSION ?= $(TAG) +# Optional additional tag applied to every image within the same `docker buildx +# build` invocation. A stable release sets this to the short, v-less version +# (e.g. "0.4.0") so both "v0.4.0" and "0.4.0" are published from one build, +# which replaces a separate `docker buildx imagetools create` retag step. +IMAGE_EXTRA_TAG ?= + HUB_AGENT_IMAGE_NAME ?= hub-agent MEMBER_AGENT_IMAGE_NAME ?= member-agent REFRESH_TOKEN_IMAGE_NAME ?= refresh-token @@ -71,7 +77,7 @@ GOIMPORTS_VER := v0.42.0 GOIMPORTS_BIN := goimports GOIMPORTS := $(abspath $(TOOLS_BIN_DIR)/$(GOIMPORTS_BIN)-$(GOIMPORTS_VER)) -GOLANGCI_LINT_VER := v1.64.7 +GOLANGCI_LINT_VER := v2.13.2 GOLANGCI_LINT_BIN := golangci-lint GOLANGCI_LINT := $(abspath $(TOOLS_BIN_DIR)/$(GOLANGCI_LINT_BIN)-$(GOLANGCI_LINT_VER)) @@ -106,7 +112,7 @@ GO_INSTALL := ./hack/go-install.sh ## -------------------------------------- $(GOLANGCI_LINT): - GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) github.com/golangci/golangci-lint/cmd/golangci-lint $(GOLANGCI_LINT_BIN) $(GOLANGCI_LINT_VER) + GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) github.com/golangci/golangci-lint/v2/cmd/golangci-lint $(GOLANGCI_LINT_BIN) $(GOLANGCI_LINT_VER) $(CONTROLLER_GEN): GOBIN=$(TOOLS_BIN_DIR) $(GO_INSTALL) sigs.k8s.io/controller-tools/cmd/controller-gen $(CONTROLLER_GEN_BIN) $(CONTROLLER_GEN_VER) @@ -151,11 +157,11 @@ help: ## Display this help .PHONY: lint lint: $(GOLANGCI_LINT) ## Run fast linting - $(GOLANGCI_LINT) run -v + $(GOLANGCI_LINT) run -v --fast-only .PHONY: lint-full lint-full: $(GOLANGCI_LINT) ## Run slower linters to detect possible issues - $(GOLANGCI_LINT) run -v --fast=false + $(GOLANGCI_LINT) run -v ## -------------------------------------- ## Development @@ -295,12 +301,49 @@ run-crdinstaller: manifests generate fmt vet ## Run CRD installer from your host OUTPUT_TYPE ?= type=registry BUILDX_BUILDER_NAME ?= img-builder -QEMU_VERSION ?= 7.2.0-1 BUILDKIT_VERSION ?= v0.18.1 +# QEMU binfmt registration images (see setup-qemu). Both run --privileged, so +# they come from the Microsoft-vetted MCR mirrors (no direct third-party pulls) +# and are pinned by digest as well as tag: a floating reference on a privileged +# release-path container is a supply-chain risk. The binfmt mirror digest is +# byte-identical to the upstream docker.io/tonistiigi/binfmt tag it mirrors. +# +# qemu-user-static has no manifest list - neither upstream nor on the mirror. It +# is a single linux/amd64 schema-2 manifest, which is all setup-qemu needs: the +# QEMU_IMAGE branch runs only when TARGET_ARCH is amd64. Obtain its digest with a +# request that names a schema-2 or OCI media type (substitute QEMU_VERSION for the +# tag): +# +# curl -sI -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \ +# https://mcr.microsoft.com/v2/mirror/docker/multiarch/qemu-user-static/manifests/7.2.0-1 \ +# | grep -i docker-content-digest +# +# `docker manifest inspect -v` and `docker buildx imagetools inspect` also report +# the correct digest. What does NOT is a bare `curl -sI` sending no Accept header, +# or one naming only the manifest-list type: MCR then answers with a deprecated +# schema-1 `v1+prettyjws` view synthesised on demand from the schema-2 manifest. +# The digest it advertises is stable, but it is never stored as a manifest +# revision, so fetching by it returns "manifest unknown" and every pull of a pin +# taken that way fails - most likely how the previous pin here was produced. +# +# Both mirrors carry exactly one tag today, so bumping either version below needs +# MCR to onboard the new tag first. +QEMU_VERSION ?= 7.2.0-1 +QEMU_IMAGE ?= mcr.microsoft.com/mirror/docker/multiarch/qemu-user-static:$(QEMU_VERSION)@sha256:fe60359c92e86a43cc87b3d906006245f77bfc0565676b80004cc666e4feb9f0 +BINFMT_VERSION ?= qemu-v9.2.2-52 +BINFMT_IMAGE ?= mcr.microsoft.com/mirror/docker/tonistiigi/binfmt:$(BINFMT_VERSION)@sha256:1b804311fe87047a4c96d38b4b3ef6f62fca8cd125265917a9e3dc3c996c39e6 + +# Platforms to build container images for. Defaults to the host/target platform +# so local single-arch builds (e.g. loading into kind, which cannot load a +# multi-platform image) keep working unchanged; `push` overrides this to build a +# multi-arch manifest for the release. +PLATFORMS ?= $(TARGET_OS)/$(TARGET_ARCH) +RELEASE_PLATFORMS ?= linux/amd64,linux/arm64 + .PHONY: push -push: ## Build and push all Docker images - $(MAKE) OUTPUT_TYPE="type=registry" docker-build-hub-agent docker-build-member-agent docker-build-refresh-token docker-build-crd-installer +push: ## Build and push all Docker images as multi-arch manifests + $(MAKE) OUTPUT_TYPE="type=registry" PLATFORMS="$(RELEASE_PLATFORMS)" docker-build-hub-agent docker-build-member-agent docker-build-refresh-token .PHONY: helm-push helm-push: ## Package and push Helm charts to OCI registry @@ -347,72 +390,78 @@ crd-verify: ## Verify the chart CRD directories cover every CRD in config/crd/ba fi; \ echo "crd-verify: chart CRD directories cover all CRDs in config/crd/bases" -# By default, docker buildx create will pull image moby/buildkit:buildx-stable-1 and hit the too many requests error -# -# Note (chenyu1): the step below sets up emulation for building/running non-native binaries on the host. The original -# setup assumes that the Makefile is always run on an x86_64 platform, and adds support for non-x86_64 hosts. Here -# we keep the original setup if the build target is x86_64 platforms (default) for compatibility reasons, but will switch to -# a more general setup for non-x86_64 hosts. -# -# On some systems the emulation setup might not work at all (e.g., macOS on Apple Silicon -> Rosetta 2 will be used -# by Docker Desktop as the default emulation option for AMD64 on ARM64 container compatibility). -.PHONY: docker-buildx-builder -# Note (chenyu1): the step below sets up emulation for building/running non-native binaries on the host. The original -# setup assumes that the Makefile is always run on an x86_64 platform, and adds support for non-x86_64 hosts. Here -# we keep the original setup if the build target is x86_64 platforms (default) for compatibility reasons, but will switch to -# a more general setup for non-x86_64 hosts. +# Register QEMU binfmt handlers so the host can build/run non-native binaries. +# This must run for any build that targets a non-native platform - whether a +# multi-platform build or a single-platform cross build (e.g. PLATFORMS=linux/arm64 +# on an amd64 host) - regardless of whether the buildx builder already exists (a +# pre-existing builder would otherwise skip emulation setup and the foreign-arch +# build would fail with "exec format error"). The registration is idempotent, so +# it is safe to re-run. It is skipped only when PLATFORMS is exactly the native +# platform, which never needs emulation. # -# On some systems the emulation setup might not work at all (e.g., macOS on Apple Silicon -> Rosetta 2 will be used -# by Docker Desktop as the default emulation option for AMD64 on ARM64 container compatibility). -docker-buildx-builder: +# On some systems the emulation setup might not work at all (e.g., macOS on Apple +# Silicon -> Rosetta 2 will be used by Docker Desktop as the default emulation +# option for AMD64 on ARM64 container compatibility). +.PHONY: setup-qemu +setup-qemu: ## Register QEMU emulation for multi-architecture image builds $(info Auto-detected system architecture: $(TARGET_ARCH)) - @if ! docker buildx ls | grep $(BUILDX_BUILDER_NAME); then \ - if [ "$(TARGET_ARCH)" = "amd64" ] ; then \ - echo "The target is an x86_64 platform; setting up emulation for other known architectures"; \ - docker run --rm --privileged mcr.microsoft.com/mirror/docker/multiarch/qemu-user-static:$(QEMU_VERSION) --reset -p yes; \ - else \ - echo "Setting up emulation for known architectures"; \ - docker run --rm --privileged tonistiigi/binfmt --install all; \ - fi ;\ - docker buildx create --driver-opt image=mcr.microsoft.com/oss/v2/moby/buildkit:$(BUILDKIT_VERSION) --name $(BUILDX_BUILDER_NAME) --use; \ - docker buildx inspect $(BUILDX_BUILDER_NAME) --bootstrap; \ + @case "$(PLATFORMS)" in \ + "$(TARGET_OS)/$(TARGET_ARCH)") \ + echo "Native single-platform build ($(PLATFORMS)); skipping QEMU setup" ;; \ + *) \ + echo "Build targets non-native platforms ($(PLATFORMS)); registering QEMU emulation"; \ + if [ "$(TARGET_ARCH)" = "amd64" ] ; then \ + docker run --rm --privileged $(QEMU_IMAGE) --reset -p yes; \ + else \ + docker run --rm --privileged $(BINFMT_IMAGE) --install all; \ + fi ;; \ + esac + +# By default, docker buildx create will pull image moby/buildkit:buildx-stable-1 and hit the too many requests error +# Always select and bootstrap the named builder, not only on creation: if the +# builder already exists but another builder is current, the subsequent +# `docker buildx build` would silently run on the wrong builder (potentially a +# docker-driver one with no multi-platform support). +.PHONY: docker-buildx-builder +docker-buildx-builder: setup-qemu + @if ! docker buildx ls | grep -q "$(BUILDX_BUILDER_NAME)"; then \ + docker buildx create --driver-opt image=mcr.microsoft.com/oss/v2/moby/buildkit:$(BUILDKIT_VERSION) --name $(BUILDX_BUILDER_NAME); \ fi + docker buildx use $(BUILDX_BUILDER_NAME) + docker buildx inspect $(BUILDX_BUILDER_NAME) --bootstrap .PHONY: docker-build-hub-agent docker-build-hub-agent: docker-buildx-builder ## Build hub-agent image docker buildx build \ --file docker/$(HUB_AGENT_IMAGE_NAME).Dockerfile \ --output=$(OUTPUT_TYPE) \ - --platform=$(TARGET_OS)/$(TARGET_ARCH) \ + --platform=$(PLATFORMS) \ --pull \ --tag $(REGISTRY)/$(HUB_AGENT_IMAGE_NAME):$(HUB_AGENT_IMAGE_VERSION) \ - --progress=$(BUILDKIT_PROGRESS_TYPE) \ - --build-arg GOARCH=$(TARGET_ARCH) \ - --build-arg GOOS=$(TARGET_OS) . + $(if $(IMAGE_EXTRA_TAG),--tag $(REGISTRY)/$(HUB_AGENT_IMAGE_NAME):$(IMAGE_EXTRA_TAG)) \ + --progress=$(BUILDKIT_PROGRESS_TYPE) . .PHONY: docker-build-member-agent docker-build-member-agent: docker-buildx-builder ## Build member-agent image docker buildx build \ --file docker/$(MEMBER_AGENT_IMAGE_NAME).Dockerfile \ --output=$(OUTPUT_TYPE) \ - --platform=$(TARGET_OS)/$(TARGET_ARCH) \ + --platform=$(PLATFORMS) \ --pull \ --tag $(REGISTRY)/$(MEMBER_AGENT_IMAGE_NAME):$(MEMBER_AGENT_IMAGE_VERSION) \ - --progress=$(BUILDKIT_PROGRESS_TYPE) \ - --build-arg GOARCH=$(TARGET_ARCH) \ - --build-arg GOOS=$(TARGET_OS) . + $(if $(IMAGE_EXTRA_TAG),--tag $(REGISTRY)/$(MEMBER_AGENT_IMAGE_NAME):$(IMAGE_EXTRA_TAG)) \ + --progress=$(BUILDKIT_PROGRESS_TYPE) . .PHONY: docker-build-refresh-token docker-build-refresh-token: docker-buildx-builder ## Build refresh-token image docker buildx build \ --file docker/$(REFRESH_TOKEN_IMAGE_NAME).Dockerfile \ --output=$(OUTPUT_TYPE) \ - --platform=$(TARGET_OS)/$(TARGET_ARCH) \ + --platform=$(PLATFORMS) \ --pull \ --tag $(REGISTRY)/$(REFRESH_TOKEN_IMAGE_NAME):$(REFRESH_TOKEN_IMAGE_VERSION) \ - --progress=$(BUILDKIT_PROGRESS_TYPE) \ - --build-arg GOARCH=$(TARGET_ARCH) \ - --build-arg GOOS=${TARGET_OS} . + $(if $(IMAGE_EXTRA_TAG),--tag $(REGISTRY)/$(REFRESH_TOKEN_IMAGE_NAME):$(IMAGE_EXTRA_TAG)) \ + --progress=$(BUILDKIT_PROGRESS_TYPE) . .PHONY: docker-build-crd-installer docker-build-crd-installer: docker-buildx-builder diff --git a/apis/kubefleet.dev/placement/v1alpha1/clusterrequest_types.go b/apis/kubefleet.dev/placement/v1alpha1/clusterclaim_types.go similarity index 68% rename from apis/kubefleet.dev/placement/v1alpha1/clusterrequest_types.go rename to apis/kubefleet.dev/placement/v1alpha1/clusterclaim_types.go index 601185319..c9f3f9ab6 100644 --- a/apis/kubefleet.dev/placement/v1alpha1/clusterrequest_types.go +++ b/apis/kubefleet.dev/placement/v1alpha1/clusterclaim_types.go @@ -21,10 +21,10 @@ import ( ) const ( - ClusterRequestCondTypeCompleted = "Completed" + ClusterClaimCondTypeCompleted = "Completed" ) -// ClusterRequest is a KubeFleet API that represents a request for a member cluster to be provisioned. +// ClusterClaim is a KubeFleet API that represents a claim for a new member cluster. // It is created by KubeFleet when it fails to find a member cluster that can fulfill some scheduling // requirements as specified in a PlacementPolicy or ClusterPlacementPolicy object. // @@ -33,21 +33,21 @@ const ( // +kubebuilder:subresource:status // +kubebuilder:resource:scope=Cluster,categories={kubefleet, kubefleet-placement} // +kubebuilder:storageversion -type ClusterRequest struct { +type ClusterClaim struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - // The specification of the cluster request. + // The specification of the cluster claim. // +kubebuilder:validation:Required - Spec ClusterRequestSpec `json:"spec,omitempty"` + Spec ClusterClaimSpec `json:"spec,omitempty"` - // The observed status of the cluster request. + // The observed status of the cluster claim. // +kubebuilder:validation:Optional - Status ClusterRequestStatus `json:"status,omitempty"` + Status ClusterClaimStatus `json:"status,omitempty"` } -type ClusterRequestSpec struct { - // The reference to the placement policy that submits the cluster request. +type ClusterClaimSpec struct { + // The reference to the placement policy that adds the cluster claim. // // This field is immutable after creation. // @@ -57,7 +57,7 @@ type ClusterRequestSpec struct { // The cluster selector terms that describe the requirements for a new member cluster. // - // If not specified, any member cluster can satisfy the request. + // If not specified, any member cluster can satisfy the claim. // // This field is immutable after creation. // @@ -66,42 +66,42 @@ type ClusterRequestSpec struct { ClusterSelectorTerms []ClusterLabelAndPropertySelectorTerm `json:"clusterSelectorTerms,omitempty"` } -type ClusterRequestStatus struct { - // A list of observed conditions of the cluster request. +type ClusterClaimStatus struct { + // A list of observed conditions of the cluster claim. // // +kubebuilder:validation:Optional Conditions []metav1.Condition `json:"conditions,omitempty"` - // The name of the cluster that has been provisioned for this cluster request, if any. + // The name of the cluster that has been provisioned for this cluster claim, if any. // // +kubebuilder:validation:Optional ProvisionedClusterName *string `json:"provisionedClusterName,omitempty"` // The last observed most recent creation timestamp across all the member clusters. This field is used - // as an expedient solution to verify if a cluster request is still valid for consideration, i.e., + // as an expedient solution to verify if a cluster claim is still valid for consideration, i.e., // if the currently observed most recent cluster creation timestamp is later than this timestamp in the - // status, a new member cluster must have been created after the cluster request was created, - // and thus the cluster request should be considered stale and can be ignored. The placement policy - // that submits the cluster request is responsible for updating this field if the new cluster does not - // meet the need of the associated cluster selector, so that the cluster request can be re-evaluated again; - // it may instead withdraw the cluster request if the new cluster has fulfilled the associated cluster selector. + // status, a new member cluster must have been created after the cluster claim was created, + // and thus the cluster claim should be considered stale and can be ignored. The placement policy + // that adds the cluster claim is responsible for updating this field if the new cluster does not + // meet the need of the associated cluster selector, so that the cluster claim can be re-evaluated again; + // it may instead withdraw the cluster claim if the new cluster has fulfilled the associated cluster selector. // // +kubebuilder:validation:Optional LastObservedMostRecentClusterCreationTimestamp *metav1.Time `json:"lastObservedMostRecentClusterCreationTimestamp,omitempty"` } -// ClusterRequestList contains a list of ClusterRequest. +// ClusterClaimList contains a list of ClusterClaim. // // +kubebuilder:object:root=true // +kubebuilder:resource:scope="Cluster" // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type ClusterRequestList struct { +type ClusterClaimList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []ClusterRequest `json:"items"` + Items []ClusterClaim `json:"items"` } func init() { - SchemeBuilder.Register(&ClusterRequest{}, &ClusterRequestList{}) + SchemeBuilder.Register(&ClusterClaim{}, &ClusterClaimList{}) } diff --git a/apis/kubefleet.dev/placement/v1alpha1/placementpolicy_types.go b/apis/kubefleet.dev/placement/v1alpha1/placementpolicy_types.go index 5d47fa4af..f29abb1cf 100644 --- a/apis/kubefleet.dev/placement/v1alpha1/placementpolicy_types.go +++ b/apis/kubefleet.dev/placement/v1alpha1/placementpolicy_types.go @@ -184,16 +184,16 @@ type ClusterSelector struct { // The action to take when KubeFleet is not able to find the desired (minimum) number of clusters based on the given terms. // // Available options are: - // * RequestCluster: KubeFleet will submit a cluster request to signal that a new cluster is needed to complete the placement. - // It is up to the platform/cloud provider to fulfill the request. - // * KeepSearching: KubeFleet will keep searching for clusters that match the given terms silently; no cluster request will be - // submitted. + // * AddClusterClaim: KubeFleet will add a cluster claim to signal that a new cluster is needed to complete the placement. + // It is up to the platform/cloud provider to fulfill the claim. + // * KeepSearching: KubeFleet will keep searching for clusters that match the given terms silently; no cluster claim will be + // added. // - // This field takes effect only when cluster requests are enabled in KubeFleet. + // This field takes effect only when cluster claims are enabled in KubeFleet. // // +kubebuilder:validation:Optional - // +kubebuilder:default=RequestCluster - // +kubebuilder:validation:Enum=RequestCluster;KeepSearching + // +kubebuilder:default=AddClusterClaim + // +kubebuilder:validation:Enum=AddClusterClaim;KeepSearching WhenUnfulfilled WhenUnfulfilledOption `json:"whenUnfulfilled,omitempty"` } @@ -271,8 +271,8 @@ const ( type WhenUnfulfilledOption string const ( - WhenUnfulfilledOptionRequestCluster WhenUnfulfilledOption = "RequestCluster" - WhenUnfulfilledOptionKeepSearching WhenUnfulfilledOption = "KeepSearching" + WhenUnfulfilledOptionAddClusterClaim WhenUnfulfilledOption = "AddClusterClaim" + WhenUnfulfilledOptionKeepSearching WhenUnfulfilledOption = "KeepSearching" ) type ResourceSelector struct { @@ -547,8 +547,8 @@ type PlacementPolicyStatus struct { // The number of clusters that have resources in the available state, as verified by KubeFleet's availability check. ResourcesAvailableClusters *int32 `json:"resourcesAvailableClusters,omitempty"` - // The number of ongoing cluster requests that have been submitted by this placement. - OngoingClusterRequests *int32 `json:"ongoingClusterRequests,omitempty"` + // The number of active cluster claims that have been submitted by this placement. + ActiveClusterClaims *int32 `json:"activeClusterClaims,omitempty"` // The binding manager that is currently managing the bindings for this placement. // +kubebuilder:validation:Optional diff --git a/apis/kubefleet.dev/placement/v1alpha1/zz_generated.deepcopy.go b/apis/kubefleet.dev/placement/v1alpha1/zz_generated.deepcopy.go index 19f036041..b483d46f4 100644 --- a/apis/kubefleet.dev/placement/v1alpha1/zz_generated.deepcopy.go +++ b/apis/kubefleet.dev/placement/v1alpha1/zz_generated.deepcopy.go @@ -46,6 +46,123 @@ func (in *BindingManager) DeepCopy() *BindingManager { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterClaim) DeepCopyInto(out *ClusterClaim) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterClaim. +func (in *ClusterClaim) DeepCopy() *ClusterClaim { + if in == nil { + return nil + } + out := new(ClusterClaim) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterClaim) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterClaimList) DeepCopyInto(out *ClusterClaimList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterClaim, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterClaimList. +func (in *ClusterClaimList) DeepCopy() *ClusterClaimList { + if in == nil { + return nil + } + out := new(ClusterClaimList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterClaimList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterClaimSpec) DeepCopyInto(out *ClusterClaimSpec) { + *out = *in + if in.PlacementPolicyRef != nil { + in, out := &in.PlacementPolicyRef, &out.PlacementPolicyRef + *out = new(ObjectReference) + **out = **in + } + if in.ClusterSelectorTerms != nil { + in, out := &in.ClusterSelectorTerms, &out.ClusterSelectorTerms + *out = make([]ClusterLabelAndPropertySelectorTerm, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterClaimSpec. +func (in *ClusterClaimSpec) DeepCopy() *ClusterClaimSpec { + if in == nil { + return nil + } + out := new(ClusterClaimSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterClaimStatus) DeepCopyInto(out *ClusterClaimStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ProvisionedClusterName != nil { + in, out := &in.ProvisionedClusterName, &out.ProvisionedClusterName + *out = new(string) + **out = **in + } + if in.LastObservedMostRecentClusterCreationTimestamp != nil { + in, out := &in.LastObservedMostRecentClusterCreationTimestamp, &out.LastObservedMostRecentClusterCreationTimestamp + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterClaimStatus. +func (in *ClusterClaimStatus) DeepCopy() *ClusterClaimStatus { + if in == nil { + return nil + } + out := new(ClusterClaimStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterLabelAndPropertySelectorTerm) DeepCopyInto(out *ClusterLabelAndPropertySelectorTerm) { *out = *in @@ -258,123 +375,6 @@ func (in *ClusterPlacementResourceSnapshotList) DeepCopyObject() runtime.Object return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ClusterRequest) DeepCopyInto(out *ClusterRequest) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterRequest. -func (in *ClusterRequest) DeepCopy() *ClusterRequest { - if in == nil { - return nil - } - out := new(ClusterRequest) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ClusterRequest) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ClusterRequestList) DeepCopyInto(out *ClusterRequestList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]ClusterRequest, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterRequestList. -func (in *ClusterRequestList) DeepCopy() *ClusterRequestList { - if in == nil { - return nil - } - out := new(ClusterRequestList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ClusterRequestList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ClusterRequestSpec) DeepCopyInto(out *ClusterRequestSpec) { - *out = *in - if in.PlacementPolicyRef != nil { - in, out := &in.PlacementPolicyRef, &out.PlacementPolicyRef - *out = new(ObjectReference) - **out = **in - } - if in.ClusterSelectorTerms != nil { - in, out := &in.ClusterSelectorTerms, &out.ClusterSelectorTerms - *out = make([]ClusterLabelAndPropertySelectorTerm, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterRequestSpec. -func (in *ClusterRequestSpec) DeepCopy() *ClusterRequestSpec { - if in == nil { - return nil - } - out := new(ClusterRequestSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ClusterRequestStatus) DeepCopyInto(out *ClusterRequestStatus) { - *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } - if in.ProvisionedClusterName != nil { - in, out := &in.ProvisionedClusterName, &out.ProvisionedClusterName - *out = new(string) - **out = **in - } - if in.LastObservedMostRecentClusterCreationTimestamp != nil { - in, out := &in.LastObservedMostRecentClusterCreationTimestamp, &out.LastObservedMostRecentClusterCreationTimestamp - *out = (*in).DeepCopy() - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterRequestStatus. -func (in *ClusterRequestStatus) DeepCopy() *ClusterRequestStatus { - if in == nil { - return nil - } - out := new(ClusterRequestStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterSelector) DeepCopyInto(out *ClusterSelector) { *out = *in @@ -860,8 +860,8 @@ func (in *PlacementPolicyStatus) DeepCopyInto(out *PlacementPolicyStatus) { *out = new(int32) **out = **in } - if in.OngoingClusterRequests != nil { - in, out := &in.OngoingClusterRequests, &out.OngoingClusterRequests + if in.ActiveClusterClaims != nil { + in, out := &in.ActiveClusterClaims, &out.ActiveClusterClaims *out = new(int32) **out = **in } diff --git a/config/crd/bases/placement.kubefleet.dev_clusterrequests.yaml b/config/crd/bases/placement.kubefleet.dev_clusterclaims.yaml similarity index 93% rename from config/crd/bases/placement.kubefleet.dev_clusterrequests.yaml rename to config/crd/bases/placement.kubefleet.dev_clusterclaims.yaml index 98eca4564..2bd4a6ad8 100644 --- a/config/crd/bases/placement.kubefleet.dev_clusterrequests.yaml +++ b/config/crd/bases/placement.kubefleet.dev_clusterclaims.yaml @@ -4,24 +4,24 @@ kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.20.0 - name: clusterrequests.placement.kubefleet.dev + name: clusterclaims.placement.kubefleet.dev spec: group: placement.kubefleet.dev names: categories: - kubefleet - kubefleet-placement - kind: ClusterRequest - listKind: ClusterRequestList - plural: clusterrequests - singular: clusterrequest + kind: ClusterClaim + listKind: ClusterClaimList + plural: clusterclaims + singular: clusterclaim scope: Cluster versions: - name: v1alpha1 schema: openAPIV3Schema: description: |- - ClusterRequest is a KubeFleet API that represents a request for a member cluster to be provisioned. + ClusterClaim is a KubeFleet API that represents a claim for a new member cluster. It is created by KubeFleet when it fails to find a member cluster that can fulfill some scheduling requirements as specified in a PlacementPolicy or ClusterPlacementPolicy object. properties: @@ -43,13 +43,13 @@ spec: metadata: type: object spec: - description: The specification of the cluster request. + description: The specification of the cluster claim. properties: clusterSelectorTerms: description: |- The cluster selector terms that describe the requirements for a new member cluster. - If not specified, any member cluster can satisfy the request. + If not specified, any member cluster can satisfy the claim. This field is immutable after creation. items: @@ -190,7 +190,7 @@ spec: rule: self == oldSelf placementPolicyRef: description: |- - The reference to the placement policy that submits the cluster request. + The reference to the placement policy that adds the cluster claim. This field is immutable after creation. properties: @@ -221,10 +221,10 @@ spec: - placementPolicyRef type: object status: - description: The observed status of the cluster request. + description: The observed status of the cluster claim. properties: conditions: - description: A list of observed conditions of the cluster request. + description: A list of observed conditions of the cluster claim. items: description: Condition contains details for one aspect of the current state of this API Resource. @@ -283,18 +283,18 @@ spec: lastObservedMostRecentClusterCreationTimestamp: description: |- The last observed most recent creation timestamp across all the member clusters. This field is used - as an expedient solution to verify if a cluster request is still valid for consideration, i.e., + as an expedient solution to verify if a cluster claim is still valid for consideration, i.e., if the currently observed most recent cluster creation timestamp is later than this timestamp in the - status, a new member cluster must have been created after the cluster request was created, - and thus the cluster request should be considered stale and can be ignored. The placement policy - that submits the cluster request is responsible for updating this field if the new cluster does not - meet the need of the associated cluster selector, so that the cluster request can be re-evaluated again; - it may instead withdraw the cluster request if the new cluster has fulfilled the associated cluster selector. + status, a new member cluster must have been created after the cluster claim was created, + and thus the cluster claim should be considered stale and can be ignored. The placement policy + that adds the cluster claim is responsible for updating this field if the new cluster does not + meet the need of the associated cluster selector, so that the cluster claim can be re-evaluated again; + it may instead withdraw the cluster claim if the new cluster has fulfilled the associated cluster selector. format: date-time type: string provisionedClusterName: description: The name of the cluster that has been provisioned for - this cluster request, if any. + this cluster claim, if any. type: string type: object required: diff --git a/config/crd/bases/placement.kubefleet.dev_clusterplacementpolicies.yaml b/config/crd/bases/placement.kubefleet.dev_clusterplacementpolicies.yaml index 8b1f91fa3..d528f88bd 100644 --- a/config/crd/bases/placement.kubefleet.dev_clusterplacementpolicies.yaml +++ b/config/crd/bases/placement.kubefleet.dev_clusterplacementpolicies.yaml @@ -221,19 +221,19 @@ spec: maxItems: 5 type: array whenUnfulfilled: - default: RequestCluster + default: AddClusterClaim description: |- The action to take when KubeFleet is not able to find the desired (minimum) number of clusters based on the given terms. Available options are: - * RequestCluster: KubeFleet will submit a cluster request to signal that a new cluster is needed to complete the placement. - It is up to the platform/cloud provider to fulfill the request. - * KeepSearching: KubeFleet will keep searching for clusters that match the given terms silently; no cluster request will be - submitted. + * AddClusterClaim: KubeFleet will add a cluster claim to signal that a new cluster is needed to complete the placement. + It is up to the platform/cloud provider to fulfill the claim. + * KeepSearching: KubeFleet will keep searching for clusters that match the given terms silently; no cluster claim will be + added. - This field takes effect only when cluster requests are enabled in KubeFleet. + This field takes effect only when cluster claims are enabled in KubeFleet. enum: - - RequestCluster + - AddClusterClaim - KeepSearching type: string type: object @@ -549,6 +549,11 @@ spec: status: description: The observed status of the cluster placement policy. properties: + activeClusterClaims: + description: The number of active cluster claims that have been submitted + by this placement. + format: int32 + type: integer bindingManager: description: The binding manager that is currently managing the bindings for this placement. @@ -653,11 +658,6 @@ spec: description: The name of the latest revision of the resources selected by this placement policy, in the form of a resource snapshot. type: string - ongoingClusterRequests: - description: The number of ongoing cluster requests that have been - submitted by this placement. - format: int32 - type: integer resourcesAvailableClusters: description: The number of clusters that have resources in the available state, as verified by KubeFleet's availability check. diff --git a/config/crd/bases/placement.kubefleet.dev_placementpolicies.yaml b/config/crd/bases/placement.kubefleet.dev_placementpolicies.yaml index 972186615..578a6eb5f 100644 --- a/config/crd/bases/placement.kubefleet.dev_placementpolicies.yaml +++ b/config/crd/bases/placement.kubefleet.dev_placementpolicies.yaml @@ -221,19 +221,19 @@ spec: maxItems: 5 type: array whenUnfulfilled: - default: RequestCluster + default: AddClusterClaim description: |- The action to take when KubeFleet is not able to find the desired (minimum) number of clusters based on the given terms. Available options are: - * RequestCluster: KubeFleet will submit a cluster request to signal that a new cluster is needed to complete the placement. - It is up to the platform/cloud provider to fulfill the request. - * KeepSearching: KubeFleet will keep searching for clusters that match the given terms silently; no cluster request will be - submitted. + * AddClusterClaim: KubeFleet will add a cluster claim to signal that a new cluster is needed to complete the placement. + It is up to the platform/cloud provider to fulfill the claim. + * KeepSearching: KubeFleet will keep searching for clusters that match the given terms silently; no cluster claim will be + added. - This field takes effect only when cluster requests are enabled in KubeFleet. + This field takes effect only when cluster claims are enabled in KubeFleet. enum: - - RequestCluster + - AddClusterClaim - KeepSearching type: string type: object @@ -549,6 +549,11 @@ spec: status: description: The observed status of the placement policy. properties: + activeClusterClaims: + description: The number of active cluster claims that have been submitted + by this placement. + format: int32 + type: integer bindingManager: description: The binding manager that is currently managing the bindings for this placement. @@ -653,11 +658,6 @@ spec: description: The name of the latest revision of the resources selected by this placement policy, in the form of a resource snapshot. type: string - ongoingClusterRequests: - description: The number of ongoing cluster requests that have been - submitted by this placement. - format: int32 - type: integer resourcesAvailableClusters: description: The number of clusters that have resources in the available state, as verified by KubeFleet's availability check. diff --git a/docker/hub-agent.Dockerfile b/docker/hub-agent.Dockerfile index 0687fc30e..99a813377 100644 --- a/docker/hub-agent.Dockerfile +++ b/docker/hub-agent.Dockerfile @@ -1,8 +1,14 @@ -# Build the hubagent binary -FROM mcr.microsoft.com/oss/go/microsoft/golang:1.26.6-1 AS builder - -ARG GOOS=linux -ARG GOARCH=amd64 +# syntax=docker/dockerfile:1 +# Build the hubagent binary. +# +# The builder stage always runs on the build host's own platform +# (--platform=$BUILDPLATFORM) and cross-compiles for each target platform, so +# the Go toolchain, cgo and gcc never execute under QEMU. Running the builder +# per target under emulation crashed gcc's cc1 ("internal compiler error: +# Segmentation fault" / "cgo: gcc produced no output" in runtime/cgo and net) +# and, when it did not crash, took over an hour per image. Only the final +# distroless stage is per-target, and it runs no commands. +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang:1.26.6-1 AS builder WORKDIR /workspace # Copy the Go Modules manifests @@ -12,20 +18,53 @@ COPY go.sum go.sum # and so that source changes don't invalidate our downloaded layer RUN go mod download +# The module download above is declared before the target-specific ARGs so +# that BuildKit shares its layer across every target platform of one build. +# TARGETOS and TARGETARCH are populated automatically by BuildKit for each +# platform being built, so a single multi-platform `docker buildx build` +# produces a correctly built binary per architecture. +ARG TARGETOS +ARG TARGETARCH +ARG BUILDARCH + +# GOEXPERIMENT=systemcrypto requires CGO_ENABLED=1. Its OpenSSL backend +# dlopen()s libcrypto at run time, so no OpenSSL headers are needed to build - +# only a C compiler and libc headers for the target architecture. When the +# target differs from the architecture this stage runs on, install Debian's +# cross toolchain for it. Either way /usr/local/bin/target-gcc ends up pointing +# at the right compiler. BUILDARCH is this stage's own architecture because the +# stage is pinned to --platform=$BUILDPLATFORM above. +RUN set -eu; \ + if [ "${TARGETARCH}" = "${BUILDARCH}" ]; then \ + ln -s /usr/bin/gcc /usr/local/bin/target-gcc; \ + else \ + case "${TARGETARCH}" in \ + arm64) pkg=gcc-aarch64-linux-gnu; triple=aarch64-linux-gnu ;; \ + amd64) pkg=gcc-x86-64-linux-gnu; triple=x86_64-linux-gnu ;; \ + *) echo "unsupported TARGETARCH ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + apt-get update; \ + apt-get install -y --no-install-recommends "${pkg}" "libc6-dev-${TARGETARCH}-cross"; \ + rm -rf /var/lib/apt/lists/*; \ + ln -s "/usr/bin/${triple}-gcc" /usr/local/bin/target-gcc; \ + fi + # Copy the go source -COPY cmd/hubagent/ cmd/hubagent/ +COPY cmd/hubagent/ cmd/hubagent/ COPY apis/ apis/ COPY pkg/ pkg/ -# Build with CGO enabled and GOEXPERIMENT=systemcrypto for internal usage -RUN echo "Building for GOOS=$GOOS GOARCH=$GOARCH" -RUN CGO_ENABLED=1 GOOS=$GOOS GOARCH=$GOARCH GOEXPERIMENT=systemcrypto GO111MODULE=on go build -o hubagent cmd/hubagent/main.go +# Build for the target platform with the compiler selected above. +RUN echo "Building hubagent with GOOS=${TARGETOS} GOARCH=${TARGETARCH} CC=$(readlink -f /usr/local/bin/target-gcc)" && \ + CGO_ENABLED=1 CC=target-gcc GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOEXPERIMENT=systemcrypto go build -o hubagent ./cmd/hubagent/ -# Use Azure Linux distroless base image to package the hubagent binary -# Refer to https://mcr.microsoft.com/en-us/artifact/mar/azurelinux/distroless/base/about for more details -FROM mcr.microsoft.com/azurelinux/distroless/base:3.0 +# Use distroless as minimal base image to package the hubagent binary. +# The pinned digest must reference a multi-arch image index so BuildKit can +# resolve the matching base layer for each target architecture. +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/base:nonroot@sha256:2d7d29b504e7166f6d0c7655a18ebf5def5b37b029f8c4f8667e434ba774844f WORKDIR / -COPY --from=builder /workspace/hubagent . +COPY --link --from=builder /workspace/hubagent . USER 65532:65532 ENTRYPOINT ["/hubagent"] diff --git a/docker/member-agent.Dockerfile b/docker/member-agent.Dockerfile index d05da60c8..f05cf953b 100644 --- a/docker/member-agent.Dockerfile +++ b/docker/member-agent.Dockerfile @@ -1,8 +1,14 @@ -# Build the memberagent binary -FROM mcr.microsoft.com/oss/go/microsoft/golang:1.26.6-1 AS builder - -ARG GOOS=linux -ARG GOARCH=amd64 +# syntax=docker/dockerfile:1 +# Build the memberagent binary. +# +# The builder stage always runs on the build host's own platform +# (--platform=$BUILDPLATFORM) and cross-compiles for each target platform, so +# the Go toolchain, cgo and gcc never execute under QEMU. Running the builder +# per target under emulation crashed gcc's cc1 ("internal compiler error: +# Segmentation fault" / "cgo: gcc produced no output" in runtime/cgo and net) +# and, when it did not crash, took over an hour per image. Only the final +# distroless stage is per-target, and it runs no commands. +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang:1.26.6-1 AS builder WORKDIR /workspace # Copy the Go Modules manifests @@ -12,20 +18,53 @@ COPY go.sum go.sum # and so that source changes don't invalidate our downloaded layer RUN go mod download +# The module download above is declared before the target-specific ARGs so +# that BuildKit shares its layer across every target platform of one build. +# TARGETOS and TARGETARCH are populated automatically by BuildKit for each +# platform being built, so a single multi-platform `docker buildx build` +# produces a correctly built binary per architecture. +ARG TARGETOS +ARG TARGETARCH +ARG BUILDARCH + +# GOEXPERIMENT=systemcrypto requires CGO_ENABLED=1. Its OpenSSL backend +# dlopen()s libcrypto at run time, so no OpenSSL headers are needed to build - +# only a C compiler and libc headers for the target architecture. When the +# target differs from the architecture this stage runs on, install Debian's +# cross toolchain for it. Either way /usr/local/bin/target-gcc ends up pointing +# at the right compiler. BUILDARCH is this stage's own architecture because the +# stage is pinned to --platform=$BUILDPLATFORM above. +RUN set -eu; \ + if [ "${TARGETARCH}" = "${BUILDARCH}" ]; then \ + ln -s /usr/bin/gcc /usr/local/bin/target-gcc; \ + else \ + case "${TARGETARCH}" in \ + arm64) pkg=gcc-aarch64-linux-gnu; triple=aarch64-linux-gnu ;; \ + amd64) pkg=gcc-x86-64-linux-gnu; triple=x86_64-linux-gnu ;; \ + *) echo "unsupported TARGETARCH ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + apt-get update; \ + apt-get install -y --no-install-recommends "${pkg}" "libc6-dev-${TARGETARCH}-cross"; \ + rm -rf /var/lib/apt/lists/*; \ + ln -s "/usr/bin/${triple}-gcc" /usr/local/bin/target-gcc; \ + fi + # Copy the go source COPY cmd/memberagent cmd/memberagent/ COPY apis/ apis/ COPY pkg/ pkg/ -# Build -RUN echo "Building images with GOOS=$GOOS GOARCH=$GOARCH" -RUN CGO_ENABLED=1 GOOS=$GOOS GOARCH=$GOARCH GOEXPERIMENT=systemcrypto GO111MODULE=on go build -o memberagent cmd/memberagent/main.go +# Build for the target platform with the compiler selected above. +RUN echo "Building memberagent with GOOS=${TARGETOS} GOARCH=${TARGETARCH} CC=$(readlink -f /usr/local/bin/target-gcc)" && \ + CGO_ENABLED=1 CC=target-gcc GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOEXPERIMENT=systemcrypto go build -o memberagent ./cmd/memberagent/ -# Use Azure Linux distroless base image to package the memberagent binary -# Refer to https://mcr.microsoft.com/en-us/artifact/mar/azurelinux/distroless/base/about for more details -FROM mcr.microsoft.com/azurelinux/distroless/base:3.0 +# Use distroless as minimal base image to package the memberagent binary. +# The pinned digest must reference a multi-arch image index so BuildKit can +# resolve the matching base layer for each target architecture. +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/base:nonroot@sha256:2d7d29b504e7166f6d0c7655a18ebf5def5b37b029f8c4f8667e434ba774844f WORKDIR / -COPY --from=builder /workspace/memberagent . +COPY --link --from=builder /workspace/memberagent . USER 65532:65532 ENTRYPOINT ["/memberagent"] diff --git a/docker/refresh-token.Dockerfile b/docker/refresh-token.Dockerfile index 1eca4fea3..75b2a98a8 100644 --- a/docker/refresh-token.Dockerfile +++ b/docker/refresh-token.Dockerfile @@ -1,8 +1,14 @@ -# Build the refreshtoken binary -FROM mcr.microsoft.com/oss/go/microsoft/golang:1.26.6-1 AS builder - -ARG GOOS="linux" -ARG GOARCH="amd64" +# syntax=docker/dockerfile:1 +# Build the refreshtoken binary. +# +# The builder stage always runs on the build host's own platform +# (--platform=$BUILDPLATFORM) and cross-compiles for each target platform, so +# the Go toolchain, cgo and gcc never execute under QEMU. Running the builder +# per target under emulation crashed gcc's cc1 ("internal compiler error: +# Segmentation fault" / "cgo: gcc produced no output" in runtime/cgo and net) +# and, when it did not crash, took over an hour per image. Only the final +# distroless stage is per-target, and it runs no commands. +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/oss/go/microsoft/golang:1.26.6-1 AS builder WORKDIR /workspace # Copy the Go Modules manifests @@ -12,23 +18,54 @@ COPY go.sum go.sum # and so that source changes don't invalidate our downloaded layer RUN go mod download +# The module download above is declared before the target-specific ARGs so +# that BuildKit shares its layer across every target platform of one build. +# TARGETOS and TARGETARCH are populated automatically by BuildKit for each +# platform being built, so a single multi-platform `docker buildx build` +# produces a correctly built binary per architecture. +ARG TARGETOS +ARG TARGETARCH +ARG BUILDARCH + +# GOEXPERIMENT=systemcrypto requires CGO_ENABLED=1. Its OpenSSL backend +# dlopen()s libcrypto at run time, so no OpenSSL headers are needed to build - +# only a C compiler and libc headers for the target architecture. When the +# target differs from the architecture this stage runs on, install Debian's +# cross toolchain for it. Either way /usr/local/bin/target-gcc ends up pointing +# at the right compiler. BUILDARCH is this stage's own architecture because the +# stage is pinned to --platform=$BUILDPLATFORM above. +RUN set -eu; \ + if [ "${TARGETARCH}" = "${BUILDARCH}" ]; then \ + ln -s /usr/bin/gcc /usr/local/bin/target-gcc; \ + else \ + case "${TARGETARCH}" in \ + arm64) pkg=gcc-aarch64-linux-gnu; triple=aarch64-linux-gnu ;; \ + amd64) pkg=gcc-x86-64-linux-gnu; triple=x86_64-linux-gnu ;; \ + *) echo "unsupported TARGETARCH ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + apt-get update; \ + apt-get install -y --no-install-recommends "${pkg}" "libc6-dev-${TARGETARCH}-cross"; \ + rm -rf /var/lib/apt/lists/*; \ + ln -s "/usr/bin/${triple}-gcc" /usr/local/bin/target-gcc; \ + fi + # Copy the go source COPY cmd/authtoken/main.go main.go COPY pkg/authtoken pkg/authtoken # writefile is a dependency of pkg/authtoken for secure file creation (0600 permissions) COPY pkg/utils/writefile pkg/utils/writefile -ARG TARGETARCH - -# Build -RUN echo "Building images with GOOS=${GOOS} GOARCH=${GOARCH}" -RUN CGO_ENABLED=1 GOOS=$GOOS GOARCH=$GOARCH GOEXPERIMENT=systemcrypto GO111MODULE=on go build -o refreshtoken main.go +# Build for the target platform with the compiler selected above. +RUN echo "Building refreshtoken with GOOS=${TARGETOS} GOARCH=${TARGETARCH} CC=$(readlink -f /usr/local/bin/target-gcc)" && \ + CGO_ENABLED=1 CC=target-gcc GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOEXPERIMENT=systemcrypto go build -o refreshtoken . -# Use Azure Linux distroless base image to package the refreshtoken binary -# Refer to https://mcr.microsoft.com/en-us/artifact/mar/azurelinux/distroless/base/about for more details -FROM mcr.microsoft.com/azurelinux/distroless/base:3.0 +# Use distroless as minimal base image to package the refreshtoken binary. +# The pinned digest must reference a multi-arch image index so BuildKit can +# resolve the matching base layer for each target architecture. +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/base:nonroot@sha256:2d7d29b504e7166f6d0c7655a18ebf5def5b37b029f8c4f8667e434ba774844f WORKDIR / -COPY --from=builder /workspace/refreshtoken . +COPY --link --from=builder /workspace/refreshtoken . USER 65532:65532 ENTRYPOINT ["/refreshtoken"] diff --git a/pkg/controllers/clusterinventory/clusterprofile/controller.go b/pkg/controllers/clusterinventory/clusterprofile/controller.go index 1e92e4858..a0f6ab57c 100644 --- a/pkg/controllers/clusterinventory/clusterprofile/controller.go +++ b/pkg/controllers/clusterinventory/clusterprofile/controller.go @@ -27,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + clientcmdv1 "k8s.io/client-go/tools/clientcmd/api/v1" "k8s.io/client-go/util/workqueue" "k8s.io/klog/v2" clusterinventory "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" @@ -180,8 +181,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Resu return ctrl.Result{}, nil } -// fillInClusterStatus fills in the ClusterProfile status fields from the MemberCluster status. -// Currently, it only fills in the Kubernetes version field. +// fillInClusterStatus fills in the ClusterProfile status fields from the MemberCluster status, +// including the Kubernetes version and, when available, the cluster access provider. func (r *Reconciler) fillInClusterStatus(mc *clusterv1beta1.MemberCluster, cp *clusterinventory.ClusterProfile) { clusterPropertyCondition := meta.FindStatusCondition(mc.Status.Conditions, string(clusterv1beta1.ConditionTypeClusterPropertyCollectionSucceeded)) if !condition.IsConditionStatusTrue(clusterPropertyCondition, mc.Generation) { @@ -196,27 +197,26 @@ func (r *Reconciler) fillInClusterStatus(mc *clusterv1beta1.MemberCluster, cp *c Kubernetes: k8sversion.Value, } } - // Add the class access provider, we only have one so far - cp.Status.AccessProviders = []clusterinventory.AccessProvider{ - { - Name: controller.ClusterManagerName, - }, - } - // TODO throw and unexpected error if clusterEntryPoint is not found - // We don't have a way to get it yet - clusterEntry, exists := mc.Status.Properties[propertyprovider.ClusterEntryPointProperty] - if exists { - klog.V(3).InfoS("Get Kubernetes cluster entry point from member cluster status", "clusterEntryPoint", clusterEntry.Value, "clusterProfile", klog.KObj(cp)) - cp.Status.AccessProviders[0].Cluster.Server = clusterEntry.Value - } - // Get the CA Data - certificateAuthorityData, exists := mc.Status.Properties[propertyprovider.ClusterCertificateAuthorityProperty] - if exists { - klog.V(3).InfoS("Get Kubernetes cluster certificate authority data from member cluster status", "clusterProfile", klog.KObj(cp)) - cp.Status.AccessProviders[0].Cluster.CertificateAuthorityData = []byte(certificateAuthorityData.Value) + + // Add cluster access provider, if and only if a cluster entry point and the CA data exist as part of the + // cluster properties. + clusterEntrypoint, entryPtExists := mc.Status.Properties[propertyprovider.ClusterEntryPointProperty] + caData, caDataExists := mc.Status.Properties[propertyprovider.ClusterCertificateAuthorityProperty] + if entryPtExists && caDataExists && len(clusterEntrypoint.Value) > 0 && len(caData.Value) > 0 { + cp.Status.AccessProviders = []clusterinventory.AccessProvider{ + { + Name: controller.ClusterManagerName, + Cluster: clientcmdv1.Cluster{ + Server: clusterEntrypoint.Value, + CertificateAuthorityData: []byte(caData.Value), + }, + }, + } } else { - // throw an alert - _ = controller.NewUnexpectedBehaviorError(fmt.Errorf("cluster certificate authority data not found in member cluster %s status", mc.Name)) + cp.Status.AccessProviders = nil + klog.V(2).InfoS("Cluster entry point and/or CA data is missing or empty; reset cluster access provider to cluster profile status", + "memberCluster", klog.KObj(mc), "clusterProfile", klog.KObj(cp), + "clusterEntryPointExists", entryPtExists, "caDataExists", caDataExists) } } diff --git a/pkg/controllers/clusterinventory/clusterprofile/controller_integration_test.go b/pkg/controllers/clusterinventory/clusterprofile/controller_integration_test.go index e97e35646..e8d50d26b 100644 --- a/pkg/controllers/clusterinventory/clusterprofile/controller_integration_test.go +++ b/pkg/controllers/clusterinventory/clusterprofile/controller_integration_test.go @@ -171,6 +171,10 @@ var _ = Describe("Test ClusterProfile Controller", func() { }, } mc.Status.Properties = map[clusterv1beta1.PropertyName]clusterv1beta1.PropertyValue{ + propertyprovider.ClusterEntryPointProperty: { + Value: "https://dummy-cluster-endpoint", + ObservationTime: metav1.Time{Time: time.Now()}, + }, propertyprovider.ClusterCertificateAuthorityProperty: { Value: "dummy-ca-data", ObservationTime: metav1.Time{Time: time.Now()}, diff --git a/pkg/controllers/clusterinventory/clusterprofile/controller_test.go b/pkg/controllers/clusterinventory/clusterprofile/controller_test.go index 3574f4607..9feebbe8c 100644 --- a/pkg/controllers/clusterinventory/clusterprofile/controller_test.go +++ b/pkg/controllers/clusterinventory/clusterprofile/controller_test.go @@ -23,6 +23,7 @@ import ( "github.com/google/go-cmp/cmp" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientcmdv1 "k8s.io/client-go/tools/clientcmd/api/v1" clusterinventory "sigs.k8s.io/cluster-inventory-api/apis/v1alpha1" clusterv1beta1 "go.goms.io/fleet/apis/cluster/v1beta1" @@ -84,7 +85,7 @@ func TestFillInClusterStatus(t *testing.T) { }, clusterProfile: &clusterinventory.ClusterProfile{}, expectVersion: false, - expectAccessProvider: true, + expectAccessProvider: false, }, { name: "Cluster property collection succeeded with k8s version only", @@ -111,7 +112,7 @@ func TestFillInClusterStatus(t *testing.T) { clusterProfile: &clusterinventory.ClusterProfile{}, expectVersion: true, expectedK8sVersion: "v1.28.0", - expectAccessProvider: true, + expectAccessProvider: false, }, { name: "Cluster property collection succeeded with all properties", @@ -176,8 +177,79 @@ func TestFillInClusterStatus(t *testing.T) { clusterProfile: &clusterinventory.ClusterProfile{}, expectVersion: true, expectedK8sVersion: "v1.27.5", - expectAccessProvider: true, - expectedServer: "https://api.partial-cluster.example.com:6443", + expectAccessProvider: false, + }, + { + name: "Cluster property collection succeeded but access provider properties are empty", + memberCluster: &clusterv1beta1.MemberCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + Generation: 1, + }, + Status: clusterv1beta1.MemberClusterStatus{ + Conditions: []metav1.Condition{ + { + Type: string(clusterv1beta1.ConditionTypeClusterPropertyCollectionSucceeded), + Status: metav1.ConditionTrue, + ObservedGeneration: 1, + }, + }, + Properties: map[clusterv1beta1.PropertyName]clusterv1beta1.PropertyValue{ + propertyprovider.K8sVersionProperty: { + Value: "v1.30.0", + }, + propertyprovider.ClusterEntryPointProperty: { + Value: "", + }, + propertyprovider.ClusterCertificateAuthorityProperty: { + Value: "", + }, + }, + }, + }, + clusterProfile: &clusterinventory.ClusterProfile{}, + expectVersion: true, + expectedK8sVersion: "v1.30.0", + expectAccessProvider: false, + }, + { + name: "Access provider properties missing resets a previously populated access provider", + memberCluster: &clusterv1beta1.MemberCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-cluster", + Generation: 1, + }, + Status: clusterv1beta1.MemberClusterStatus{ + Conditions: []metav1.Condition{ + { + Type: string(clusterv1beta1.ConditionTypeClusterPropertyCollectionSucceeded), + Status: metav1.ConditionTrue, + ObservedGeneration: 1, + }, + }, + Properties: map[clusterv1beta1.PropertyName]clusterv1beta1.PropertyValue{ + propertyprovider.K8sVersionProperty: { + Value: "v1.31.0", + }, + }, + }, + }, + clusterProfile: &clusterinventory.ClusterProfile{ + Status: clusterinventory.ClusterProfileStatus{ + AccessProviders: []clusterinventory.AccessProvider{ + { + Name: controller.ClusterManagerName, + Cluster: clientcmdv1.Cluster{ + Server: "https://api.stale-cluster.example.com:6443", + CertificateAuthorityData: []byte("c3RhbGUtY2EtZGF0YQ=="), + }, + }, + }, + }, + }, + expectVersion: true, + expectedK8sVersion: "v1.31.0", + expectAccessProvider: false, }, } diff --git a/test/e2e/join_and_leave_test.go b/test/e2e/join_and_leave_test.go index 0235e142e..269e6ff17 100644 --- a/test/e2e/join_and_leave_test.go +++ b/test/e2e/join_and_leave_test.go @@ -35,7 +35,6 @@ import ( clusterv1beta1 "go.goms.io/fleet/apis/cluster/v1beta1" placementv1beta1 "go.goms.io/fleet/apis/placement/v1beta1" "go.goms.io/fleet/pkg/utils" - "go.goms.io/fleet/pkg/utils/controller" ) const ( @@ -462,14 +461,10 @@ var _ = Describe("Test member cluster join and leave with clusterProfile", Label if cp.Status.Version.Kubernetes == "" { return fmt.Errorf("cluster profile %s Kubernetes version should not be empty", cp.Name) } - if len(cp.Status.AccessProviders) != 1 { - return fmt.Errorf("cluster profile %s has no access providers %+v", cp.Name, cp.Status.AccessProviders) - } - if cp.Status.AccessProviders[0].Name != controller.ClusterManagerName { - return fmt.Errorf("cluster profile %s access provider name %s doesn't match expected %s", cp.Name, cp.Status.AccessProviders[0].Name, controller.ClusterManagerName) - } - if len(cp.Status.AccessProviders[0].Cluster.CertificateAuthorityData) == 0 { - return fmt.Errorf("cluster profile %s access provider certificate authority data should not be empty", allMemberClusterNames[idx]) + if len(cp.Status.AccessProviders) != 0 { + // Note: at this moment the Azure property provider does not expose cluster FQDNs, and + // as a result no access provider will be populated. + return fmt.Errorf("cluster profile %s has access providers %+v", cp.Name, cp.Status.AccessProviders) } } return nil