For pilot partners.

Install it with one Helm command.

Both charts are published from this site. greenshift-agents goes into each customer cluster; greenshift-core runs the GreenShift Master — the attribution pipeline and its PostgreSQL, Kafka and Redis — in the operator's own cluster.

Terminal
# 1. Add the GreenShift chart repository
helm repo add greenshift https://greenshift-registry.online/charts
helm repo update

# 2. Install, supplying the registry and MinIO credentials and the
#    Master connection (described below)
#    (sent to you through our secure delivery channel)
helm install greenshift-agents greenshift/greenshift-agents \
  -n greenshift-system --create-namespace \
  --set registry.password="<provided separately>" \
  --set minio.rootUser="<choose a value>" \
  --set minio.rootPassword="<choose a value>" \
  --set master.url="https://gateway.example.com" \
  --set master.token="<provided separately>" \
  --set master.clusterInstanceId="<provided separately>"

# 3. Verify
kubectl get pods -n greenshift-system
Connecting a customer cluster to the Master
# The Registration Service in each customer cluster registers the cluster
# with the GreenShift Master and keeps its node list current there; the
# Attributor Service pulls the cluster's power back from it.

# master.url — scheme and host of the Master's public Gateway, no path.
#   The same value the operator set as global.gatewayPublicUrl. https:// only.
--set master.url=https://gateway.example.com

# master.token — this cluster's credential, a bearer token. The operator
#   puts only its SHA-256 fingerprint in the Master's cluster credential
#   registry:   printf %s "$TOKEN" | sha256sum
#   Or keep it in your own Secret (key credential) in greenshift-system:
#   --set master.existingSecret=<name>
--set master.token="<provided separately>"

# master.clusterInstanceId — the ID the operator provisioned for this
#   cluster, so the Master recognises it on first registration.
--set master.clusterInstanceId="<provided separately>"

# master.ca — only if the Gateway's certificate comes from a private CA:
#   that CA (PEM). It is added to the public roots, never replaces them.
#   Or name a ConfigMap you already have: --set master.caConfigMap=<name>
#   --set master.caConfigMapKey=<key>
--set-file master.ca=gateway-ca.crt

# Optional: how the cluster names itself to the Master.
--set cluster.localName=prod-eu-1 --set cluster.environment=production --set cluster.site=dc-1

# Leave master.* out to run without a Master: nodes still register locally
# and nothing leaves the cluster. A missing or rejected credential never
# stops a workload; registration is retried. Rotate the token with
# helm upgrade --reuse-values --set master.token=... (no restart needed).

Seeing your figures. The chart installs the App Owner Dashboard and the App Owner API beside the agents. Neither is published outside your cluster: anyone allowed to port-forward in greenshift-system can open them, and no one else can.

App Owner Dashboard
# Forward the dashboard to your workstation, then open http://localhost:8090
kubectl -n greenshift-system port-forward svc/greenshift-dashboard 8090:8090

# Where your cluster's energy goes: the share drawn by your applications, the
# host overhead, and the idle draw of machines that are powered on but busy
# with nothing. Pick 24h, 7d or the month; live power refreshes every 10 s,
# energy every minute. Break it down by machine, namespace, deployment and pod.

# The dashboard calls the API for you, with a token that stays in the cluster
# (Secret greenshift-dashboard-api-token, generated on install). To choose it:
#   --set dashboard.apiToken=... or --set dashboard.existingSecret=<name>
App Owner API
# The same figures as JSON, for your own reports and tooling. In the cluster
# it answers at http://greenshift-app-owner-api.greenshift-system:8080; from a
# workstation, forward it first:
kubectl -n greenshift-system port-forward svc/greenshift-app-owner-api 8080:8080

# Every /v1 call needs an Authorization: Bearer header. The pilot carries one
# tenant per cluster and does not check the token yet, but it rejects a call
# without one.
curl -H 'Authorization: Bearer <token>' http://localhost:8080/v1/namespaces

# Energy over a period. scope: cluster, nodes, namespaces, deployments, pods.
# period: day, week, month, year (default day), or an explicit window with
# from= and to= in RFC3339 UTC. namespace= narrows the result.
GET /v1/energy/{scope}?period=week
GET /v1/energy/pods?from=2026-10-01T00:00:00Z&to=2026-10-02T00:00:00Z&namespace=shop

# One entity
GET /v1/energy/namespaces/{name}
GET /v1/energy/deployments/{namespace}/{name}
GET /v1/energy/nodes/{name}

# Power right now, same scopes
GET /v1/power/{scope}

# What there is to ask about: what has drawn power in the last day
GET /v1/namespaces    GET /v1/deployments    GET /v1/pods

# Energy is in energy_microjoules and energy_kwh, power in microwatts. Only
# CPU energy is measured (measured_components: ["cpu"]). cluster and nodes
# also report the unattributed host-process and idle energy. A response
# with status "partial" covers only part of the window, e.g. one reaching
# back before the cluster began collecting; the missing part is never a zero.
# Set that start with --set appOwnerApi.dataStart=2026-07-29T14:16:00Z if
# metrics were collected before the chart was installed.
GreenShift Master (operators)
# 1. A pull secret for our images, in the Master namespace
kubectl create namespace greenshift-master
kubectl -n greenshift-master create secret docker-registry greenshift-registry-credentials \
  --docker-server=greenshift-registry.online --docker-username=greenshift \
  --docker-password="<provided separately>"

# 2. Install with your four site inputs (described below)
helm install greenshift-core greenshift/greenshift-core -n greenshift-master \
  --set global.imageRegistry=greenshift-registry.online \
  --set 'global.imagePullSecrets[0]=greenshift-registry-credentials' \
  --set global.gatewayPublicUrl=https://gateway.example.com \
  --set-file ingestionApi.hostCredentialRegistry.content=host-credentials.yaml \
  --set-file clusterCredentialRegistry.content=cluster-credentials.yaml \
  --set-file vmMapper.metadata.content=pilot-metadata.yaml \
  --wait --wait-for-jobs --timeout 15m

#    On a bare cluster with a NodePort Gateway, only the node running the
#    Gateway pod answers: pin it to the node your public URL names with
#    --set gateway.nodeSelector."kubernetes\.io/hostname"=<node>

# 3. Verify: database, migrations, service discovery, gateway
helm test greenshift-core -n greenshift-master

Site inputs. The Master needs four things only you know. Keep all four files out of version control: they bind credentials to identities. They hold SHA-256 fingerprints, never a token or password, so a leaked file cannot be replayed — but it still names your hosts and clusters.

1 · Public Gateway URL
# global.gatewayPublicUrl — an absolute https:// URL.
# The address at which customer clusters and hypervisor hosts reach the
# GreenShift Gateway from outside the Master cluster. Registration hands it
# to every customer cluster as the base URL for its power queries, so it must
# be the name on the Gateway's TLS certificate, not an internal address.
--set global.gatewayPublicUrl=https://gateway.example.com
2 · Host credential registry — host-credentials.yaml
# Which hypervisor host may push power telemetry, and as whom. Read by the
# Ingestion API; a host may only ever submit data for its own host_id.
# Reloaded every 30 s when its content changes — revoke by editing.
schema_version: "1.0"
registry_revision: 1                 # increment on every edit
updated_at: "2026-10-01T09:00:00Z"    # RFC3339 UTC, ends in Z
credentials:                         # 1 to 1000 entries
  - credential_id: "cred-host-001-a"  # your label, shown in audit logs
    credential_type: "host_bearer_token"
    # lowercase hex SHA-256 of the bearer token alone (no "Bearer ");
    # 64 characters, unique across the file:  printf %s "$TOKEN" | sha256sum
    fingerprint_sha256: "<64 hex characters>"
    host_id: "host-001"               # the host this token belongs to
    identity_status: "active"        # active | revoked | expired
    not_after: "2027-10-01T00:00:00Z"

# Rotate a credential by adding the new one as active and marking the old
# one revoked, both under the same host_id. Never put a token, password,
# secret or private_key field in this file: the whole file is rejected.
# Or name a Secret you created (key registry.yaml):
#   --set ingestionApi.hostCredentialRegistry.existingSecret=<name>
3 · Cluster credential registry — cluster-credentials.yaml
# Which customer cluster may register and query power, and as whom. One
# entry per customer cluster, binding the SHA-256 fingerprint of that
# cluster's credential to its Master-allocated cluster ID, which always has
# the form gsc- followed by 8 lowercase hex characters (gsc-1a2b3c4d).
# One file serves both the Registration Manager and the Customer Attributor.
# Fingerprints only, never the credential itself. The exact fields are in
# the greenshift-core chart README (helm pull greenshift/greenshift-core --untar).
# Or name a ConfigMap you created (key registry.yaml):
#   --set clusterCredentialRegistry.existingConfigMap=<name>
4 · Pilot VM metadata — pilot-metadata.yaml
# Which VMs are in scope, and which customer cluster each belongs to. Read
# by the VM Mapper, which writes power only for the VMs listed here.
schema_version: "1.0"
greenshift_cluster_id: "gsc-1a2b3c4d"   # default cluster for every VM below
metadata_confidence: 0.95              # optional, defaults to 0.95
vms:                                   # at least one; vm_uuid unique
  - vm_uuid: "550e8400-e29b-41d4-a716-446655440000"
    node_uid: "node-uid-1"               # optional Kubernetes node UID
  - vm_uuid: "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
    greenshift_cluster_id: "gsc-9f8e7d6c"  # this VM is in another cluster

# Every cluster ID here must already be in the cluster credential registry,
# or that VM's power is written where no one reads it. This file says
# nothing about tenants: VM ownership comes from the tenant assignments.
# Or name a ConfigMap you created (key pilot_metadata.yaml):
#   --set vmMapper.metadata.existingConfigMap=<name>

Power source. The Master reads each hypervisor host's power at the PDU that feeds it, splits it across the host's VMs, and hands every customer cluster its own share — which is what the App Owner Dashboard and API then show. For an Eaton Rack PDU G4 one value chooses the source at install time, and the chart writes the host-to-PDU inventory for you: pdu.source=eaton for a real device, or pdu.source=emulator for a test cluster that has none. Add the values below to the helm install of step 2, or apply them later with helm upgrade --reuse-values. Needs greenshift-core 0.3.0 or later.

5 · Eaton Rack PDU G4 emulator — test clusters only
# Test only: the power it serves is synthetic. Never select it on a
# deployment that serves a customer. It runs one emulated Eaton Rack PDU G4
# inside the Master cluster, reachable only by the Infra Mapper, and wires
# the Infra Mapper to it; its passphrase is generated and never shown.
--set pdu.source=emulator \
--set 'pdu.emulator.hosts[0]=host-001'

# Exactly one host. host-001 must be the GS_HOST_ID of the Host Mapper on the
# hypervisor that runs your customer-cluster node VMs, with an entry for it
# in the host credential registry (site input 2).

# Its outlets and their watts, one per PSU of that host. Default 1:410,2:420,
# i.e. 830 W in all. Write outlet ids without leading zeros. To change them:
--set eatonEmulator.config.GS_EATON_EMULATOR_OUTLETS='1:820\,2:840'
# The emulator restarts and the Infra Mapper logs in again on its next read.
6 · A real Eaton Rack PDU G4
# 1. The password of the PDU's REST API account, in a Secret you create.
#    The chart never takes it as a value. Use a dedicated account: the
#    Infra Mapper only reads measurements.
kubectl -n greenshift-master create secret generic eaton-pdu-password \
  --from-literal=password='<PDU account password>'

# 2. Point the Infra Mapper at the device
--set pdu.source=eaton \
--set pdu.eaton.address=192.0.2.10 \
--set pdu.eaton.username=greenshift \
--set pdu.eaton.passwordSecret.name=eaton-pdu-password \
--set 'pdu.eaton.hosts[0].hostId=host-001' \
--set 'pdu.eaton.hosts[0].outlets={1,2}'

# address — the management address, or address:port; no https://, no path.
#   The Master cluster must be able to reach it over HTTPS.
# hostId — as for the emulator: the GS_HOST_ID of that host's Host Mapper.
# outlets — the outlets feeding that host's PSUs, by the number in the
#   PDU's REST path .../powerDistributions/1/outlets/<n> (the web UI shows
#   physical names such as A1). An outlet feeds one host only. More hosts:
#   --set 'pdu.eaton.hosts[1].hostId=host-002' --set 'pdu.eaton.hosts[1].outlets={3,4}'

# TLS is verified. A G4 ships with a self-signed certificate: install a
# CA-signed one on the PDU, or, where you cannot, --set pdu.eaton.tlsVerify=false.
# Several PDUs: write the inventory yourself (fields in the chart README) and
# pass it with --set-file infraMapper.inventory.content=inventory.yaml.
7 · Check that the power reaches your figures
# Master: one window per host every 10 s, read from the PDU or the emulator
kubectl -n greenshift-master logs deploy/greenshift-infra-mapper -c infra-mapper \
  | grep 'Closed window'
#   Closed window for host-001: status=valid, avg_power_microwatts=830124000 ...

# Hypervisor: the Host Mapper reports that same host_id, with Scaphandre on
# 127.0.0.1:9999. Without it the host's power is not split across its VMs and
# no customer cluster sees any of it.

# Customer cluster, a minute or two later: power per node, above zero, in the
# App Owner API and in the Dashboard's live power.
curl -H 'Authorization: Bearer <token>' http://localhost:8080/v1/power/nodes

# With the emulator, raise its outlet watts (panel 5) with helm upgrade
# --reuse-values: the figures rise within a few minutes.
No credential lives on this page. The chart and its install docs are public; the registry password that lets it pull our images is issued and delivered to each partner individually through a secure channel — never published here.