diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index ef126d4..d53b822 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -2,9 +2,6 @@ name: Pull Request Checks on: pull_request: - paths: - - 'charts/jupiterone-integration-operator/**' - - 'charts/jupiterone-integration-runner/**' jobs: verify-operator-crds: @@ -18,3 +15,36 @@ jobs: run: make verify-crds env: GH_TOKEN: ${{ secrets.AUTO_GITHUB_PAT_TOKEN }} + + chart-tests: + name: Chart Tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + path: helm-charts + + - name: Set up Helm + uses: azure/setup-helm@v4 + + - name: Lint charts + run: | + helm lint helm-charts/charts/jupiterone-integration-operator + helm lint helm-charts/charts/jupiterone-integration-runner \ + --set accountID=ci --set apiToken=ci + + - name: Run chart render tests + run: | + set -euo pipefail + status=0 + for script in \ + helm-charts/charts/jupiterone-integration-operator/tests/declared-values_test.sh \ + helm-charts/charts/jupiterone-integration-operator/tests/private-registry-values_test.sh \ + helm-charts/charts/jupiterone-integration-operator/tests/irsa-service-account_test.sh \ + helm-charts/charts/jupiterone-integration-runner/tests/runner-values_test.sh; do + echo "::group::$script" + bash "$script" || status=1 + echo "::endgroup::" + done + exit "$status" diff --git a/Makefile b/Makefile index 48a6954..22373b0 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ sync-crds: ## Sync CRDs from the latest jupiterone-integration-operator release tar xzf operator.tar.gz -C _operator_src && \ echo "Syncing CRDs to $(OPERATOR_CRD_DIR)..." && \ for f in _operator_src/*/config/crd/bases/*.yaml; do \ - sed '1{/^---$$/d;}' "$$f" > "$(OPERATOR_CRD_DIR)/$$(basename $$f)"; \ + sed '1{/^---$$/d;}' "$$f" | awk '/^ annotations:$$/ && !done { print; print " {{- if .Values.crd.keep }}"; print " \"helm.sh/resource-policy\": keep"; print " {{- end }}"; done=1; next } { print }' > "$(OPERATOR_CRD_DIR)/$$(basename $$f)"; \ echo " Synced $$(basename $$f)"; \ done && \ rm -rf _operator_src operator.tar.gz && \ diff --git a/charts/jupiterone-integration-operator/Chart.yaml b/charts/jupiterone-integration-operator/Chart.yaml index 0b61e9c..e457f19 100644 --- a/charts/jupiterone-integration-operator/Chart.yaml +++ b/charts/jupiterone-integration-operator/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: jupiterone-integration-operator description: JupiterOne Integration Operator for running integrations in Kubernetes type: application -version: 1.3.4 -appVersion: "v0.3.1" +version: 1.4.0 +appVersion: "v0.4.0" diff --git a/charts/jupiterone-integration-operator/README.md b/charts/jupiterone-integration-operator/README.md index 40ae015..9a4d0c4 100644 --- a/charts/jupiterone-integration-operator/README.md +++ b/charts/jupiterone-integration-operator/README.md @@ -51,10 +51,106 @@ Refer to the [values.yaml](./values.yaml) for all available configuration option | Parameter | Description | Default | |---|---|---| -| `controllerManager.imageRegistry` | Image registry override for private registry environments. When set, integration job images are pulled from this registry instead of `ghcr.io`. | `""` | -| `controllerManager.imagePullSecrets` | Secrets for pulling images from private registries. Applied to both the operator Deployment and propagated to spawned integration job pods. | `[]` | -| `controllerManager.disableImageSignatureCheck` | Disable cosign image signature verification for integration job images. Set to `true` when using registries that don't mirror ghcr.io cosign signatures. | `false` | -| `controllerManager.jobResources` | Resource requests and limits applied to integration job containers. Configure to comply with cluster resource policies (e.g. Kyverno, OPA/Gatekeeper). | `{}` | +| `nameOverride` | Overrides the chart name used in the `app.kubernetes.io/name` label. | `""` | +| `controllerManager.replicas` | Manager replicas. Leader election is on, so extra replicas are standby only. | `1` | +| `controllerManager.container.image.repository` | Manager image repository. | `ghcr.io/jupiterone/jupiterone-integration-operator` | +| `controllerManager.container.image.tag` | Manager image tag. Empty uses the chart `appVersion`. | `""` | +| `controllerManager.container.args` | Manager command-line flags. | `--leader-elect`, `--metrics-bind-address=:8443`, `--health-probe-bind-address=:8081` | +| `controllerManager.container.env` | Extra environment variables on the manager as a `KEY: value` map (`JOB_TTL_SECONDS`, `JOB_ACTIVE_DEADLINE_SECONDS`, `ASM_CACHE_TTL_SECONDS`, `AWS_REGION`, `LOG_LEVEL`, `HTTP_PROXY`, ...). | `JOB_TTL_SECONDS: "604800"` | +| `controllerManager.container.resources` | Manager container requests and limits. | `100m`/`64Mi` requests, `500m`/`512Mi` limits | +| `controllerManager.container.livenessProbe` | Manager liveness probe. | `GET /healthz` on `8081` | +| `controllerManager.container.readinessProbe` | Manager readiness probe. | `GET /readyz` on `8081` | +| `controllerManager.container.securityContext` | Manager container security context. | `allowPrivilegeEscalation: false`, drop `ALL` | +| `controllerManager.securityContext` | Manager pod security context. | `runAsNonRoot: true`, seccomp `RuntimeDefault` | +| `controllerManager.pod.labels` | Extra labels on the manager pod. | `{}` | +| `controllerManager.terminationGracePeriodSeconds` | Manager pod termination grace period. | `10` | +| `controllerManager.serviceAccountName` | Name of the operator ServiceAccount. | `jupiterone-integration-operator-controller-manager` | +| `controllerManager.serviceAccount.annotations` | Annotations on the operator ServiceAccount. Set the IRSA role here when the operator reads credentials from AWS Secrets Manager. | `{}` | +| `controllerManager.imageRegistry` | Registry that replaces `ghcr.io` for integration job images (`/jupiterone/graph-:latest`). Hostname only. | `""` | +| `controllerManager.disableImageSignatureCheck` | Skip cosign signature verification of integration job images. | `false` | +| `controllerManager.imagePullSecrets` | `imagePullSecrets` for the manager pod and every integration job pod. | `[]` | +| `controllerManager.jobResources` | Requests and limits applied to integration job containers. | `{}` | +| `rbac.enable` | Create the operator ServiceAccount, Roles and bindings. | `true` | +| `metrics.enable` | Create the metrics Service. Remove `--metrics-bind-address` from `args` when disabling. | `true` | +| `prometheus.enable` | Create a `ServiceMonitor` for the metrics Service. | `false` | +| `certmanager.enable` | Issue the metrics serving certificate with cert-manager. | `false` | +| `crd.keep` | Annotate the operator CRDs (and the metrics Certificate) with `helm.sh/resource-policy: keep` so `helm uninstall` leaves them, and every `IntegrationRunner`/`IntegrationInstance`/`IntegrationInstanceJob`, in place. | `false` | +| `networkPolicy.enable` | Create a NetworkPolicy allowing metrics scrapes from namespaces labeled `metrics: enabled`. | `false` | +| `integration.create` | Create the `kubernetes-managed` integration ServiceAccount, ClusterRole and ClusterRoleBinding. | `true` | +| `integration.serviceAccountName` | ServiceAccount used by `kubernetes-managed` integration job pods (`K8S_INTEGRATION_SERVICE_ACCOUNT`). | `jupiterone` | +| `integration.serviceAccountNamespace` | Namespace of that ServiceAccount. Changing it is not supported. | `jupiterone` | +| `integration.serviceAccount.annotations` | Annotations on the `kubernetes-managed` integration ServiceAccount. | `{}` | +| `integration.jobServiceAccount.create` | Create a ServiceAccount for all other integration job pods. | `false` | +| `integration.jobServiceAccount.name` | Name of that ServiceAccount. Defaults to `jupiterone-integration-job` when created. Set without `create` to reference one managed elsewhere. | `""` | +| `integration.jobServiceAccount.annotations` | Annotations on the integration job ServiceAccount. Set the IRSA role here to give job pods an AWS identity. | `{}` | + +Environment variables the manager reads that have no dedicated value are set +through `controllerManager.container.env`: + +| Variable | Description | Default | +|---|---|---| +| `JOB_TTL_SECONDS` | Seconds a finished `IntegrationInstanceJob` and its Job are kept. | `2592000`; the chart sets `604800` | +| `JOB_ACTIVE_DEADLINE_SECONDS` | `activeDeadlineSeconds` on each integration Job. | `86400` | +| `ASM_CACHE_TTL_SECONDS` | Cache TTL for AWS Secrets Manager lookups; `0` disables. | `60` | +| `AWS_REGION` | Region for the AWS SDK default chain when a CR omits `region`. | unset | +| `LOG_LEVEL` | `debug`, `info`, `warn` or `error`. | `info` | +| `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` | Proxy settings; also injected into integration job pods. | unset | + +### ServiceAccount annotations (IRSA) + +Every ServiceAccount the chart creates accepts annotations, so an IAM role can +be attached without editing rendered manifests: + +```yaml +controllerManager: + serviceAccount: + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam:::role/jupiterone-integration-operator +``` + +The operator needs that role only when a CR reads credentials from AWS Secrets +Manager (`apiTokenSource` / `secretSource` with `provider: awsSecretsManager`). +Integration job pods and the `kubernetes-managed` ServiceAccount take their +annotations from `integration.jobServiceAccount.annotations` and +`integration.serviceAccount.annotations`; see +[AWS Access for Integration Job Pods (IRSA)](#aws-access-for-integration-job-pods-irsa). +Trust policies and permission policies are in the operator README under +[AWS authentication (IRSA)](https://github.com/JupiterOne/jupiterone-integration-operator#aws-authentication-irsa). + +### AWS Secrets Manager Example + +```yaml +controllerManager: + serviceAccount: + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam:::role/jupiterone-integration-operator + container: + env: + AWS_REGION: us-east-1 + ASM_CACHE_TTL_SECONDS: "60" +``` + +Then reference the secret from the runner chart (`apiTokenSource`) or an +`IntegrationInstance` (`secretSource`). + +### Metrics with Prometheus and cert-manager + +```yaml +metrics: + enable: true +prometheus: + enable: true +certmanager: + enable: true +networkPolicy: + enable: true +``` + +cert-manager issues `metrics-server-cert` for +`jupiterone-integration-operator-metrics-service..svc`, the +ServiceMonitor scrapes it over TLS, and the NetworkPolicy admits scrapes only +from namespaces labeled `metrics: enabled`. Without `certmanager.enable` the +ServiceMonitor uses `insecureSkipVerify: true`. ### Private Registry Example @@ -78,6 +174,69 @@ helm install integration-operator jupiterone/jupiterone-integration-operator \ > **Note:** `disableImageSignatureCheck` is independent of `imageRegistry`. Cosign verification may work through registry proxies since it resolves signatures against the original source. Only disable it if verification fails in your environment. +### AWS Access for Integration Job Pods (IRSA) + +Integration job pods run under the `default` ServiceAccount, which normally has +no AWS identity. Integrations that call AWS -- for example SBOM for AWS ECR -- +need one. Set `integration.jobServiceAccount` and the chart creates the +ServiceAccount, annotates it for IRSA, and points the operator at it through +`INTEGRATION_JOB_SERVICE_ACCOUNT`, so every integration job pod runs with that +identity. + +```yaml +integration: + jobServiceAccount: + create: true + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam:::role/jupiterone-integration-job +``` + +The `kubernetes-managed` integration keeps its own ServiceAccount (it is bound +to the in-cluster read ClusterRole) and is annotated separately: + +```yaml +integration: + serviceAccount: + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam:::role/jupiterone-kubernetes-managed +``` + +The operator itself needs a role only when a CR resolves credentials from AWS +Secrets Manager: + +```yaml +controllerManager: + serviceAccount: + annotations: + eks.amazonaws.com/role-arn: arn:aws:iam:::role/jupiterone-integration-operator +``` + +**Same-account ECR.** Grant the job role ECR read directly +(`ecr:GetAuthorizationToken` on `*`, plus `ecr:BatchGetImage`, +`ecr:GetDownloadUrlForLayer`, `ecr:BatchCheckLayerAvailability`, +`ecr:DescribeRepositories`, `ecr:DescribeImages`, `ecr:ListImages`, +`ecr:ListTagsForResource` on the repository ARNs). + +**Cross-account ECR.** Grant the job role only `sts:AssumeRole` on the role in +the registry's account; that role holds the ECR permissions and trusts the job +role. That trust is between two identities the customer owns: the `Principal` +is the customer's job pod role, not a JupiterOne AWS account. Several registries +means one such role per account and one `sts:AssumeRole` resource per target. + +This ServiceAccount is shared by every integration job pod in the release, so +keep its own policy to the `sts:AssumeRole` targets it needs and leave the ECR +permissions on the assumed roles. Where a workload needs stronger separation, +install a second operator and runner in their own namespace with their own +`integration.jobServiceAccount`. + +Trust policies, IAM policy documents and the step-by-step SBOM for AWS ECR +setup are in the operator repository: +[AWS authentication (IRSA)](https://github.com/JupiterOne/jupiterone-integration-operator#aws-authentication-irsa). + +Requires the operator release that adds `INTEGRATION_JOB_SERVICE_ACCOUNT` +(`v0.4.0`). On older operators the env var is ignored and job pods keep using +the `default` ServiceAccount. + ### Job Resources Example If your cluster enforces resource policies (e.g. Kyverno `require-requests-limits`), configure resource requirements for integration job containers: diff --git a/charts/jupiterone-integration-operator/templates/_helpers.tpl b/charts/jupiterone-integration-operator/templates/_helpers.tpl index c699264..4cbccc7 100644 --- a/charts/jupiterone-integration-operator/templates/_helpers.tpl +++ b/charts/jupiterone-integration-operator/templates/_helpers.tpl @@ -1,12 +1,8 @@ {{- define "chart.name" -}} -{{- if .Chart }} - {{- if .Chart.Name }} - {{- .Chart.Name | trunc 63 | trimSuffix "-" }} - {{- else if .Values.nameOverride }} - {{ .Values.nameOverride | trunc 63 | trimSuffix "-" }} - {{- else }} - jupiterone-integration-operator - {{- end }} +{{- if .Values.nameOverride }} + {{- .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- else if and .Chart .Chart.Name }} + {{- .Chart.Name | trunc 63 | trimSuffix "-" }} {{- else }} jupiterone-integration-operator {{- end }} @@ -48,3 +44,12 @@ app.kubernetes.io/instance: {{ .Release.Name }} $hasValidating = true }}{{- end }} {{- end }} {{ $hasValidating }}}}{{- end }} + + +{{- define "chart.integrationJobServiceAccountName" -}} +{{- if .Values.integration.jobServiceAccount.name -}} +{{- .Values.integration.jobServiceAccount.name -}} +{{- else if .Values.integration.jobServiceAccount.create -}} +jupiterone-integration-job +{{- end -}} +{{- end }} diff --git a/charts/jupiterone-integration-operator/templates/crds/integrations.jupiterone.io_integrationinstancejobs.yaml b/charts/jupiterone-integration-operator/templates/crds/integrations.jupiterone.io_integrationinstancejobs.yaml index 09725a7..95e0e2a 100644 --- a/charts/jupiterone-integration-operator/templates/crds/integrations.jupiterone.io_integrationinstancejobs.yaml +++ b/charts/jupiterone-integration-operator/templates/crds/integrations.jupiterone.io_integrationinstancejobs.yaml @@ -2,6 +2,9 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: + {{- if .Values.crd.keep }} + "helm.sh/resource-policy": keep + {{- end }} controller-gen.kubebuilder.io/version: v0.18.0 name: integrationinstancejobs.integrations.jupiterone.io spec: diff --git a/charts/jupiterone-integration-operator/templates/crds/integrations.jupiterone.io_integrationinstances.yaml b/charts/jupiterone-integration-operator/templates/crds/integrations.jupiterone.io_integrationinstances.yaml index 5b135d2..6b992c1 100644 --- a/charts/jupiterone-integration-operator/templates/crds/integrations.jupiterone.io_integrationinstances.yaml +++ b/charts/jupiterone-integration-operator/templates/crds/integrations.jupiterone.io_integrationinstances.yaml @@ -2,6 +2,9 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: + {{- if .Values.crd.keep }} + "helm.sh/resource-policy": keep + {{- end }} controller-gen.kubebuilder.io/version: v0.18.0 name: integrationinstances.integrations.jupiterone.io spec: diff --git a/charts/jupiterone-integration-operator/templates/crds/integrations.jupiterone.io_integrationrunners.yaml b/charts/jupiterone-integration-operator/templates/crds/integrations.jupiterone.io_integrationrunners.yaml index 1d2bfa1..1af6711 100644 --- a/charts/jupiterone-integration-operator/templates/crds/integrations.jupiterone.io_integrationrunners.yaml +++ b/charts/jupiterone-integration-operator/templates/crds/integrations.jupiterone.io_integrationrunners.yaml @@ -2,6 +2,9 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: + {{- if .Values.crd.keep }} + "helm.sh/resource-policy": keep + {{- end }} controller-gen.kubebuilder.io/version: v0.18.0 name: integrationrunners.integrations.jupiterone.io spec: diff --git a/charts/jupiterone-integration-operator/templates/integration/job-serviceaccount.yaml b/charts/jupiterone-integration-operator/templates/integration/job-serviceaccount.yaml new file mode 100644 index 0000000..8128594 --- /dev/null +++ b/charts/jupiterone-integration-operator/templates/integration/job-serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if and .Values.integration.jobServiceAccount.create (include "chart.integrationJobServiceAccountName" .) -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "chart.integrationJobServiceAccountName" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "chart.labels" . | nindent 4 }} + {{- with .Values.integration.jobServiceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/jupiterone-integration-operator/templates/integration/serviceaccount.yaml b/charts/jupiterone-integration-operator/templates/integration/serviceaccount.yaml index 4e12da7..9c6a0f7 100644 --- a/charts/jupiterone-integration-operator/templates/integration/serviceaccount.yaml +++ b/charts/jupiterone-integration-operator/templates/integration/serviceaccount.yaml @@ -6,4 +6,8 @@ metadata: namespace: {{ .Values.integration.serviceAccountNamespace }} labels: {{- include "chart.labels" . | nindent 4 }} -{{- end }} \ No newline at end of file + {{- with .Values.integration.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/jupiterone-integration-operator/templates/manager/manager.yaml b/charts/jupiterone-integration-operator/templates/manager/manager.yaml index 01f5b36..fcdc214 100644 --- a/charts/jupiterone-integration-operator/templates/manager/manager.yaml +++ b/charts/jupiterone-integration-operator/templates/manager/manager.yaml @@ -21,7 +21,7 @@ spec: control-plane: controller-manager {{- if and .Values.controllerManager.pod .Values.controllerManager.pod.labels }} {{- range $key, $value := .Values.controllerManager.pod.labels }} - {{ $key }}: {{ $value }} + {{ $key }}: {{ $value | quote }} {{- end }} {{- end }} spec: @@ -45,6 +45,10 @@ spec: fieldPath: metadata.namespace - name: K8S_INTEGRATION_SERVICE_ACCOUNT value: {{ .Values.integration.serviceAccountName }} + {{- with (include "chart.integrationJobServiceAccountName" .) }} + - name: INTEGRATION_JOB_SERVICE_ACCOUNT + value: {{ . | quote }} + {{- end }} {{- if .Values.controllerManager.imageRegistry }} - name: IMAGE_REGISTRY value: {{ .Values.controllerManager.imageRegistry | quote }} diff --git a/charts/jupiterone-integration-operator/templates/prometheus/monitor.yaml b/charts/jupiterone-integration-operator/templates/prometheus/monitor.yaml index 703eb35..c64dd94 100644 --- a/charts/jupiterone-integration-operator/templates/prometheus/monitor.yaml +++ b/charts/jupiterone-integration-operator/templates/prometheus/monitor.yaml @@ -16,7 +16,7 @@ spec: bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token tlsConfig: {{- if .Values.certmanager.enable }} - serverName: jupiterone-integration-operator-controller-manager-metrics-service.{{ .Release.Namespace }}.svc + serverName: jupiterone-integration-operator-metrics-service.{{ .Release.Namespace }}.svc # Apply secure TLS configuration with cert-manager insecureSkipVerify: false ca: diff --git a/charts/jupiterone-integration-operator/tests/declared-values_test.sh b/charts/jupiterone-integration-operator/tests/declared-values_test.sh new file mode 100755 index 0000000..e18d69c --- /dev/null +++ b/charts/jupiterone-integration-operator/tests/declared-values_test.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +set -euo pipefail + +CHART_DIR="helm-charts/charts/jupiterone-integration-operator" +PASSED=0 +FAILED=0 + +if [ ! -d "$CHART_DIR" ]; then + echo "ERROR: Run this script from the repository root" + echo " Expected chart directory: $CHART_DIR" + exit 1 +fi + +# --- Helper functions --- + +assert_contains() { + local description="$1" + local needle="$2" + local haystack="$3" + + if grep -qF -- "$needle" <<<"$haystack"; then + echo " PASS: $description" + PASSED=$((PASSED + 1)) + else + echo " FAIL: $description" + echo " Expected output to contain: $needle" + FAILED=$((FAILED + 1)) + fi +} + +assert_not_contains() { + local description="$1" + local needle="$2" + local haystack="$3" + + if grep -qF -- "$needle" <<<"$haystack"; then + echo " FAIL: $description" + echo " Expected output NOT to contain: $needle" + FAILED=$((FAILED + 1)) + else + echo " PASS: $description" + PASSED=$((PASSED + 1)) + fi +} + +run_test() { + local test_name="$1" + local test_func="$2" + + echo "" + echo "=== Test: $test_name ===" + $test_func +} + +# --- Test cases --- + +test_every_template_value_is_declared() { + local referenced declared missing="" + referenced=$(grep -rhoE '\.Values\.[A-Za-z0-9_.]+' "$CHART_DIR/templates" | sed 's/^\.Values\.//' | sort -u) + declared=$(yq '[.. | path | join(".")] | .[]' "$CHART_DIR/values.yaml" | sed -E 's/\.[0-9]+//g' | grep -v '^$' | sort -u) + + for key in $referenced; do + if ! echo "$declared" | grep -qE "^${key}(\.|$)"; then + missing="$missing $key" + fi + done + + if [ -z "$missing" ]; then + echo " PASS: every .Values reference in templates is declared in values.yaml" + PASSED=$((PASSED + 1)) + else + echo " FAIL: undeclared values referenced by templates:$missing" + FAILED=$((FAILED + 1)) + fi +} + +test_pod_labels() { + local output + output=$(helm template test-release "$CHART_DIR" --set controllerManager.pod.labels.team=platform) + + assert_contains "pod label rendered (quoted string value)" 'team: "platform"' "$output" +} + +test_name_override() { + local output + + output=$(helm template test-release "$CHART_DIR" --set nameOverride=custom-operator) + assert_contains "nameOverride overrides the app.kubernetes.io/name label" "app.kubernetes.io/name: custom-operator" "$output" + assert_not_contains "nameOverride replaces the default chart name" "app.kubernetes.io/name: jupiterone-integration-operator" "$output" + + output=$(helm template test-release "$CHART_DIR") + assert_contains "default name label falls back to the chart name" "app.kubernetes.io/name: jupiterone-integration-operator" "$output" +} + +test_operator_service_account_annotations() { + local output + output=$(helm template test-release "$CHART_DIR" \ + --set 'controllerManager.serviceAccount.annotations.eks\.amazonaws\.com/role-arn=arn:aws:iam::123456789012:role/j1-operator') + + assert_contains "IRSA annotation on operator ServiceAccount" \ + "eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/j1-operator" "$output" +} + +test_metrics_tls_names_match() { + local output + output=$(helm template test-release "$CHART_DIR" \ + --set prometheus.enable=true --set certmanager.enable=true --set crd.keep=true) + + assert_contains "ServiceMonitor serverName matches metrics Service" \ + "serverName: jupiterone-integration-operator-metrics-service.default.svc" "$output" + assert_not_contains "No stale controller-manager-metrics-service serverName" \ + "controller-manager-metrics-service.default.svc" "$output" + assert_contains "Certificate dnsNames include metrics Service" \ + "jupiterone-integration-operator-metrics-service.default.svc" "$output" + assert_contains "crd.keep adds resource-policy keep" '"helm.sh/resource-policy": keep' "$output" +} + +# --- Run all tests --- + +run_test "Template references vs values.yaml" test_every_template_value_is_declared +run_test "controllerManager.pod.labels" test_pod_labels +run_test "nameOverride" test_name_override +run_test "controllerManager.serviceAccount.annotations" test_operator_service_account_annotations +run_test "Prometheus + cert-manager TLS names" test_metrics_tls_names_match + +# --- Summary --- + +echo "" +echo "==============================" +echo "Results: $PASSED passed, $FAILED failed" +echo "==============================" +[ "$FAILED" -eq 0 ] || exit 1 diff --git a/charts/jupiterone-integration-operator/tests/irsa-service-account_test.sh b/charts/jupiterone-integration-operator/tests/irsa-service-account_test.sh new file mode 100755 index 0000000..60a8135 --- /dev/null +++ b/charts/jupiterone-integration-operator/tests/irsa-service-account_test.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +set -euo pipefail + +CHART_DIR="helm-charts/charts/jupiterone-integration-operator" +PASSED=0 +FAILED=0 + +if [ ! -d "$CHART_DIR" ]; then + echo "ERROR: Run this script from the repository root" + echo " Expected chart directory: $CHART_DIR" + exit 1 +fi + +# --- Helper functions --- + +assert_contains() { + local description="$1" + local needle="$2" + local haystack="$3" + + if grep -qF -- "$needle" <<<"$haystack"; then + echo " PASS: $description" + PASSED=$((PASSED + 1)) + else + echo " FAIL: $description" + echo " Expected output to contain: $needle" + FAILED=$((FAILED + 1)) + fi +} + +assert_not_contains() { + local description="$1" + local needle="$2" + local haystack="$3" + + if grep -qF -- "$needle" <<<"$haystack"; then + echo " FAIL: $description" + echo " Expected output NOT to contain: $needle" + FAILED=$((FAILED + 1)) + else + echo " PASS: $description" + PASSED=$((PASSED + 1)) + fi +} + +run_test() { + local test_name="$1" + local test_func="$2" + + echo "" + echo "=== Test: $test_name ===" + $test_func +} + +# --- Test cases --- + +test_default_values() { + local output + output=$(helm template test-release "$CHART_DIR") + + assert_not_contains "No job ServiceAccount by default" \ + "name: jupiterone-integration-job" "$output" + assert_not_contains "No INTEGRATION_JOB_SERVICE_ACCOUNT by default" \ + "INTEGRATION_JOB_SERVICE_ACCOUNT" "$output" + assert_contains "Integration ServiceAccount still rendered" \ + "name: jupiterone" "$output" +} + +test_integration_service_account_annotations() { + local output + output=$(helm template test-release "$CHART_DIR" \ + --set 'integration.serviceAccount.annotations.eks\.amazonaws\.com/role-arn=arn:aws:iam::123456789012:role/j1-k8s') + + assert_contains "IRSA annotation on the kubernetes-managed ServiceAccount" \ + "eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/j1-k8s" "$output" +} + +test_job_service_account_created() { + local output + output=$(helm template test-release "$CHART_DIR" \ + --set integration.jobServiceAccount.create=true \ + --set 'integration.jobServiceAccount.annotations.eks\.amazonaws\.com/role-arn=arn:aws:iam::123456789012:role/j1-job') + + assert_contains "Job ServiceAccount created with the default name" \ + "name: jupiterone-integration-job" "$output" + assert_contains "IRSA annotation on the job ServiceAccount" \ + "eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/j1-job" "$output" + assert_contains "INTEGRATION_JOB_SERVICE_ACCOUNT env var name present" \ + "name: INTEGRATION_JOB_SERVICE_ACCOUNT" "$output" + assert_contains "INTEGRATION_JOB_SERVICE_ACCOUNT value is correct" \ + 'value: "jupiterone-integration-job"' "$output" +} + +test_job_service_account_custom_name() { + local output + output=$(helm template test-release "$CHART_DIR" \ + --set integration.jobServiceAccount.create=true \ + --set integration.jobServiceAccount.name=sbom-runner) + + assert_contains "Job ServiceAccount uses the configured name" \ + "name: sbom-runner" "$output" + assert_contains "INTEGRATION_JOB_SERVICE_ACCOUNT uses the configured name" \ + 'value: "sbom-runner"' "$output" +} + +test_job_service_account_externally_managed() { + local output + output=$(helm template test-release "$CHART_DIR" \ + --set integration.jobServiceAccount.create=false \ + --set integration.jobServiceAccount.name=externally-managed) + + assert_contains "Operator points at the externally managed ServiceAccount" \ + 'value: "externally-managed"' "$output" + assert_not_contains "Chart does not create the externally managed ServiceAccount" \ + " name: externally-managed" "$output" +} + +# --- Run all tests --- + +run_test "Default values (backwards compatibility)" test_default_values +run_test "integration.serviceAccount.annotations set" test_integration_service_account_annotations +run_test "integration.jobServiceAccount created" test_job_service_account_created +run_test "integration.jobServiceAccount custom name" test_job_service_account_custom_name +run_test "integration.jobServiceAccount managed externally" test_job_service_account_externally_managed + +# --- Summary --- + +echo "" +echo "==============================" +echo "Results: $PASSED passed, $FAILED failed" +echo "==============================" +[ "$FAILED" -eq 0 ] || exit 1 diff --git a/charts/jupiterone-integration-operator/tests/private-registry-values_test.sh b/charts/jupiterone-integration-operator/tests/private-registry-values_test.sh index 45e218a..3bf4815 100755 --- a/charts/jupiterone-integration-operator/tests/private-registry-values_test.sh +++ b/charts/jupiterone-integration-operator/tests/private-registry-values_test.sh @@ -18,7 +18,7 @@ assert_contains() { local needle="$2" local haystack="$3" - if echo "$haystack" | grep -qF "$needle"; then + if grep -qF -- "$needle" <<<"$haystack"; then echo " PASS: $description" PASSED=$((PASSED + 1)) else @@ -33,7 +33,7 @@ assert_not_contains() { local needle="$2" local haystack="$3" - if echo "$haystack" | grep -qF "$needle"; then + if grep -qF -- "$needle" <<<"$haystack"; then echo " FAIL: $description" echo " Expected output NOT to contain: $needle" FAILED=$((FAILED + 1)) diff --git a/charts/jupiterone-integration-operator/values.yaml b/charts/jupiterone-integration-operator/values.yaml index 22a6ad3..f23f76a 100644 --- a/charts/jupiterone-integration-operator/values.yaml +++ b/charts/jupiterone-integration-operator/values.yaml @@ -1,3 +1,6 @@ +# Overrides the chart name used in the app.kubernetes.io/name label. +nameOverride: "" + # [MANAGER]: Manager Deployment Configurations controllerManager: replicas: 1 @@ -43,6 +46,9 @@ controllerManager: runAsNonRoot: true seccompProfile: type: RuntimeDefault + # Extra labels on the manager pod. + pod: + labels: {} terminationGracePeriodSeconds: 10 serviceAccountName: jupiterone-integration-operator-controller-manager # Annotations applied to the operator ServiceAccount. @@ -99,6 +105,13 @@ prometheus: certmanager: enable: false +# [CRD]: Keep the operator CRDs (IntegrationRunner, IntegrationInstance, +# IntegrationInstanceJob) and the metrics Certificate on helm uninstall by +# annotating them helm.sh/resource-policy: keep. Custom resources survive the +# uninstall; delete the CRDs by hand to remove them. +crd: + keep: false + # [NETWORK POLICIES]: To enable NetworkPolicies set true networkPolicy: enable: false @@ -112,3 +125,28 @@ integration: # Service account namespace for the cluster role binding # NOTE: We don't support changing this at this time serviceAccountNamespace: jupiterone + # Annotations applied to the kubernetes-managed integration ServiceAccount + # above. Use this to give that integration an AWS identity via IRSA. + # Example: + # serviceAccount: + # annotations: + # eks.amazonaws.com/role-arn: arn:aws:iam:::role/ + serviceAccount: + annotations: {} + # ServiceAccount used by every other integration job pod (for example SBOM for + # AWS ECR). Integration job pods run as "default" until a name is set here, + # which is passed to the operator as INTEGRATION_JOB_SERVICE_ACCOUNT. + # Annotate it to grant the job pods an AWS identity via IRSA, including + # cross-account access where the annotated role assumes a role in the account + # that owns the ECR registry. + # Example: + # jobServiceAccount: + # create: true + # annotations: + # eks.amazonaws.com/role-arn: arn:aws:iam:::role/ + jobServiceAccount: + # Create the ServiceAccount. Set to false to reference one managed elsewhere. + create: false + # Defaults to "jupiterone-integration-job" when created. + name: "" + annotations: {} diff --git a/charts/jupiterone-integration-runner/Chart.yaml b/charts/jupiterone-integration-runner/Chart.yaml index 98e3ed2..9af30e5 100644 --- a/charts/jupiterone-integration-runner/Chart.yaml +++ b/charts/jupiterone-integration-runner/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: jupiterone-integration-runner description: A Helm chart for the JupiterOne Integration Runner type: application -version: 1.0.3 +version: 1.1.0 appVersion: "v1.0.0" diff --git a/charts/jupiterone-integration-runner/README.md b/charts/jupiterone-integration-runner/README.md index 40f07a2..e1eecc6 100644 --- a/charts/jupiterone-integration-runner/README.md +++ b/charts/jupiterone-integration-runner/README.md @@ -1,13 +1,19 @@ # JupiterOne Integration Runner -This chart installs a single instance of the Runner using a Custom Resource. +This chart installs a single `IntegrationRunner` custom resource. The +[JupiterOne Integration Operator](../jupiterone-integration-operator) reconciles +it: it registers the runner with JupiterOne using your API token, then spawns a +Kubernetes Job for every integration instance assigned to the runner. The +chart itself creates no Deployment, Pod or ServiceAccount. ## Prerequisites - Kubernetes 1.16+ - Helm 3+ -- Access to a JupiterOne account with API credentials -- JupiterOne Integration Operator helm chart installed +- The `jupiterone-integration-operator` chart installed in the same cluster +- A JupiterOne account API token with **Collector -- CRUD** and **Shared: + Graph Data -- Read/Write** permissions (Settings > Account API Tokens) +- Your JupiterOne account ID (Settings > Account Management) ## Installation @@ -18,100 +24,148 @@ helm repo add jupiterone https://jupiterone.github.io/helm-charts helm repo update ``` -### 2. Create the Namespace +### 2. Install the Runner -All resources are created in the namespace `jupiterone`. If it does not exist, create it: +Install into the namespace the operator watches (`jupiterone` by default). +Replace `` and ``: ```console -kubectl create namespace jupiterone +helm install integration-runner jupiterone/jupiterone-integration-runner \ + --namespace jupiterone \ + --set accountID= \ + --set apiToken= ``` -### 3. Install the Runner +The chart fails to render if `accountID` is missing or `apiToken` is left at +its placeholder while `createSecret` is true. -Replace ``, and `` with the correct values +### 3. Verify Installation ```console -helm install operator-1 jupiterone/jupiterone-integration-runner \ - --namespace jupiterone \ - --set accountID= \ - --set apiToken= +kubectl get integrationrunner -n jupiterone +# NAME STATE REGISTRATION +# integration-runner running registered ``` -#### Example +Registration takes about 30 seconds. If `STATE` stays `pending`, check the +operator logs: ```console +kubectl logs -n jupiterone deploy/jupiterone-integration-operator-controller-manager +``` + +## Parameters + +| Parameter | Description | Default | +|---|---|---| +| `accountID` | JupiterOne account ID. Required. | `` | +| `jupiterOneEnvironment` | JupiterOne environment the runner connects to (`us`, `eu`, `gov`, ...). | `us` | +| `syncIntervalSeconds` | How often, in seconds, the runner polls JupiterOne for work. | `30` | +| `createSecret` | Create a Kubernetes Secret named `secretAPITokenName` from `apiToken`. Set `false` when the Secret is managed elsewhere or the token comes from AWS Secrets Manager. | `true` | +| `apiToken` | JupiterOne API token. Required when `createSecret` is `true`. | `` | +| `secretAPITokenName` | Name of the Kubernetes Secret holding the token under the `token` key. | `j1token` | +| `apiTokenSource.provider` | Where the runner reads the token: `kubernetes` or `awsSecretsManager`. Empty uses `secretAPITokenName`. | `""` | +| `apiTokenSource.kubernetes.name` | Kubernetes Secret name when `provider` is `kubernetes`. Defaults to `secretAPITokenName`. | `""` | +| `apiTokenSource.awsSecretsManager.secretId` | ARN or name of the AWS Secrets Manager secret. Required when `provider` is `awsSecretsManager`. | `""` | +| `apiTokenSource.awsSecretsManager.region` | Region of the secret. Defaults to the operator's region. | `""` | +| `apiTokenSource.awsSecretsManager.versionStage` | Staging label to read. Defaults to `AWSCURRENT`. | `""` | + +### Existing Kubernetes Secret + +Create the Secret yourself, with the token under the `token` key, and point the +runner at it: + +```console +kubectl create secret generic j1token -n jupiterone --from-literal=token= + helm install integration-runner jupiterone/jupiterone-integration-runner \ --namespace jupiterone \ - --set collectorID=abcd1234 \ - --set accountID=efgh5678 \ - --set authToken=your-token-here + --set accountID= \ + --set createSecret=false \ + --set secretAPITokenName=j1token ``` -### 4. Verify Installation - -Check that the runner is installed +### AWS Secrets Manager + +Store the token as a JSON object, `{ "token": "" }`, and reference +it. The operator resolves the secret, so the **operator's** ServiceAccount +needs an IAM role with `secretsmanager:GetSecretValue` on it -- see +[AWS authentication (IRSA)](https://github.com/JupiterOne/jupiterone-integration-operator#aws-authentication-irsa). + +```yaml +# values.yaml +accountID: +createSecret: false +apiTokenSource: + provider: awsSecretsManager + awsSecretsManager: + secretId: jupiterone/runner-api-token + region: us-east-1 +``` ```console -kubectl get integrationrunner -n jupiterone +helm install integration-runner jupiterone/jupiterone-integration-runner \ + --namespace jupiterone -f values.yaml ``` -## Configuration +## ServiceAccounts and AWS identity + +The runner has no pod, so this chart has no ServiceAccount to annotate. The +identities involved are all configured on the operator chart: -You can customize the installation using Helm values. For example, to set resource limits or configure logging, update your `values.yaml` or pass additional flags to `helm install`. +| Workload | Chart value on `jupiterone-integration-operator` | +|---|---| +| Operator (reads AWS Secrets Manager) | `controllerManager.serviceAccount.annotations` | +| Integration job pods spawned for this runner | `integration.jobServiceAccount` | +| `kubernetes-managed` integration job pods | `integration.serviceAccount.annotations` | -Refer to the [values.yaml](./values.yaml) for all available configuration options. +Integrations that call AWS from the job pod -- for example SBOM for AWS ECR -- +get their IAM role through those values, not through this chart. ## Usage ### Set Default Namespace -To avoid specifying `-n jupiterone` in every command: - ```console kubectl config set-context --current --namespace jupiterone ``` -### List Integration Runners +### Inspect the runner and its jobs ```console kubectl get integrationrunner -``` - -### List Integration Instance Jobs - -```console kubectl get integrationinstancejob -``` - -### List Kubernetes Jobs - -```console kubectl get job ``` -## Updating the Operator +### Multiple runners -To upgrade to a newer version: +Each release is one runner. Install the chart again with a different release +name to add another runner to the same account, or into another namespace +watched by its own operator. + +## Updating the Runner ```console helm repo update -helm upgrade integration-operator jupiterone/jupiterone-integration-operator --namespace jupiterone +helm upgrade integration-runner jupiterone/jupiterone-integration-runner --namespace jupiterone --reuse-values ``` ## Uninstalling -To remove the operator and all related resources: - ```console -helm uninstall integration-operator --namespace jupiterone -kubectl delete namespace jupiterone +helm uninstall integration-runner --namespace jupiterone ``` +The operator deregisters the runner from JupiterOne. Integration instances +assigned to it stop running until reassigned. + ## Troubleshooting -- **Pod not starting:** Check logs with `kubectl logs `. -- **CRDs not found:** Ensure the operator pod is running and healthy. -- **Authentication errors:** Double-check your `collectorID`, `accountID`, and `authToken`. +- **`STATE` stays `pending`:** the API token lacks Collector CRUD permission, or `jupiterOneEnvironment` does not match the account's region. Check the operator logs. +- **`secret not found`:** `createSecret=false` but no Secret named `secretAPITokenName` exists in the release namespace, or it lacks a `token` key. +- **`AWS Secrets Manager provider is not configured`:** the operator has no AWS credentials. Annotate its ServiceAccount for IRSA (operator chart `controllerManager.serviceAccount.annotations`). +- **Integration jobs fail with `Configuration failure`:** the job pod has no AWS identity. Set `integration.jobServiceAccount` on the operator chart. ## Support diff --git a/charts/jupiterone-integration-runner/templates/runner.yaml b/charts/jupiterone-integration-runner/templates/runner.yaml index 663083c..3ee723c 100644 --- a/charts/jupiterone-integration-runner/templates/runner.yaml +++ b/charts/jupiterone-integration-runner/templates/runner.yaml @@ -1,11 +1,35 @@ +{{- if or (not .Values.accountID) (eq (toString .Values.accountID) "") }} +{{- fail "accountID is required (JupiterOne UI > Settings > Account Management)" }} +{{- end }} apiVersion: integrations.jupiterone.io/v1 kind: IntegrationRunner metadata: name: {{ .Release.Name }} namespace: {{ .Release.Namespace }} spec: - accountId: {{ .Values.accountID }} + accountId: {{ .Values.accountID | quote }} secretName: {{ .Release.Name }}-j1internal secretAPITokenName: {{ .Values.secretAPITokenName }} syncIntervalSeconds: {{ .Values.syncIntervalSeconds }} jupiterOneEnvironment: {{ .Values.jupiterOneEnvironment }} + {{- with .Values.apiTokenSource }} + {{- if .provider }} + apiTokenSource: + provider: {{ .provider }} + {{- if eq .provider "kubernetes" }} + kubernetes: + name: {{ .kubernetes.name | default $.Values.secretAPITokenName }} + {{- else if eq .provider "awsSecretsManager" }} + awsSecretsManager: + secretId: {{ required "apiTokenSource.awsSecretsManager.secretId is required when provider is awsSecretsManager" .awsSecretsManager.secretId }} + {{- with .awsSecretsManager.region }} + region: {{ . }} + {{- end }} + {{- with .awsSecretsManager.versionStage }} + versionStage: {{ . }} + {{- end }} + {{- else }} + {{- fail (printf "apiTokenSource.provider must be \"kubernetes\" or \"awsSecretsManager\", got %q" .provider) }} + {{- end }} + {{- end }} + {{- end }} diff --git a/charts/jupiterone-integration-runner/templates/secret.yaml b/charts/jupiterone-integration-runner/templates/secret.yaml index 3f1c72d..fed07c0 100644 --- a/charts/jupiterone-integration-runner/templates/secret.yaml +++ b/charts/jupiterone-integration-runner/templates/secret.yaml @@ -1,4 +1,7 @@ -{{if .Values.createSecret}} +{{- if .Values.createSecret }} +{{- if or (not .Values.apiToken) (eq .Values.apiToken "") }} +{{- fail "apiToken is required when createSecret is true (or set createSecret=false and provide the Secret yourself)" }} +{{- end }} apiVersion: v1 kind: Secret metadata: @@ -7,4 +10,4 @@ metadata: type: Opaque data: token: {{ .Values.apiToken | b64enc }} -{{- end }} \ No newline at end of file +{{- end }} diff --git a/charts/jupiterone-integration-runner/tests/runner-values_test.sh b/charts/jupiterone-integration-runner/tests/runner-values_test.sh new file mode 100755 index 0000000..830c8c3 --- /dev/null +++ b/charts/jupiterone-integration-runner/tests/runner-values_test.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +set -euo pipefail + +CHART_DIR="helm-charts/charts/jupiterone-integration-runner" +PASSED=0 +FAILED=0 + +if [ ! -d "$CHART_DIR" ]; then + echo "ERROR: Run this script from the repository root" + echo " Expected chart directory: $CHART_DIR" + exit 1 +fi + +# --- Helper functions --- + +assert_contains() { + local description="$1" + local needle="$2" + local haystack="$3" + + if grep -qF -- "$needle" <<<"$haystack"; then + echo " PASS: $description" + PASSED=$((PASSED + 1)) + else + echo " FAIL: $description" + echo " Expected output to contain: $needle" + FAILED=$((FAILED + 1)) + fi +} + +assert_not_contains() { + local description="$1" + local needle="$2" + local haystack="$3" + + if grep -qF -- "$needle" <<<"$haystack"; then + echo " FAIL: $description" + echo " Expected output NOT to contain: $needle" + FAILED=$((FAILED + 1)) + else + echo " PASS: $description" + PASSED=$((PASSED + 1)) + fi +} + +assert_render_fails() { + local description="$1" + local needle="$2" + shift 2 + + local output + if output=$(helm template test-release "$CHART_DIR" "$@" 2>&1); then + echo " FAIL: $description" + echo " Expected helm template to fail" + FAILED=$((FAILED + 1)) + elif grep -qF -- "$needle" <<<"$output"; then + echo " PASS: $description" + PASSED=$((PASSED + 1)) + else + echo " FAIL: $description" + echo " Expected error to contain: $needle" + FAILED=$((FAILED + 1)) + fi +} + +run_test() { + local test_name="$1" + local test_func="$2" + + echo "" + echo "=== Test: $test_name ===" + $test_func +} + +# --- Test cases --- + +test_kubernetes_secret_created() { + local output + output=$(helm template test-release "$CHART_DIR" \ + --set accountID=acct-1 --set apiToken=tok) + + assert_contains "IntegrationRunner rendered" "kind: IntegrationRunner" "$output" + assert_contains "accountId is quoted" 'accountId: "acct-1"' "$output" + assert_contains "Secret created" "kind: Secret" "$output" + assert_contains "Secret named by secretAPITokenName" "name: j1token" "$output" + assert_contains "token base64 encoded" "token: dG9r" "$output" + assert_not_contains "No apiTokenSource without a provider" "apiTokenSource:" "$output" +} + +test_external_secret() { + local output + output=$(helm template test-release "$CHART_DIR" \ + --set accountID=acct-1 --set createSecret=false --set secretAPITokenName=external) + + assert_not_contains "No Secret when createSecret=false" "kind: Secret" "$output" + assert_contains "Runner references external secret" "secretAPITokenName: external" "$output" +} + +test_api_token_source_kubernetes() { + local output + output=$(helm template test-release "$CHART_DIR" \ + --set accountID=acct-1 --set createSecret=false \ + --set apiTokenSource.provider=kubernetes) + + assert_contains "provider kubernetes" "provider: kubernetes" "$output" + assert_contains "kubernetes name defaults to secretAPITokenName" "name: j1token" "$output" +} + +test_api_token_source_asm() { + local output + output=$(helm template test-release "$CHART_DIR" \ + --set accountID=acct-1 --set createSecret=false \ + --set apiTokenSource.provider=awsSecretsManager \ + --set apiTokenSource.awsSecretsManager.secretId=jupiterone/runner \ + --set apiTokenSource.awsSecretsManager.region=us-east-1 \ + --set apiTokenSource.awsSecretsManager.versionStage=AWSCURRENT) + + assert_contains "provider awsSecretsManager" "provider: awsSecretsManager" "$output" + assert_contains "secretId rendered" "secretId: jupiterone/runner" "$output" + assert_contains "region rendered" "region: us-east-1" "$output" + assert_contains "versionStage rendered" "versionStage: AWSCURRENT" "$output" +} + +test_validation() { + assert_render_fails "Placeholder apiToken rejected" "apiToken is required" \ + --set accountID=acct-1 + assert_render_fails "Missing accountID rejected" "accountID is required" \ + --set apiToken=tok + assert_render_fails "ASM provider without secretId rejected" "secretId is required" \ + --set accountID=acct-1 --set createSecret=false --set apiTokenSource.provider=awsSecretsManager + assert_render_fails "Unknown provider rejected" "must be" \ + --set accountID=acct-1 --set createSecret=false --set apiTokenSource.provider=vault +} + +# --- Run all tests --- + +run_test "Kubernetes Secret created (default path)" test_kubernetes_secret_created +run_test "External Secret" test_external_secret +run_test "apiTokenSource kubernetes" test_api_token_source_kubernetes +run_test "apiTokenSource awsSecretsManager" test_api_token_source_asm +run_test "Validation" test_validation + +# --- Summary --- + +echo "" +echo "==============================" +echo "Results: $PASSED passed, $FAILED failed" +echo "==============================" +[ "$FAILED" -eq 0 ] || exit 1 diff --git a/charts/jupiterone-integration-runner/values.yaml b/charts/jupiterone-integration-runner/values.yaml index 6bdaa85..795acb4 100644 --- a/charts/jupiterone-integration-runner/values.yaml +++ b/charts/jupiterone-integration-runner/values.yaml @@ -1,19 +1,48 @@ -# The name of the secret that holds the API token. -# The key in the secret must be 'token' -secretAPITokenName: j1token +# The JupiterOne account ID, found in the JupiterOne UI under Settings > Account Management +accountID: + +# The environment for JupiterOne API +jupiterOneEnvironment: us + +# The interval in seconds for syncing data with JupiterOne +syncIntervalSeconds: 30 -# Set this to false if you are using an external secret +# Create a Kubernetes Secret holding the API token from `apiToken` below. +# Set to false when the Secret is managed elsewhere, or when reading the token +# from AWS Secrets Manager via `apiTokenSource`. createSecret: true # This is an account level API token # This is only required if 'createSecret' is true apiToken: -# The JupiterOne account ID, found in the JupiterOne UI under Settings > Account Management -accountID: - -# The interval in seconds for syncing data with JupiterOne -syncIntervalSeconds: 30 +# The name of the Kubernetes Secret that holds the API token. +# The key in the secret must be 'token'. Created by this chart when +# `createSecret` is true; referenced by the runner otherwise. +secretAPITokenName: j1token -# The environment for JupiterOne API -jupiterOneEnvironment: us +# Where the runner reads its JupiterOne API token from. Leave `provider` empty +# to use the Kubernetes Secret named by `secretAPITokenName`. +# The resolved secret must be a JSON object with a "token" key. +# Example (AWS Secrets Manager; the operator must have IRSA access, see the +# jupiterone-integration-operator chart): +# createSecret: false +# apiTokenSource: +# provider: awsSecretsManager +# awsSecretsManager: +# secretId: jupiterone/runner-api-token +# region: us-east-1 +# versionStage: AWSCURRENT +apiTokenSource: + # "kubernetes" or "awsSecretsManager" + provider: "" + kubernetes: + # Defaults to `secretAPITokenName` when empty + name: "" + awsSecretsManager: + # ARN or name of the secret; required when provider is awsSecretsManager + secretId: "" + # Defaults to the operator's region when empty + region: "" + # Defaults to AWSCURRENT when empty + versionStage: ""