diff --git a/Makefile b/Makefile index c1d1b26..bedaf70 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,14 @@ .DEFAULT_GOAL := help -# Possible envs are gcp, e2e-gcp, kind, e2e-kind +# Possible envs are gcp, e2e-gcp, kind, e2e-kind, oci # Default to gcp HELMFILE_ENV ?= gcp -ifeq ($(findstring gcp,$(HELMFILE_ENV)),) +ifeq ($(HELMFILE_ENV),oci) + -include env.oci +else ifeq ($(findstring gcp,$(HELMFILE_ENV)),) -include env.kind else -include env.gcp @@ -273,8 +275,9 @@ check-helmfile-env-generated: ## Check that the generated directory exists based elif [ "$(HELMFILE_ENV)" = "kind" ]; then \ test -d $(GENERATED_RABBITMQ_DIR) || { echo "ERROR: generated-values-rabbitmq directory does not exist"; exit 1; }; \ echo "OK: generated-values-rabbitmq directory exists"; \ + else \ + echo "OK: no generated values needed for environment: $(HELMFILE_ENV)"; \ fi - @echo "OK: Did not need to validate generated values for environment: $(HELMFILE_ENV)" .PHONY: check-kubectl-context @@ -299,9 +302,12 @@ check-kubectl-context: check-kubectl ## Verify kubectl context matches HELMFILE_ exit 1; \ fi \ ;; \ + oci) \ + echo "OK: connected to OCI/OKE cluster (context: $$CONTEXT)"; \ + ;; \ *) \ echo "ERROR: invalid HELMFILE_ENV: $(HELMFILE_ENV)"; \ - echo " Valid values: gcp, e2e-gcp, kind, e2e-kind"; \ + echo " Valid values: gcp, e2e-gcp, kind, e2e-kind, oci"; \ exit 1 \ ;; \ esac \ @@ -372,7 +378,7 @@ help: ## Show this help message @echo "" @echo "Usage: make [target] [VARIABLE=value ...]" @echo "" - @echo "Environment: HELMFILE_ENV=$(HELMFILE_ENV) (gcp|kind|e2e-gcp|e2e-kind)" + @echo "Environment: HELMFILE_ENV=$(HELMFILE_ENV) (gcp|kind|e2e-gcp|e2e-kind|oci)" @echo "" @awk '/^# ====/ { \ section = $$0; \ @@ -397,6 +403,16 @@ help: ## Show this help message +# ==== OCI/OKE Deployment Targets ==== + +.PHONY: install-all-oci +install-all-oci: check-helmfile-env ## Full OCI/OKE install (rabbitmq + api + sentinel + adapter1 via helmfile) + helmfile -f helmfile/helmfile.yaml.gotmpl -e $(HELMFILE_ENV) apply + +.PHONY: uninstall-all-oci +uninstall-all-oci: check-helmfile-env ## Uninstall all OCI components + helmfile -f helmfile/helmfile.yaml.gotmpl -e $(HELMFILE_ENV) destroy + # ==== CI Targets ==== # ci-dry-run: validation on terraform and helm plugins and maestro helm chart # ci-test: Run terraform install + maestro install + health check on maestro diff --git a/env.oci b/env.oci new file mode 100644 index 0000000..e5b9135 --- /dev/null +++ b/env.oci @@ -0,0 +1,25 @@ +# OCI/OKE Installation Configuration +RABBITMQ_URL ?= "amqp://guest:guest@rabbitmq:5672" +API_SERVICE_TYPE ?= ClusterIP +API_BASE_URL ?= http://hyperfleet-api:8000 + +# Container Registry Configuration +REGISTRY ?= quay.io +API_REPOSITORY ?= openshift-hyperfleet/hyperfleet-api +SENTINEL_REPOSITORY ?= openshift-hyperfleet/hyperfleet-sentinel +ADAPTER_REPOSITORY ?= openshift-hyperfleet/hyperfleet-adapter + +# Helm Charts +CHART_ORG ?= openshift-hyperfleet +API_CHART_REF ?= v0.2.1 +SENTINEL_CHART_REF ?= v0.2.1 +ADAPTER_CHART_REF ?= v0.2.1 + +# Image Tags +API_IMAGE_TAG ?= v0.2.1 +SENTINEL_IMAGE_TAG ?= v0.2.1 +ADAPTER_IMAGE_TAG ?= v0.2.1 +IMAGE_PULL_POLICY ?= Always + +# Kubernetes Namespaces +NAMESPACE ?= hyperfleet diff --git a/helm/adapter-hypershift-kubeconfig/Chart.yaml b/helm/adapter-hypershift-kubeconfig/Chart.yaml new file mode 100644 index 0000000..11168f0 --- /dev/null +++ b/helm/adapter-hypershift-kubeconfig/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: adapter-hypershift-kubeconfig +description: HyperShift kubeconfig adapter - reads HostedCluster kubeconfig and exposes it via the CLM API +type: application +version: 0.1.0 +appVersion: "0.0.0-dev" + +dependencies: + - name: hyperfleet-adapter + version: "2.0.0" + repository: "git+https://github.com/openshift-hyperfleet/hyperfleet-adapter@charts?ref=main" diff --git a/helm/adapter-hypershift-kubeconfig/adapter-config.yaml b/helm/adapter-hypershift-kubeconfig/adapter-config.yaml new file mode 100644 index 0000000..6290bdc --- /dev/null +++ b/helm/adapter-hypershift-kubeconfig/adapter-config.yaml @@ -0,0 +1,26 @@ +# HyperShift kubeconfig adapter deployment configuration +# Reads the HostedCluster kubeconfig Secret and reports it via the CLM API +adapter: + name: adapter-hypershift-kubeconfig + version: "0.2.0" + +debug_config: true +log: + level: debug + +clients: + hyperfleet_api: + base_url: http://hyperfleet-api:8000 + version: v1 + timeout: 10s + retry_attempts: 3 + retry_backoff: exponential + + broker: + subscription_id: "adapter-hypershift-kubeconfig" + topic: "hyperfleet-clusters" + + kubernetes: + api_version: "v1" + # Use the mounted kubeconfig to target the remote HyperShift management cluster + kube_config_path: /etc/hypershift/kubeconfig diff --git a/helm/adapter-hypershift-kubeconfig/adapter-task-config.yaml b/helm/adapter-hypershift-kubeconfig/adapter-task-config.yaml new file mode 100644 index 0000000..bfe36dd --- /dev/null +++ b/helm/adapter-hypershift-kubeconfig/adapter-task-config.yaml @@ -0,0 +1,110 @@ +# HyperShift kubeconfig adapter task configuration +# Discovers the HostedCluster kubeconfig Secret and exposes it via the CLM API. +# Platform-agnostic: works with any HyperShift management cluster. +params: + + - name: "clusterId" + source: "event.id" + type: "string" + required: true + + - name: "generation" + source: "event.generation" + type: "int" + required: true + + - name: "namespace" + source: "env.CLUSTERS_NAMESPACE" + type: "string" + +# Preconditions: fetch cluster name from API +preconditions: + - name: "clusterStatus" + api_call: + method: "GET" + url: "/clusters/{{ .clusterId }}" + timeout: 10s + retry_attempts: 3 + retry_backoff: "exponential" + capture: + - name: "clusterName" + field: "name" + - name: "generation" + field: "generation" + +# Resources: discover the kubeconfig Secret on the management cluster. +# HyperShift creates a Secret named {clusterName}-admin-kubeconfig in the +# clusters namespace. The adapter discovers it without modifying it. +resources: + + - name: "kubeconfigSecret" + transport: + client: "kubernetes" + manifest: + apiVersion: v1 + kind: Secret + metadata: + name: "{{ .clusterName }}-admin-kubeconfig" + namespace: "{{ .namespace }}" + discovery: + namespace: "{{ .namespace }}" + by_name: "{{ .clusterName }}-admin-kubeconfig" + +# Post-processing: report kubeconfig back to the CLM API +post: + payloads: + - name: "statusPayload" + build: + adapter: "{{ .adapter.name }}" + conditions: + - type: "Applied" + status: + expression: | + resources.?kubeconfigSecret.?metadata.?name.hasValue() ? "True" : "False" + reason: + expression: | + resources.?kubeconfigSecret.?metadata.?name.hasValue() ? "KubeconfigFound" : "KubeconfigNotFound" + message: + expression: | + resources.?kubeconfigSecret.?metadata.?name.hasValue() + ? "Kubeconfig secret discovered on management cluster" + : "Kubeconfig secret not yet available" + - type: "Available" + status: + expression: | + resources.?kubeconfigSecret.?data.?kubeconfig.hasValue() ? "True" : "False" + reason: + expression: | + resources.?kubeconfigSecret.?data.?kubeconfig.hasValue() ? "KubeconfigReady" : "KubeconfigPending" + message: + expression: | + resources.?kubeconfigSecret.?data.?kubeconfig.hasValue() + ? "Kubeconfig is available" + : "Waiting for kubeconfig to be populated" + - type: "Health" + status: + expression: | + adapter.?executionStatus.orValue("") == "success" ? "True" : (adapter.?executionStatus.orValue("") == "failed" ? "False" : "Unknown") + reason: + expression: | + adapter.?errorReason.orValue("") != "" ? adapter.?errorReason.orValue("") : "Healthy" + message: + expression: | + adapter.?errorMessage.orValue("") != "" ? adapter.?errorMessage.orValue("") : "Adapter executed successfully" + observed_generation: + expression: "generation" + observed_time: "{{ now | date \"2006-01-02T15:04:05Z07:00\" }}" + data: + kubeconfig: + expression: | + resources.?kubeconfigSecret.?data.?kubeconfig.orValue("") + + post_actions: + - name: "reportClusterStatus" + api_call: + method: "POST" + url: "/clusters/{{ .clusterId }}/statuses" + headers: + - name: "Content-Type" + value: "application/json" + body: "{{ .statusPayload }}" diff --git a/helm/adapter-hypershift-kubeconfig/charts/hyperfleet-adapter-2.0.0.tgz b/helm/adapter-hypershift-kubeconfig/charts/hyperfleet-adapter-2.0.0.tgz new file mode 100644 index 0000000..d4753d0 Binary files /dev/null and b/helm/adapter-hypershift-kubeconfig/charts/hyperfleet-adapter-2.0.0.tgz differ diff --git a/helm/adapter-hypershift-kubeconfig/values.yaml b/helm/adapter-hypershift-kubeconfig/values.yaml new file mode 100644 index 0000000..745abad --- /dev/null +++ b/helm/adapter-hypershift-kubeconfig/values.yaml @@ -0,0 +1,56 @@ +# Values for adapter-hypershift-kubeconfig +# Reads HostedCluster kubeconfig and exposes it via the CLM API + +hyperfleet-adapter: + image: + registry: CHANGE_ME + repository: CHANGE_ME + tag: latest + + adapterConfig: + create: true + log: + level: debug + + adapterTaskConfig: + create: true + + broker: + type: googlepubsub + googlepubsub: + projectId: CHANGE_ME + subscriptionId: CHANGE_ME + topic: CHANGE_ME + deadLetterTopic: "" + createTopicIfMissing: true + createSubscriptionIfMissing: true + rabbitmq: + url: CHANGE_ME + queue: "" + exchange: "" + routingKey: "" + + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: CLUSTERS_NAMESPACE + value: clusters + + # Mount the management cluster kubeconfig + extraVolumeMounts: + - name: hypershift-kubeconfig + mountPath: /etc/hypershift + readOnly: true + + extraVolumes: + - name: hypershift-kubeconfig + secret: + secretName: hypershift-mgmt-kubeconfig + + # RBAC for reading Secrets on the local cluster (discovery uses the mounted kubeconfig) + rbac: + resources: + - configmaps + - secrets diff --git a/helm/adapter-hypershift-nodepool/Chart.yaml b/helm/adapter-hypershift-nodepool/Chart.yaml new file mode 100644 index 0000000..018acec --- /dev/null +++ b/helm/adapter-hypershift-nodepool/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: adapter-hypershift-nodepool +description: HyperShift NodePool adapter - creates NodePool resources on a remote management cluster +type: application +version: 0.1.0 +appVersion: "0.0.0-dev" + +dependencies: + - name: hyperfleet-adapter + version: "2.0.0" + repository: "git+https://github.com/openshift-hyperfleet/hyperfleet-adapter@charts?ref=main" diff --git a/helm/adapter-hypershift-nodepool/adapter-config.yaml b/helm/adapter-hypershift-nodepool/adapter-config.yaml new file mode 100644 index 0000000..1515786 --- /dev/null +++ b/helm/adapter-hypershift-nodepool/adapter-config.yaml @@ -0,0 +1,26 @@ +# HyperShift NodePool adapter deployment configuration +# Creates NodePool resources on a remote HyperShift management cluster +adapter: + name: adapter-hypershift-nodepool + version: "0.2.0" + +debug_config: true +log: + level: debug + +clients: + hyperfleet_api: + base_url: http://hyperfleet-api:8000 + version: v1 + timeout: 10s + retry_attempts: 3 + retry_backoff: exponential + + broker: + subscription_id: "adapter-hypershift-nodepool" + topic: "hyperfleet-nodepools" + + kubernetes: + api_version: "v1" + # Use the mounted kubeconfig to target the remote HyperShift management cluster + kube_config_path: /etc/hypershift/kubeconfig diff --git a/helm/adapter-hypershift-nodepool/adapter-task-config.yaml b/helm/adapter-hypershift-nodepool/adapter-task-config.yaml new file mode 100644 index 0000000..f391c74 --- /dev/null +++ b/helm/adapter-hypershift-nodepool/adapter-task-config.yaml @@ -0,0 +1,211 @@ +# HyperShift NodePool adapter task configuration +# Creates a NodePool resource on the remote management cluster +params: + + - name: "nodepoolId" + source: "event.id" + type: "string" + required: true + + - name: "clusterId" + source: "event.owner_references.id" + type: "string" + required: true + + - name: "generation" + source: "event.generation" + type: "int" + required: true + + - name: "namespace" + source: "env.CLUSTERS_NAMESPACE" + type: "string" + + - name: "ociAD" + source: "env.OCI_AD" + type: "string" + + - name: "ociSubnetId" + source: "env.OCI_SUBNET_ID" + type: "string" + + - name: "ociShape" + source: "env.OCI_SHAPE" + type: "string" + + - name: "ociOcpus" + source: "env.OCI_OCPUS" + type: "string" + + - name: "ociMemoryGBs" + source: "env.OCI_MEMORY_GBS" + type: "string" + + - name: "ociBootVolumeGB" + source: "env.OCI_BOOT_VOLUME_GB" + type: "string" + +# Preconditions: look up nodepool and parent cluster from the API +preconditions: + + # Fetch nodepool details (name, spec, status) + - name: "nodepoolDetails" + api_call: + method: "GET" + url: "/clusters/{{ .clusterId }}/nodepools/{{ .nodepoolId }}" + timeout: 10s + retry_attempts: 3 + retry_backoff: "exponential" + capture: + - name: "nodepoolName" + field: "name" + - name: "generation" + field: "generation" + - name: "nodepoolSpec" + field: "spec" + - name: "nodepoolNotReady" + expression: | + !(status.?conditions.orValue([]).filter(c, c.type == "Ready").size() > 0 + && status.?conditions.orValue([]).filter(c, c.type == "Ready")[0].status == "True") + + # Fetch parent cluster details (need the cluster name for clusterName ref) + - name: "clusterDetails" + api_call: + method: "GET" + url: "/clusters/{{ .clusterId }}" + timeout: 10s + retry_attempts: 3 + retry_backoff: "exponential" + capture: + - name: "clusterName" + field: "name" + + # Check if HostedCluster is Available via adapter-hypershift status + - name: "clusterAdapterStatus" + api_call: + method: "GET" + url: "/clusters/{{ .clusterId }}/statuses" + timeout: 10s + retry_attempts: 3 + retry_backoff: "exponential" + capture: + - name: "clusterAvailable" + expression: | + items.filter(s, s.adapter == "adapter-hypershift").size() > 0 + ? (has(items.filter(s, s.adapter == "adapter-hypershift")[0].conditions) + ? (items.filter(s, s.adapter == "adapter-hypershift")[0].conditions.filter(c, c.type == "Available").size() > 0 + ? items.filter(s, s.adapter == "adapter-hypershift")[0].conditions.filter(c, c.type == "Available")[0].status == "True" + : false) + : false) + : false + + - name: "validationCheck" + # Only proceed if nodepool is NOT Ready AND HostedCluster adapter reports Available + expression: | + nodepoolNotReady && clusterAvailable + +# Resources: NodePool on the remote management cluster +resources: + + - name: "nodePool" + transport: + client: "kubernetes" + manifest: + apiVersion: hypershift.openshift.io/v1beta1 + kind: NodePool + metadata: + name: "{{ .clusterName }}-{{ .nodepoolName }}" + namespace: "{{ .namespace }}" + labels: + hyperfleet.io/cluster-id: "{{ .clusterId }}" + hyperfleet.io/cluster-name: "{{ .clusterName }}" + hyperfleet.io/nodepool-id: "{{ .nodepoolId }}" + hyperfleet.io/nodepool-name: "{{ .nodepoolName }}" + spec: + clusterName: "{{ .clusterName }}" + replicas: '{{ index .nodepoolSpec "replicas" | default 2 }}' + management: + autoRepair: true + upgradeType: Replace + platform: + type: OCI + oci: + instanceShape: '{{ index .nodepoolSpec "instanceShape" | default .ociShape }}' + instanceShapeConfig: + ocpus: '{{ index .nodepoolSpec "ocpus" | default .ociOcpus }}' + memoryInGBs: '{{ index .nodepoolSpec "memoryInGBs" | default .ociMemoryGBs }}' + availabilityDomain: '{{ index .nodepoolSpec "availabilityDomain" | default .ociAD }}' + subnetId: '{{ index .nodepoolSpec "subnetId" | default .ociSubnetId }}' + bootVolumeSize: '{{ index .nodepoolSpec "bootVolumeSize" | default .ociBootVolumeGB }}' + release: + image: '{{ index .nodepoolSpec "releaseImage" | default "quay.io/openshift-release-dev/ocp-release:4.20.2-x86_64" }}' + discovery: + namespace: "{{ .namespace }}" + by_selectors: + label_selector: + hyperfleet.io/nodepool-id: "{{ .nodepoolId }}" + +# Post-processing: report NodePool status back to API +post: + payloads: + - name: "statusPayload" + build: + adapter: "{{ .adapter.name }}" + conditions: + - type: "Applied" + status: + expression: | + has(resources.nodePool.metadata.creationTimestamp) ? "True" : "False" + reason: + expression: | + has(resources.nodePool.metadata.creationTimestamp) ? "NodePoolCreated" : "NodePoolPending" + message: + expression: | + has(resources.nodePool.metadata.creationTimestamp) + ? "NodePool has been created on the management cluster" + : "NodePool is pending creation" + - type: "Available" + status: + expression: | + has(resources.nodePool.status) && has(resources.nodePool.status.conditions) + ? (resources.nodePool.status.conditions.filter(c, c.type == "Ready").size() > 0 + ? resources.nodePool.status.conditions.filter(c, c.type == "Ready")[0].status + : "False") + : "False" + reason: + expression: | + has(resources.nodePool.status) && has(resources.nodePool.status.conditions) + ? (resources.nodePool.status.conditions.filter(c, c.type == "Ready").size() > 0 + ? resources.nodePool.status.conditions.filter(c, c.type == "Ready")[0].reason + : "WaitingForNodes") + : "WaitingForNodes" + message: + expression: | + has(resources.nodePool.status) && has(resources.nodePool.status.conditions) + ? (resources.nodePool.status.conditions.filter(c, c.type == "Ready").size() > 0 + ? resources.nodePool.status.conditions.filter(c, c.type == "Ready")[0].message + : "Waiting for worker nodes to be provisioned") + : "Waiting for worker nodes to be provisioned" + - type: "Health" + status: + expression: | + adapter.?executionStatus.orValue("") == "success" ? "True" : (adapter.?executionStatus.orValue("") == "failed" ? "False" : "Unknown") + reason: + expression: | + adapter.?errorReason.orValue("") != "" ? adapter.?errorReason.orValue("") : "Healthy" + message: + expression: | + adapter.?errorMessage.orValue("") != "" ? adapter.?errorMessage.orValue("") : "Adapter executed successfully" + observed_generation: + expression: "generation" + observed_time: "{{ now | date \"2006-01-02T15:04:05Z07:00\" }}" + + post_actions: + - name: "reportNodepoolStatus" + api_call: + method: "POST" + url: "/clusters/{{ .clusterId }}/nodepools/{{ .nodepoolId }}/statuses" + headers: + - name: "Content-Type" + value: "application/json" + body: "{{ .statusPayload }}" diff --git a/helm/adapter-hypershift-nodepool/charts/hyperfleet-adapter-2.0.0.tgz b/helm/adapter-hypershift-nodepool/charts/hyperfleet-adapter-2.0.0.tgz new file mode 100644 index 0000000..332a9d8 Binary files /dev/null and b/helm/adapter-hypershift-nodepool/charts/hyperfleet-adapter-2.0.0.tgz differ diff --git a/helm/adapter-hypershift-nodepool/nodepool-manifest.yaml b/helm/adapter-hypershift-nodepool/nodepool-manifest.yaml new file mode 100644 index 0000000..167f82a --- /dev/null +++ b/helm/adapter-hypershift-nodepool/nodepool-manifest.yaml @@ -0,0 +1,28 @@ +apiVersion: hypershift.openshift.io/v1beta1 +kind: NodePool +metadata: + name: "{{ .clusterName }}-{{ .nodepoolName }}" + namespace: "{{ .namespace }}" + labels: + hyperfleet.io/cluster-id: "{{ .clusterId }}" + hyperfleet.io/cluster-name: "{{ .clusterName }}" + hyperfleet.io/nodepool-id: "{{ .nodepoolId }}" + hyperfleet.io/nodepool-name: "{{ .nodepoolName }}" +spec: + clusterName: "{{ .clusterName }}" + replicas: {{ index .nodepoolSpec "replicas" | default 2 }} + management: + autoRepair: true + upgradeType: Replace + platform: + type: OCI + oci: + instanceShape: '{{ index .nodepoolSpec "instanceShape" | default .ociShape }}' + instanceShapeConfig: + ocpus: '{{ index .nodepoolSpec "ocpus" | default .ociOcpus }}' + memoryInGBs: '{{ index .nodepoolSpec "memoryInGBs" | default .ociMemoryGBs }}' + availabilityDomain: '{{ index .nodepoolSpec "availabilityDomain" | default .ociAD }}' + subnetId: '{{ index .nodepoolSpec "subnetId" | default .ociSubnetId }}' + bootVolumeSize: '{{ index .nodepoolSpec "bootVolumeSize" | default .ociBootVolumeGB }}' + release: + image: '{{ index .nodepoolSpec "releaseImage" | default "quay.io/openshift-release-dev/ocp-release:4.20.2-x86_64" }}' diff --git a/helm/adapter-hypershift-nodepool/values.yaml b/helm/adapter-hypershift-nodepool/values.yaml new file mode 100644 index 0000000..cdb8816 --- /dev/null +++ b/helm/adapter-hypershift-nodepool/values.yaml @@ -0,0 +1,67 @@ +# Values for adapter-hypershift-nodepool +# Creates NodePool resources on a remote HyperShift management cluster + +hyperfleet-adapter: + image: + registry: CHANGE_ME + repository: CHANGE_ME + tag: latest + + adapterConfig: + create: true + log: + level: debug + + adapterTaskConfig: + create: true + + broker: + type: googlepubsub + googlepubsub: + projectId: CHANGE_ME + subscriptionId: CHANGE_ME + topic: CHANGE_ME + deadLetterTopic: "" + createTopicIfMissing: true + createSubscriptionIfMissing: true + rabbitmq: + url: CHANGE_ME + queue: "" + exchange: "" + routingKey: "" + + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: CLUSTERS_NAMESPACE + value: clusters + - name: OCI_AD + value: US-SANJOSE-1-AD-1 + - name: OCI_SUBNET_ID + value: CHANGE_ME + - name: OCI_SHAPE + value: VM.Standard.E4.Flex + - name: OCI_OCPUS + value: "4" + - name: OCI_MEMORY_GBS + value: "16" + - name: OCI_BOOT_VOLUME_GB + value: "120" + + # Mount the management cluster kubeconfig + extraVolumeMounts: + - name: hypershift-kubeconfig + mountPath: /etc/hypershift + readOnly: true + + extraVolumes: + - name: hypershift-kubeconfig + secret: + secretName: hypershift-mgmt-kubeconfig + + # RBAC is for the local CLM cluster only; remote access uses the mounted kubeconfig + rbac: + resources: + - configmaps diff --git a/helm/adapter-hypershift/Chart.yaml b/helm/adapter-hypershift/Chart.yaml new file mode 100644 index 0000000..8a7fcc4 --- /dev/null +++ b/helm/adapter-hypershift/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: adapter-hypershift +description: HyperShift adapter - creates HostedCluster resources on a remote management cluster +type: application +version: 0.1.0 +appVersion: "0.0.0-dev" + +dependencies: + - name: hyperfleet-adapter + version: "2.0.0" + repository: "git+https://github.com/openshift-hyperfleet/hyperfleet-adapter@charts?ref=main" diff --git a/helm/adapter-hypershift/adapter-config.yaml b/helm/adapter-hypershift/adapter-config.yaml new file mode 100644 index 0000000..338866a --- /dev/null +++ b/helm/adapter-hypershift/adapter-config.yaml @@ -0,0 +1,26 @@ +# HyperShift adapter deployment configuration +# Creates HostedCluster resources on a remote HyperShift management cluster +adapter: + name: adapter-hypershift + version: "0.2.0" + +debug_config: true +log: + level: debug + +clients: + hyperfleet_api: + base_url: http://hyperfleet-api:8000 + version: v1 + timeout: 10s + retry_attempts: 3 + retry_backoff: exponential + + broker: + subscription_id: "adapter-hypershift" + topic: "hyperfleet-clusters" + + kubernetes: + api_version: "v1" + # Use the mounted kubeconfig to target the remote HyperShift management cluster + kube_config_path: /etc/hypershift/kubeconfig diff --git a/helm/adapter-hypershift/adapter-task-config.yaml b/helm/adapter-hypershift/adapter-task-config.yaml new file mode 100644 index 0000000..e4a8440 --- /dev/null +++ b/helm/adapter-hypershift/adapter-task-config.yaml @@ -0,0 +1,209 @@ +# HyperShift adapter task configuration +# Creates a HostedCluster + required secrets on the remote management cluster +params: + + - name: "clusterId" + source: "event.id" + type: "string" + required: true + + - name: "generation" + source: "event.generation" + type: "int" + required: true + + - name: "namespace" + source: "env.CLUSTERS_NAMESPACE" + type: "string" + + - name: "ociRegion" + source: "env.OCI_REGION" + type: "string" + + - name: "ociCompartmentId" + source: "env.OCI_COMPARTMENT_ID" + type: "string" + + - name: "releaseImage" + source: "env.OPENSHIFT_RELEASE_IMAGE" + type: "string" + + - name: "baseDomain" + source: "env.BASE_DOMAIN" + type: "string" + + - name: "cpoImage" + source: "env.CPO_IMAGE" + type: "string" + + - name: "nodeAddress" + source: "env.NODE_ADDRESS" + type: "string" + required: true + + +# Preconditions: check cluster details from API +preconditions: + - name: "clusterStatus" + api_call: + method: "GET" + url: "/clusters/{{ .clusterId }}" + timeout: 10s + retry_attempts: 3 + retry_backoff: "exponential" + capture: + - name: "clusterName" + field: "name" + - name: "generation" + field: "generation" + - name: "clusterNotReady" + expression: | + !(status.?conditions.orValue([]).filter(c, c.type == "Ready").size() > 0 + && status.?conditions.orValue([]).filter(c, c.type == "Ready")[0].status == "True") + + - name: "validationCheck" + expression: | + clusterNotReady + +# Resources: Namespace + HostedCluster on the remote management cluster +resources: + + # Ensure the clusters namespace exists on the management cluster + - name: "clustersNamespace" + transport: + client: "kubernetes" + manifest: + apiVersion: v1 + kind: Namespace + metadata: + name: "{{ .namespace }}" + discovery: + by_name: "{{ .namespace }}" + + # Create the HostedCluster resource + - name: "hostedCluster" + transport: + client: "kubernetes" + manifest: + apiVersion: hypershift.openshift.io/v1beta1 + kind: HostedCluster + metadata: + name: "{{ .clusterName }}" + namespace: "{{ .namespace }}" + annotations: + hypershift.openshift.io/pod-security-admission-label-override: "privileged" + hypershift.openshift.io/control-plane-operator-image: "{{ .cpoImage }}" + hypershift.openshift.io/disable-monitoring-services: "true" + labels: + hyperfleet.io/cluster-id: "{{ .clusterId }}" + hyperfleet.io/cluster-name: "{{ .clusterName }}" + spec: + platform: + type: OCI + oci: + identityRef: + name: oci-credentials + region: "{{ .ociRegion }}" + compartmentId: "{{ .ociCompartmentId }}" + controllerAvailabilityPolicy: SingleReplica + pullSecret: + name: pull-secret + sshKey: + name: ssh-key + networking: + clusterNetwork: + - cidr: 10.132.0.0/14 + serviceNetwork: + - cidr: 172.31.0.0/16 + networkType: OVNKubernetes + services: + - service: Ignition + servicePublishingStrategy: + type: NodePort + nodePort: + address: "{{ .nodeAddress }}" + - service: OAuthServer + servicePublishingStrategy: + type: NodePort + nodePort: + address: "{{ .nodeAddress }}" + - service: APIServer + servicePublishingStrategy: + type: LoadBalancer + - service: Konnectivity + servicePublishingStrategy: + type: LoadBalancer + release: + image: "{{ .releaseImage }}" + dns: + baseDomain: "{{ .baseDomain }}" + discovery: + namespace: "{{ .namespace }}" + by_selectors: + label_selector: + hyperfleet.io/cluster-id: "{{ .clusterId }}" + +# Post-processing: report HostedCluster status back to API +post: + payloads: + - name: "statusPayload" + build: + adapter: "{{ .adapter.name }}" + conditions: + - type: "Applied" + status: + expression: | + has(resources.hostedCluster.metadata.creationTimestamp) ? "True" : "False" + reason: + expression: | + has(resources.hostedCluster.metadata.creationTimestamp) ? "HostedClusterCreated" : "HostedClusterPending" + message: + expression: | + has(resources.hostedCluster.metadata.creationTimestamp) + ? "HostedCluster has been created on the management cluster" + : "HostedCluster is pending creation" + - type: "Available" + status: + expression: | + has(resources.hostedCluster.status) && has(resources.hostedCluster.status.conditions) + ? (resources.hostedCluster.status.conditions.filter(c, c.type == "Available").size() > 0 + ? resources.hostedCluster.status.conditions.filter(c, c.type == "Available")[0].status + : "False") + : "False" + reason: + expression: | + has(resources.hostedCluster.status) && has(resources.hostedCluster.status.conditions) + ? (resources.hostedCluster.status.conditions.filter(c, c.type == "Available").size() > 0 + ? resources.hostedCluster.status.conditions.filter(c, c.type == "Available")[0].reason + : "WaitingForControlPlane") + : "WaitingForControlPlane" + message: + expression: | + has(resources.hostedCluster.status) && has(resources.hostedCluster.status.conditions) + ? (resources.hostedCluster.status.conditions.filter(c, c.type == "Available").size() > 0 + ? resources.hostedCluster.status.conditions.filter(c, c.type == "Available")[0].message + : "Waiting for hosted control plane to become available") + : "Waiting for hosted control plane to become available" + - type: "Health" + status: + expression: | + adapter.?executionStatus.orValue("") == "success" ? "True" : (adapter.?executionStatus.orValue("") == "failed" ? "False" : "Unknown") + reason: + expression: | + adapter.?errorReason.orValue("") != "" ? adapter.?errorReason.orValue("") : "Healthy" + message: + expression: | + adapter.?errorMessage.orValue("") != "" ? adapter.?errorMessage.orValue("") : "Adapter executed successfully" + observed_generation: + expression: "generation" + observed_time: "{{ now | date \"2006-01-02T15:04:05Z07:00\" }}" + + post_actions: + - name: "reportClusterStatus" + api_call: + method: "POST" + url: "/clusters/{{ .clusterId }}/statuses" + headers: + - name: "Content-Type" + value: "application/json" + body: "{{ .statusPayload }}" diff --git a/helm/adapter-hypershift/charts/hyperfleet-adapter-2.0.0.tgz b/helm/adapter-hypershift/charts/hyperfleet-adapter-2.0.0.tgz new file mode 100644 index 0000000..d4753d0 Binary files /dev/null and b/helm/adapter-hypershift/charts/hyperfleet-adapter-2.0.0.tgz differ diff --git a/helm/adapter-hypershift/values.yaml b/helm/adapter-hypershift/values.yaml new file mode 100644 index 0000000..de0c6c6 --- /dev/null +++ b/helm/adapter-hypershift/values.yaml @@ -0,0 +1,65 @@ +# Values for adapter-hypershift +# Creates HostedCluster resources on a remote HyperShift management cluster + +hyperfleet-adapter: + image: + registry: CHANGE_ME + repository: CHANGE_ME + tag: latest + + adapterConfig: + create: true + log: + level: debug + + adapterTaskConfig: + create: true + + broker: + type: googlepubsub + googlepubsub: + projectId: CHANGE_ME + subscriptionId: CHANGE_ME + topic: CHANGE_ME + deadLetterTopic: "" + createTopicIfMissing: true + createSubscriptionIfMissing: true + rabbitmq: + url: CHANGE_ME + queue: "" + exchange: "" + routingKey: "" + + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: CLUSTERS_NAMESPACE + value: clusters + - name: OCI_REGION + value: us-sanjose-1 + - name: OCI_COMPARTMENT_ID + value: ocid1.compartment.oc1..aaaaaaaazgovbe2qxduadk3bmj5dobvoe5wnengzavax5pwsfr3bqbdrrcqa + - name: OPENSHIFT_RELEASE_IMAGE + value: "quay.io/openshift-release-dev/ocp-release:4.20.2-x86_64" + - name: BASE_DOMAIN + value: hyperfleet.local + - name: CPO_IMAGE + value: "quay.io/vkareh/control-plane-operator:1775859030" + + # Mount the management cluster kubeconfig + extraVolumeMounts: + - name: hypershift-kubeconfig + mountPath: /etc/hypershift + readOnly: true + + extraVolumes: + - name: hypershift-kubeconfig + secret: + secretName: hypershift-mgmt-kubeconfig + + # RBAC is for the local CLM cluster only; remote access uses the mounted kubeconfig + rbac: + resources: + - configmaps diff --git a/helmfile/configs/oci/adapters/adapter1/adapter-config.yaml b/helmfile/configs/oci/adapters/adapter1/adapter-config.yaml new file mode 100644 index 0000000..7bdf7bc --- /dev/null +++ b/helmfile/configs/oci/adapters/adapter1/adapter-config.yaml @@ -0,0 +1,26 @@ +# Example HyperFleet Adapter deployment configuration +adapter: + name: adapter1 + version: "0.2.0" + +# Log the full merged configuration after load (default: false) +debug_config: true +log: + level: debug + +clients: + hyperfleet_api: + base_url: http://hyperfleet-api:8000 + version: v1 + timeout: 2s + retry_attempts: 3 + retry_backoff: exponential + + broker: + # These values are overridden at deploy time via env vars from Helm values + subscription_id: "placeholder" + topic: "placeholder" + + kubernetes: + api_version: "v1" + #kube_config_path: PATH_TO_KUBECONFIG # for local development diff --git a/helmfile/configs/oci/adapters/adapter1/adapter-task-config.yaml b/helmfile/configs/oci/adapters/adapter1/adapter-task-config.yaml new file mode 100644 index 0000000..0cbd92f --- /dev/null +++ b/helmfile/configs/oci/adapters/adapter1/adapter-task-config.yaml @@ -0,0 +1,149 @@ +# HyperFleet Adapter task configuration (v0.2.1 compatible) +params: + + - name: "clusterId" + source: "event.id" + type: "string" + required: true + + - name: "generation" + source: "event.generation" + type: "int" + required: true + + - name: "namespace" + source: "env.NAMESPACE" + type: "string" + +# Preconditions with valid operators and CEL expressions +preconditions: + - name: "clusterStatus" + api_call: + method: "GET" + url: "/clusters/{{ .clusterId }}" + timeout: 10s + retry_attempts: 3 + retry_backoff: "exponential" + capture: + - name: "clusterName" + field: "name" + - name: "generation" + field: "generation" + - name: "clusterNotReady" + expression: | + status.conditions.filter(c, c.type == "Ready").size() > 0 + ? status.conditions.filter(c, c.type == "Ready")[0].status != "True" + : true + - name: "clusterReadyTTL" + expression: | + (timestamp(now()) - timestamp( + status.conditions.filter(c, c.type == "Ready").size() > 0 + ? status.conditions.filter(c, c.type == "Ready")[0].last_transition_time + : now() + )).getSeconds() > 300 + + - name: "validationCheck" + # Precondition passes if cluster is NOT Ready OR if cluster is Ready and stable for >300 seconds since last transition (enables self-healing) + expression: | + clusterNotReady || clusterReadyTTL + +# Resources with valid K8s manifests +resources: + - name: "resource0" + transport: + client: "kubernetes" + manifest: + apiVersion: v1 + kind: ConfigMap + data: + cluster_id: "{{ .clusterId }}" + cluster_name: "{{ .clusterName }}" + metadata: + name: "{{ .clusterId | lower }}-adapter1-configmap" + namespace: "{{ .namespace }}" + labels: + app.kubernetes.io/component: adapter + app.kubernetes.io/instance: adapter1 + app.kubernetes.io/name: hyperfleet-adapter + app.kubernetes.io/version: 1.0.0 + app.kubernetes.io/transport: kubernetes + hyperfleet.io/cluster-id: "{{ .clusterId }}" + hyperfleet.io/cluster-name: "{{ .clusterName }}" + annotations: + hyperfleet.io/generation: "{{ .generation }}" + discovery: + namespace: "{{ .namespace }}" + by_selectors: + label_selector: + hyperfleet.io/cluster-id: "{{ .clusterId }}" + hyperfleet.io/cluster-name: "{{ .clusterName }}" + +# Post-processing with valid CEL expressions +post: + payloads: + - name: "statusPayload" + build: + adapter: "{{ .adapter.name }}" + conditions: + # Applied: ConfigMap successfully created in the target namespace + - type: "Applied" + status: + expression: | + resources.?resource0.?metadata.?creationTimestamp.hasValue() ? "True" : "False" + reason: + expression: | + resources.?resource0.?metadata.?creationTimestamp.hasValue() ? "ConfigMapCreated" : "ConfigMapNotCreated" + message: + expression: | + resources.?resource0.?metadata.?creationTimestamp.hasValue() + ? "ConfigMap created in namespace " + resources.resource0.metadata.namespace + : "ConfigMap has not been created yet" + # Available: ConfigMap data keys are populated and accessible + - type: "Available" + status: + expression: | + resources.?resource0.?data.?cluster_id.hasValue() ? "True" : "False" + reason: + expression: | + resources.?resource0.?data.?cluster_id.hasValue() + ? "ConfigMapDataPopulated" + : "ConfigMapDataMissing" + message: + expression: | + resources.?resource0.?data.?cluster_id.hasValue() + ? "ConfigMap data keys are populated and accessible" + : "ConfigMap data keys are not yet populated" + # Health: Adapter execution status (runtime) + - type: "Health" + status: + expression: | + adapter.?executionStatus.orValue("") == "success" ? "True" : (adapter.?executionStatus.orValue("") == "failed" ? "False" : "Unknown") + reason: + expression: | + adapter.?errorReason.orValue("") != "" ? adapter.?errorReason.orValue("") : "Healthy" + message: + expression: | + adapter.?errorMessage.orValue("") != "" ? adapter.?errorMessage.orValue("") : "All adapter operations completed successfully" + # Event generation ID metadata field needs to use expression to avoid interpolation issues + observed_generation: + expression: "generation" + observed_time: "{{ now | date \"2006-01-02T15:04:05Z07:00\" }}" + # Optional data field for adapter-specific metrics extracted from resources + data: + namespace: + name: + expression: | + resources.?resource0.?metadata.?name.orValue("") + creationTimestamp: + expression: | + resources.?resource0.?metadata.?creationTimestamp.orValue("") + + post_actions: + - name: "reportClusterStatus" + api_call: + method: "PUT" + url: "/clusters/{{ .clusterId }}/statuses" + headers: + - name: "Content-Type" + value: "application/json" + body: "{{ .statusPayload }}" diff --git a/helmfile/environments/oci/adapter-configs.yaml.gotmpl b/helmfile/environments/oci/adapter-configs.yaml.gotmpl new file mode 100644 index 0000000..0d855c5 --- /dev/null +++ b/helmfile/environments/oci/adapter-configs.yaml.gotmpl @@ -0,0 +1,26 @@ +{{ $namespace := requiredEnv "NAMESPACE" }} +adapters: + - name: adapter1 + resourceType: clusters + values: + - adapterTaskConfig: + create: true + adapterConfig: + create: true + log: + level: debug + rbac: + resources: + - configmaps + broker: + create: true + rabbitmq: + url: "amqp://guest:guest@rabbitmq:5672" + queue: {{ $namespace }}-clusters-adapter1 + exchange: {{ $namespace }}-clusters + routingKey: "#" + setFiles: + - name: adapterConfig.yaml + file: configs/oci/adapters/adapter1/adapter-config.yaml + - name: adapterTaskConfig.yaml + file: configs/oci/adapters/adapter1/adapter-task-config.yaml diff --git a/helmfile/environments/oci/sentinel-configs.yaml b/helmfile/environments/oci/sentinel-configs.yaml new file mode 100644 index 0000000..6be831f --- /dev/null +++ b/helmfile/environments/oci/sentinel-configs.yaml @@ -0,0 +1,35 @@ +sentinels: + - name: clusters + values: + - config: + resourceType: clusters + resourceSelector: [] + broker: + topic: hyperfleet-clusters + rabbitmq: + exchangeType: topic + monitoring: + podMonitoring: + enabled: false + prometheusRule: + enabled: false + + - name: nodepools + values: + - config: + resourceType: nodepools + resourceSelector: [] + messageData: + owner_references: + id: "resource.owner_references.id" + href: "resource.owner_references.href" + kind: "resource.owner_references.kind" + broker: + topic: hyperfleet-nodepools + rabbitmq: + exchangeType: topic + monitoring: + podMonitoring: + enabled: false + prometheusRule: + enabled: false diff --git a/helmfile/helmfile.yaml.gotmpl b/helmfile/helmfile.yaml.gotmpl index 4852c35..475c9f2 100644 --- a/helmfile/helmfile.yaml.gotmpl +++ b/helmfile/helmfile.yaml.gotmpl @@ -48,6 +48,12 @@ environments: serviceType: {{ env "API_SERVICE_TYPE" | default "LoadBalancer" }} - environments/gcp/adapter-configs.yaml.gotmpl - environments/gcp/sentinel-configs.yaml.gotmpl + oci: + values: + - brokerType: rabbitmq + serviceType: {{ env "API_SERVICE_TYPE" | default "ClusterIP" }} + - environments/oci/adapter-configs.yaml.gotmpl + - environments/oci/sentinel-configs.yaml commonLabels: group: hyperfleet