Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 36 additions & 13 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,11 @@ UNBOUNDED_OPERATOR_API_SERVER_ENDPOINT ?=
UNBOUNDED_OPERATOR_IMAGE_REGISTRY ?= $(CONTAINER_REGISTRY)
UNBOUNDED_OPERATOR_REAP_LEGACY_RESOURCES ?= true
export UNBOUNDED_OPERATOR_API_SERVER_ENDPOINT
UNBOUNDED_OPERATOR_MANIFEST_TEMPLATES_DIR := deploy/unbounded-operator
UNBOUNDED_OPERATOR_MANIFEST_RENDERED_DIR := deploy/unbounded-operator/rendered
UNBOUNDED_OPERATOR_CHART_DIR := deploy/unbounded-operator/chart
UNBOUNDED_OPERATOR_RENDER_DIR := tmp/unbounded-operator-render
UNBOUNDED_OPERATOR_CHART_VERSION ?= 0.0.0-dev
UNBOUNDED_OPERATOR_CHART_APP_VERSION ?= $(VERSION_TAG)

TOKEN_REFRESHER_NAMESPACE ?= $(UNBOUNDED_NAMESPACE)
TOKEN_REFRESHER_MANIFEST_TEMPLATES_DIR := deploy/token-refresher
Expand Down Expand Up @@ -235,7 +238,7 @@ NET_FRONTEND_CACHE_FILE := $(NET_FRONTEND_DIST_DIR)/.frontend-build-key
REACT_DEV ?= false

.PHONY: all help fmt lint lint-actions test build vulncheck check-deps kubectl-unbounded kubectl-unbounded-build install-tools install-protoc install-helm install-kind generate kubectl-unbounded forge relctl relctl-build agent-artifacts-builder agent-artifacts-builder-build orcadev unbounded-agent machina machina-build machina-oci machina-oci-push machina-manifests machine-ops-controller machine-ops-controller-build machine-ops-controller-oci machine-ops-controller-oci-push machine-ops-manifests metalman metalman-build metalman-oci metalman-oci-push unbounded-operator unbounded-operator-build unbounded-operator-manifests playpen-manifests e2e-gantry e2e-playpen e2e-racer gomod docs-serve unbounded-net-controller unbounded-net-controller-build unbounded-net-node unbounded-net-node-build unbounded-net-routeplan-debug unping unping-build unroute unroute-build license-check notice notice-check gantry gantry-build gantry-manifests inventory-manifests
.PHONY: net-frontend net-frontend-clean net-ebpf-build net-ebpf-generate net-ebpf-verify net-manifests gantry-chart-lint gantry-chart-package release-bom release-manifests unbounded-operator-release-manifest
.PHONY: net-frontend net-frontend-clean net-ebpf-build net-ebpf-generate net-ebpf-verify net-manifests gantry-chart-lint gantry-chart-package unbounded-operator-chart-lint unbounded-operator-chart-package release-bom release-manifests unbounded-operator-release-manifest
.PHONY: image-machina-local image-token-refresher-local image-machine-ops-controller-local image-metalman-local image-unbounded-operator-local image-unbounded-operator-push image-playpen-local image-net-controller-local image-net-node-local image-gantry-local image-gantry-push images-local
.PHONY: image-net-controller-push image-net-node-push images-net-all images-net-all-push

Expand Down Expand Up @@ -349,6 +352,8 @@ help: ## Show this help
@echo " unbounded-operator-manifests Render unbounded-operator manifests into deploy/unbounded-operator/rendered"
@echo " gantry-chart-lint Validate the standalone Gantry Helm chart"
@echo " gantry-chart-package Package the standalone Gantry Helm chart"
@echo " unbounded-operator-chart-lint Validate the unbounded-operator Helm chart"
@echo " unbounded-operator-chart-package Package the unbounded-operator Helm chart"
@echo " unbounded-operator-release-manifest Build a versioned, directly applicable operator manifest under build/"
@echo ""
@echo "Net Kubernetes (apply to current kubectl context):"
Expand Down Expand Up @@ -569,13 +574,13 @@ ifdef CI
# In CI each job is independent; skip chained prerequisites.

test: machina-manifests token-refresher-manifests machine-ops-manifests playpen-manifests net-manifests unbounded-operator-manifests gantry-manifests ## Run all tests with race detector
$(GOTEST) -race ./...
HELM=$(HELM) $(GOTEST) -race ./...

else
# Locally, chain test -> lint for convenience.

test: lint machina-manifests token-refresher-manifests machine-ops-manifests playpen-manifests net-manifests unbounded-operator-manifests gantry-manifests ## Run all tests (implies lint)
$(GOTEST) ./...
HELM=$(HELM) $(GOTEST) ./...

endif

Expand Down Expand Up @@ -1226,19 +1231,37 @@ token-refresher-manifests: ## Render token-refresher manifests into deploy/token
--set Image=$(TOKEN_REFRESHER_IMAGE)
@echo "Rendered token-refresher manifests into $(TOKEN_REFRESHER_MANIFEST_RENDERED_DIR) (image: $(TOKEN_REFRESHER_IMAGE))"

unbounded-operator-manifests: ## Render unbounded-operator manifests into deploy/unbounded-operator/rendered
unbounded-operator-manifests: $(HELM) ## Render unbounded-operator manifests into deploy/unbounded-operator/rendered
@mkdir -p $(UNBOUNDED_OPERATOR_MANIFEST_RENDERED_DIR)
@find $(UNBOUNDED_OPERATOR_MANIFEST_RENDERED_DIR) -mindepth 1 -not -name .gitignore -delete
$(GOCMD) run ./hack/cmd/render-manifests \
--templates-dir $(UNBOUNDED_OPERATOR_MANIFEST_TEMPLATES_DIR) \
--output-dir $(UNBOUNDED_OPERATOR_MANIFEST_RENDERED_DIR) \
--set Namespace=$(UNBOUNDED_OPERATOR_NAMESPACE) \
--set OperatorImage=$(UNBOUNDED_OPERATOR_IMAGE) \
--set ImageRegistry=$(UNBOUNDED_OPERATOR_IMAGE_REGISTRY) \
--set "APIServerEndpoint=$${UNBOUNDED_OPERATOR_API_SERVER_ENDPOINT}" \
--set ReapLegacyResources=$(UNBOUNDED_OPERATOR_REAP_LEGACY_RESOURCES)
@rm -rf $(UNBOUNDED_OPERATOR_RENDER_DIR)
@mkdir -p $(UNBOUNDED_OPERATOR_RENDER_DIR)
@# --set-file passes the endpoint verbatim; --set-string would split on commas.
@printf '%s' "$${UNBOUNDED_OPERATOR_API_SERVER_ENDPOINT}" > $(UNBOUNDED_OPERATOR_RENDER_DIR)/api-server-endpoint
$(HELM) template unbounded-operator $(UNBOUNDED_OPERATOR_CHART_DIR) \
--namespace $(UNBOUNDED_OPERATOR_NAMESPACE) \
--values $(UNBOUNDED_OPERATOR_CHART_DIR)/values-static.yaml \
--skip-schema-validation \
--set-string image.reference=$(UNBOUNDED_OPERATOR_IMAGE) \
--set-string imageRegistry=$(UNBOUNDED_OPERATOR_IMAGE_REGISTRY) \
--set-file apiServerEndpoint=$(UNBOUNDED_OPERATOR_RENDER_DIR)/api-server-endpoint \
--set reapLegacyResources=$(UNBOUNDED_OPERATOR_REAP_LEGACY_RESOURCES) \
--output-dir $(UNBOUNDED_OPERATOR_RENDER_DIR)/out
@cp $(UNBOUNDED_OPERATOR_RENDER_DIR)/out/unbounded-operator/templates/*.yaml $(UNBOUNDED_OPERATOR_MANIFEST_RENDERED_DIR)/
@rm -rf $(UNBOUNDED_OPERATOR_RENDER_DIR)
@echo "Rendered unbounded-operator manifests into $(UNBOUNDED_OPERATOR_MANIFEST_RENDERED_DIR) (image: $(UNBOUNDED_OPERATOR_IMAGE))"

unbounded-operator-chart-lint: $(HELM) ## Validate the unbounded-operator Helm chart
$(HELM) lint --strict $(UNBOUNDED_OPERATOR_CHART_DIR) --namespace $(UNBOUNDED_OPERATOR_NAMESPACE)

unbounded-operator-chart-package: unbounded-operator-chart-lint ## Package the unbounded-operator Helm chart
@mkdir -p $(GANTRY_CHART_PACKAGE_DIR)
@rm -f $(GANTRY_CHART_PACKAGE_DIR)/unbounded-operator-$(UNBOUNDED_OPERATOR_CHART_VERSION).tgz
$(HELM) package $(UNBOUNDED_OPERATOR_CHART_DIR) \
--version $(UNBOUNDED_OPERATOR_CHART_VERSION) \
--app-version $(UNBOUNDED_OPERATOR_CHART_APP_VERSION) \
--destination $(GANTRY_CHART_PACKAGE_DIR)

machine-ops-manifests: ## Render machine-ops-controller manifests into deploy/machine-ops/rendered
@mkdir -p $(MACHINE_OPS_MANIFEST_RENDERED_DIR)
@find $(MACHINE_OPS_MANIFEST_RENDERED_DIR) -mindepth 1 -not -name .gitignore -delete
Expand Down
60 changes: 60 additions & 0 deletions cmd/kubectl-unbounded/app/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ func (h *installHandler) execute(ctx context.Context) error {
}
}

if err := h.refuseHelmManagedOperator(ctx); err != nil {
return err
}

if err := h.prepareOperatorConfig(ctx); err != nil {
return err
}
Expand Down Expand Up @@ -175,6 +179,62 @@ func (h *installHandler) execute(ctx context.Context) error {
return nil
}

// helmReleaseAnnotation marks objects owned by a Helm release, including the
// unbounded-operator chart installed by Helm or an AKS cluster extension.
const helmReleaseAnnotation = "meta.helm.sh/release-name"

// refuseHelmManagedOperator stops install from server-side applying over an
// operator that a Helm release owns. Applying with ForceOwnership would flip
// the operator image back and forth with every helm upgrade, and the shared
// ClusterRoleBinding would point at whichever operator was installed last,
// leaving the other one without RBAC.
func (h *installHandler) refuseHelmManagedOperator(ctx context.Context) error {
checks := []struct {
apiVersion, kind string
key client.ObjectKey
}{
{"apps/v1", "Deployment", client.ObjectKey{Namespace: h.namespace, Name: "unbounded-operator"}},
{"rbac.authorization.k8s.io/v1", "ClusterRoleBinding", client.ObjectKey{Name: "unbounded-operator"}},
}

for _, c := range checks {
obj := &unstructured.Unstructured{}
obj.SetAPIVersion(c.apiVersion)
obj.SetKind(c.kind)

if err := h.kubeResourcesCli.Get(ctx, c.key, obj); err != nil {
if apierrors.IsNotFound(err) {
continue
}

return fmt.Errorf("inspect %s %s: %w", c.kind, c.key, err)
}

release := obj.GetAnnotations()[helmReleaseAnnotation]
if release == "" {
continue
}

where := obj.GetAnnotations()["meta.helm.sh/release-namespace"]
if where == "" {
where = obj.GetNamespace()
}

ref := c.key.Name
if c.key.Namespace != "" {
ref = c.key.String()
}

return fmt.Errorf(
"%s %s is managed by Helm release %q in namespace %q (for example the AKS flex operator extension); "+
"upgrade or remove it with helm or the extension instead of kubectl unbounded install",
c.kind, ref, release, where,
)
}

return nil
}

func (h *installHandler) initializeClients() error {
h.kubeconfigPath = getKubeconfigPath(h.kubeconfigPath)
if !isReadableFile(h.kubeconfigPath) {
Expand Down
65 changes: 65 additions & 0 deletions cmd/kubectl-unbounded/app/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
rbacv1 "k8s.io/api/rbac/v1"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
Expand Down Expand Up @@ -66,6 +67,70 @@ func TestInstallRejectsLegacyNamespace(t *testing.T) {
}
}

func TestInstallRefusesHelmManagedOperator(t *testing.T) {
helmAnnotations := map[string]string{
"meta.helm.sh/release-name": "flex-operator",
"meta.helm.sh/release-namespace": "flex-system",
}

for name, existing := range map[string]client.Object{
// Same namespace: install would overwrite the Helm release's Deployment.
"deployment in target namespace": &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{
Name: "unbounded-operator", Namespace: "unbounded-system", Annotations: helmAnnotations,
}},
// Different namespace: install would repoint the shared ClusterRoleBinding
// and strip the Helm-managed operator of its RBAC.
"cluster role binding": &rbacv1.ClusterRoleBinding{ObjectMeta: metav1.ObjectMeta{
Name: "unbounded-operator", Annotations: helmAnnotations,
}},
} {
t.Run(name, func(t *testing.T) {
applied := 0
cli := fakeclient.NewClientBuilder().
WithObjects(existing).
WithInterceptorFuncs(interceptor.Funcs{
Apply: func(context.Context, client.WithWatch, runtime.ApplyConfiguration, ...client.ApplyOption) error {
applied++
return nil
},
}).
Build()

h := installHandler{
namespace: "unbounded-system",
operatorImage: "unbounded-operator:test",
imageRegistry: "registry.example.com",
kubeResourcesCli: cli,
logger: discardLogger(),
}

err := h.execute(context.Background())
require.ErrorContains(t, err, `managed by Helm release "flex-operator"`)
require.Zero(t, applied, "install must not apply anything over a Helm-managed operator")
})
}
}

func TestInstallReappliesOwnInstall(t *testing.T) {
// A previous `kubectl unbounded install` has no Helm annotations, so a
// reinstall still goes through.
cli, captured := newCapturingInstallClient(
&appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "unbounded-operator", Namespace: "unbounded-system"}},
&rbacv1.ClusterRoleBinding{ObjectMeta: metav1.ObjectMeta{Name: "unbounded-operator"}},
)

h := installHandler{
namespace: "unbounded-system",
operatorImage: "unbounded-operator:test",
imageRegistry: "registry.example.com",
kubeResourcesCli: cli,
logger: discardLogger(),
}

require.NoError(t, h.execute(context.Background()))
require.NotNil(t, captured.deployment)
}

func TestInstallHandlerApplyBootstrapManifests(t *testing.T) {
var (
mu sync.Mutex
Expand Down
10 changes: 0 additions & 10 deletions deploy/unbounded-operator/00-namespace.yaml.tmpl

This file was deleted.

12 changes: 0 additions & 12 deletions deploy/unbounded-operator/01-serviceaccount.yaml.tmpl

This file was deleted.

2 changes: 2 additions & 0 deletions deploy/unbounded-operator/chart/.helmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Only used by `make unbounded-operator-manifests`; not part of the packaged chart.
values-static.yaml
6 changes: 6 additions & 0 deletions deploy/unbounded-operator/chart/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: v2
name: unbounded-operator
description: Unbounded operator, which installs Unbounded CRDs and reconciles components from Sites
type: application
version: 0.1.0
appVersion: dev
12 changes: 12 additions & 0 deletions deploy/unbounded-operator/chart/templates/00-namespace.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{{- if .Values.namespace.create }}
# Only the static `kubectl apply` render creates the namespace (values-static.yaml,
# rendered with --skip-schema-validation). The schema pins namespace.create to
# false so a Helm or AKS extension install can't take ownership of the namespace
# that holds every operator-managed component.
apiVersion: v1
kind: Namespace
metadata:
name: {{ .Release.Namespace }}
labels:
app.kubernetes.io/name: unbounded-operator
{{- end }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: unbounded-operator
namespace: {{ .Release.Namespace }}
labels:
{{- include "unbounded-operator.labels" . | nindent 4 }}
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0

---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: unbounded-operator
labels:
app.kubernetes.io/name: unbounded-operator
app.kubernetes.io/component: controller
{{- include "unbounded-operator.labels" . | nindent 4 }}
rules:
- apiGroups: ["racer.unbounded-cloud.io"]
resources: ["clustercaches"]
Expand Down Expand Up @@ -121,26 +116,24 @@ kind: ClusterRoleBinding
metadata:
name: unbounded-operator
labels:
app.kubernetes.io/name: unbounded-operator
app.kubernetes.io/component: controller
{{- include "unbounded-operator.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: unbounded-operator
subjects:
- kind: ServiceAccount
name: unbounded-operator
namespace: {{ default "unbounded-system" .Namespace }}
namespace: {{ .Release.Namespace }}

---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: unbounded-operator-registry-auth-migration
namespace: {{ default "unbounded-system" .Namespace }}
namespace: {{ .Release.Namespace }}
labels:
app.kubernetes.io/name: unbounded-operator
app.kubernetes.io/component: controller
{{- include "unbounded-operator.labels" . | nindent 4 }}
rules:
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
Expand All @@ -152,15 +145,14 @@ apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: unbounded-operator-registry-auth-migration
namespace: {{ default "unbounded-system" .Namespace }}
namespace: {{ .Release.Namespace }}
labels:
app.kubernetes.io/name: unbounded-operator
app.kubernetes.io/component: controller
{{- include "unbounded-operator.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: unbounded-operator-registry-auth-migration
subjects:
- kind: ServiceAccount
name: unbounded-operator
namespace: {{ default "unbounded-system" .Namespace }}
namespace: {{ .Release.Namespace }}
Loading
Loading