diff --git a/.dev/ko-build-faucet.sh b/.dev/ko-build-faucet.sh deleted file mode 100755 index d923eb74..00000000 --- a/.dev/ko-build-faucet.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -: "${EXPECTED_REF:?Tilt must set EXPECTED_REF for the faucet image build}" - -export CGO_ENABLED="${CGO_ENABLED:-0}" - -built_ref="$(ko build --local ./services/faucet/cmd/yacd-faucet | tail -n 1)" -docker tag "$built_ref" "$EXPECTED_REF" diff --git a/.dev/scripts/check.sh b/.dev/scripts/check.sh index 4cb7cced..afabbe52 100755 --- a/.dev/scripts/check.sh +++ b/.dev/scripts/check.sh @@ -2,8 +2,8 @@ set -euo pipefail echo "== go format ==" -go_roots=(charts cli cmd containers/cardano-testnet containers/cardano-tools services test) -for optional_dir in api internal; do +go_roots=(charts cli cmd containers/cardano-testnet containers/cardano-tools test) +for optional_dir in api internal services; do if [ -d "$optional_dir" ]; then go_roots+=("$optional_dir") fi diff --git a/.dev/scripts/deploy.sh b/.dev/scripts/deploy.sh index f1be5551..44f4d728 100755 --- a/.dev/scripts/deploy.sh +++ b/.dev/scripts/deploy.sh @@ -4,7 +4,6 @@ set -euo pipefail release="${HELM_RELEASE:-yacd}" namespace="${HELM_NAMESPACE:-yacd-system}" image="${IMG:-}" -faucet_image="${FAUCET_IMG:-}" shopt -s nullglob crds=(charts/yacd/crds/*.yaml) @@ -40,24 +39,6 @@ if [ -n "$image" ]; then fi fi -if [ -n "$faucet_image" ]; then - if [[ "$faucet_image" == *@* ]]; then - args+=(--set-string "faucet.image.repository=${faucet_image%@*}") - args+=(--set-string "faucet.image.digest=${faucet_image#*@}") - args+=(--set-string "faucet.image.tag=") - else - last_segment="${faucet_image##*/}" - if [[ "$last_segment" == *:* ]]; then - args+=(--set-string "faucet.image.repository=${faucet_image%:*}") - args+=(--set-string "faucet.image.tag=${faucet_image##*:}") - else - args+=(--set-string "faucet.image.repository=$faucet_image") - args+=(--set-string "faucet.image.tag=") - fi - args+=(--set-string "faucet.image.digest=") - fi -fi - if [ "${LOCAL_IMAGE:-false}" = "true" ]; then args+=(--set "image.pullPolicy=IfNotPresent") fi diff --git a/.dev/scripts/test-e2e.sh b/.dev/scripts/test-e2e.sh index 228c2c2d..97f6f33a 100755 --- a/.dev/scripts/test-e2e.sh +++ b/.dev/scripts/test-e2e.sh @@ -6,7 +6,6 @@ command -v "$kind_bin" >/dev/null cluster="${KIND_CLUSTER:-yacd-test-e2e}" manager_image="${IMG:-example.com/yacd:v0.0.1}" -faucet_image="${FAUCET_IMG:-example.com/yacd-faucet:v0.0.1}" kubeconfig_dir="$(mktemp -d)" kubeconfig="$kubeconfig_dir/kubeconfig" created=0 @@ -31,14 +30,12 @@ fi "$kind_bin" export kubeconfig --name "$cluster" --kubeconfig "$kubeconfig" export KUBECONFIG="$kubeconfig" -# The manager and faucet images are the code under test, so they are built from -# source and loaded into Kind. The cardano-testnet and cardano-tools images are -# published, digest-pinned manager defaults, so Kind pulls them at pod-creation -# time rather than building them here. +# The manager image is the code under test, so it is built from source and +# loaded into Kind. The cardano-testnet and cardano-tools images are published, +# digest-pinned manager defaults, so Kind pulls them at pod-creation time rather +# than building them here. docker build -t "$manager_image" . -docker build -f services/faucet/Dockerfile -t "$faucet_image" . "$kind_bin" load docker-image "$manager_image" --name "$cluster" -"$kind_bin" load docker-image "$faucet_image" --name "$cluster" -KIND="$kind_bin" KIND_CLUSTER="$cluster" IMG="$manager_image" FAUCET_IMG="$faucet_image" KUBECTL_KUBERC="${KUBECTL_KUBERC:-false}" \ +KIND="$kind_bin" KIND_CLUSTER="$cluster" IMG="$manager_image" KUBECTL_KUBERC="${KUBECTL_KUBERC:-false}" \ chainsaw test --config test/chainsaw/chainsaw-config.yaml test/chainsaw diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aaa9f2a6..a048ed5c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,6 @@ permissions: {} env: IMAGE_NAME: ghcr.io/meigma/yacd - FAUCET_IMAGE_NAME: ghcr.io/meigma/yacd/faucet CHART_NAME: ghcr.io/meigma/yacd/chart CHART_REF: oci://ghcr.io/meigma/yacd/chart CHART_REPOSITORY: oci://ghcr.io/meigma/yacd @@ -405,224 +404,12 @@ jobs: subject-digest: ${{ steps.manifest.outputs.digest }} push-to-registry: true - faucet-container-image-build: - name: Faucet Container Image Build (${{ matrix.platform }}) - runs-on: ${{ matrix.runner }} - needs: - - resolve-release - - binary-release-assets - permissions: - contents: read - packages: write - id-token: write - attestations: write - artifact-metadata: write - strategy: - fail-fast: false - matrix: - include: - - platform: linux/amd64 - runner: ubuntu-24.04 - arch: amd64 - - platform: linux/arm64 - runner: ubuntu-24.04-arm - arch: arm64 - steps: - - name: Check out repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - fetch-depth: 0 - - - name: Check out requested tag - if: ${{ github.event_name == 'workflow_dispatch' }} - env: - RELEASE_TAG: ${{ needs.resolve-release.outputs.tag }} - run: git checkout --detach "$RELEASE_TAG" - - - name: Resolve image metadata - id: image - env: - RELEASE_TAG: ${{ needs.resolve-release.outputs.tag }} - RELEASE_VERSION: ${{ needs.resolve-release.outputs.version }} - run: | - set -euo pipefail - - echo "name=${FAUCET_IMAGE_NAME}" >> "$GITHUB_OUTPUT" - echo "commit=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - echo "date=$(git show -s --format=%cI HEAD)" >> "$GITHUB_OUTPUT" - echo "tag=${FAUCET_IMAGE_NAME}:${RELEASE_TAG}" >> "$GITHUB_OUTPUT" - echo "version=${RELEASE_VERSION}" >> "$GITHUB_OUTPUT" - - - name: Log in to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ github.token }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 - - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 - with: - images: ${{ steps.image.outputs.name }} - tags: | - type=raw,value=${{ needs.resolve-release.outputs.tag }} - labels: | - org.opencontainers.image.title=yacd-faucet - org.opencontainers.image.description=YACD local development faucet service - org.opencontainers.image.source=https://github.com/${{ github.repository }} - org.opencontainers.image.version=${{ needs.resolve-release.outputs.version }} - org.opencontainers.image.revision=${{ steps.image.outputs.commit }} - - - name: Build and push faucet container image - id: build - uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7 - with: - context: . - file: services/faucet/Dockerfile - platforms: ${{ matrix.platform }} - outputs: type=image,name=${{ steps.image.outputs.name }},push-by-digest=true,name-canonical=true,push=true - labels: ${{ steps.meta.outputs.labels }} - provenance: mode=max - sbom: true - cache-from: type=gha,scope=yacd-faucet-release-${{ matrix.arch }} - cache-to: type=gha,mode=max,scope=yacd-faucet-release-${{ matrix.arch }} - build-args: | - VERSION=${{ steps.image.outputs.version }} - COMMIT=${{ steps.image.outputs.commit }} - DATE=${{ steps.image.outputs.date }} - - - name: Smoke test faucet platform image - env: - IMAGE_REF: ${{ steps.image.outputs.name }}@${{ steps.build.outputs.digest }} - run: docker run --rm --platform "${{ matrix.platform }}" "$IMAGE_REF" --version >/tmp/yacd-faucet-version.txt - - - name: Export faucet platform digest - env: - DIGEST: ${{ steps.build.outputs.digest }} - run: | - set -euo pipefail - - if [[ ! "$DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then - echo "unexpected faucet image digest: $DIGEST" >&2 - exit 1 - fi - - mkdir -p /tmp/faucet-container-digests - touch "/tmp/faucet-container-digests/${DIGEST#sha256:}" - - - name: Upload faucet platform digest - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: faucet-container-digest-${{ matrix.arch }} - path: /tmp/faucet-container-digests/* - if-no-files-found: error - retention-days: 1 - - faucet-container-image-release: - name: Faucet Container Image Release - runs-on: ubuntu-24.04 - needs: - - resolve-release - - binary-release-assets - - faucet-container-image-build - permissions: - contents: read - packages: write - id-token: write - attestations: write - artifact-metadata: write - outputs: - image-name: ${{ steps.manifest.outputs.name }} - image-digest: ${{ steps.manifest.outputs.digest }} - steps: - - name: Resolve faucet image metadata - id: image - env: - RELEASE_TAG: ${{ needs.resolve-release.outputs.tag }} - run: | - set -euo pipefail - - echo "name=${FAUCET_IMAGE_NAME}" >> "$GITHUB_OUTPUT" - echo "tag=${FAUCET_IMAGE_NAME}:${RELEASE_TAG}" >> "$GITHUB_OUTPUT" - - - name: Log in to GitHub Container Registry - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ github.token }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 - - - name: Download faucet platform digests - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: faucet-container-digest-* - path: /tmp/faucet-container-digests - merge-multiple: true - - - name: Create faucet multi-platform manifest - id: manifest - env: - IMAGE_NAME: ${{ steps.image.outputs.name }} - IMAGE_TAG: ${{ steps.image.outputs.tag }} - run: | - set -euo pipefail - - mapfile -t digest_files < <(find /tmp/faucet-container-digests -maxdepth 1 -type f -print | sort) - if [ "${#digest_files[@]}" -ne 2 ]; then - echo "expected 2 faucet platform digest files, found ${#digest_files[@]}" >&2 - printf '%s\n' "${digest_files[@]}" >&2 - exit 1 - fi - - refs=() - for digest_file in "${digest_files[@]}"; do - digest="$(basename "$digest_file")" - if [[ ! "$digest" =~ ^[0-9a-f]{64}$ ]]; then - echo "unexpected faucet digest artifact name: $digest" >&2 - exit 1 - fi - refs+=("${IMAGE_NAME}@sha256:${digest}") - done - - docker buildx imagetools create --tag "$IMAGE_TAG" "${refs[@]}" - - manifest_json="$(docker buildx imagetools inspect "$IMAGE_TAG" --format '{{json .}}')" - manifest_digest="$(jq -r '.manifest.digest' <<< "$manifest_json")" - - if [[ ! "$manifest_digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then - echo "unexpected faucet manifest digest: $manifest_digest" >&2 - exit 1 - fi - - echo "name=${IMAGE_NAME}" >> "$GITHUB_OUTPUT" - echo "tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT" - echo "digest=${manifest_digest}" >> "$GITHUB_OUTPUT" - - - name: Smoke test faucet release image - run: docker run --rm "${{ steps.manifest.outputs.tag }}" --version >/tmp/yacd-faucet-version.txt - - - name: Attest faucet container image - uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 - with: - subject-name: ${{ steps.manifest.outputs.name }} - subject-digest: ${{ steps.manifest.outputs.digest }} - push-to-registry: true - helm-chart-release: name: Helm Chart Release runs-on: ubuntu-24.04 needs: - resolve-release - container-image-release - - faucet-container-image-release permissions: contents: read packages: write @@ -688,7 +475,6 @@ jobs: grep -Fq "app.kubernetes.io/name: yacd" /tmp/yacd-chart.yaml grep -Fq "helm.sh/chart: chart-${RELEASE_VERSION}" /tmp/yacd-chart.yaml grep -Fq "image: \"${IMAGE_NAME}:v${RELEASE_VERSION}\"" /tmp/yacd-chart.yaml - grep -Fq -- "--default-faucet-image=${FAUCET_IMAGE_NAME}:v${RELEASE_VERSION}" /tmp/yacd-chart.yaml helm install yacd "$archive" --namespace yacd-system --dry-run=client --server-side=false >/dev/null echo "archive=$archive" >> "$GITHUB_OUTPUT" @@ -752,7 +538,6 @@ jobs: - resolve-release - binary-release-assets - container-image-release - - faucet-container-image-release - helm-chart-release permissions: {} steps: @@ -762,8 +547,6 @@ jobs: RELEASE_VERSION: ${{ needs.resolve-release.outputs.version }} IMAGE_NAME: ${{ needs.container-image-release.outputs.image-name }} IMAGE_DIGEST: ${{ needs.container-image-release.outputs.image-digest }} - FAUCET_IMAGE_NAME: ${{ needs.faucet-container-image-release.outputs.image-name }} - FAUCET_IMAGE_DIGEST: ${{ needs.faucet-container-image-release.outputs.image-digest }} CHART_DIGEST: ${{ needs.helm-chart-release.outputs.chart-digest }} run: | { @@ -788,15 +571,6 @@ jobs: echo "gh attestation verify \"oci://${IMAGE_NAME}@${IMAGE_DIGEST}\" --repo \"$GITHUB_REPOSITORY\" --signer-workflow \"$GITHUB_REPOSITORY/.github/workflows/release.yml\" --source-ref \"refs/tags/$RELEASE_TAG\" --deny-self-hosted-runners" echo '```' echo - echo "Faucet container verification commands:" - echo - echo '```sh' - echo "docker login ghcr.io" - echo "docker pull \"${FAUCET_IMAGE_NAME}:${RELEASE_TAG}\"" - echo "docker run --rm \"${FAUCET_IMAGE_NAME}:${RELEASE_TAG}\" --version" - echo "gh attestation verify \"oci://${FAUCET_IMAGE_NAME}@${FAUCET_IMAGE_DIGEST}\" --repo \"$GITHUB_REPOSITORY\" --signer-workflow \"$GITHUB_REPOSITORY/.github/workflows/release.yml\" --source-ref \"refs/tags/$RELEASE_TAG\" --deny-self-hosted-runners" - echo '```' - echo echo "Helm chart verification commands:" echo echo '```sh' diff --git a/Tiltfile b/Tiltfile index 8c85d0dd..2fe35f92 100644 --- a/Tiltfile +++ b/Tiltfile @@ -1,7 +1,6 @@ EXPECTED_CONTEXT = 'kind-yacd-dev' NAMESPACE = 'yacd-system' IMAGE = 'ghcr.io/meigma/yacd' -FAUCET_IMAGE = 'ghcr.io/meigma/yacd/faucet' CARDANO_TESTNET_IMAGE = 'ghcr.io/meigma/yacd/cardano-testnet' CARDANO_TOOLS_IMAGE = 'ghcr.io/meigma/yacd/cardano-tools' CHART = 'charts/yacd' @@ -27,12 +26,6 @@ custom_build( deps=['cmd', 'api', 'internal', 'go.mod', 'go.sum', '.ko.yaml', '.dev/ko-build.sh'], ) -local_resource( - name='faucet-image', - cmd='EXPECTED_REF=%s:tilt ./.dev/ko-build-faucet.sh && kind load docker-image --name yacd-dev %s:tilt' % (FAUCET_IMAGE, FAUCET_IMAGE), - deps=['services/faucet', 'go.mod', 'go.sum', '.ko.yaml', '.dev/ko-build-faucet.sh'], -) - # Build the cardano-testnet tools image from local source so the operator # uses a publisher that includes post-release changes db-sync depends on # (notably the genesis hash enrichment added in PR #31). The published @@ -62,8 +55,6 @@ k8s_yaml(helm( 'image.repository=%s' % IMAGE, 'image.tag=tilt', 'image.pullPolicy=IfNotPresent', - 'faucet.image.repository=%s' % FAUCET_IMAGE, - 'faucet.image.tag=tilt', 'cardanoTestnet.image.repository=%s' % CARDANO_TESTNET_IMAGE, 'cardanoTestnet.image.tag=tilt', 'cardanoTools.image.repository=%s' % CARDANO_TOOLS_IMAGE, @@ -76,5 +67,5 @@ k8s_yaml(helm( k8s_resource( workload='yacd-controller-manager', new_name='controller', - resource_deps=['faucet-image', 'cardano-testnet-image', 'cardano-tools-image'], + resource_deps=['cardano-testnet-image', 'cardano-tools-image'], ) diff --git a/api/v1alpha1/cardanonetwork_types.go b/api/v1alpha1/cardanonetwork_types.go index c067a518..baa53718 100644 --- a/api/v1alpha1/cardanonetwork_types.go +++ b/api/v1alpha1/cardanonetwork_types.go @@ -84,8 +84,7 @@ type CardanoNetworkSpec struct { // chainAPI configures network-facing APIs exposed next to the primary node. // Ogmios and Kupo are enabled by default as the first chain API and chain - // index endpoints. The faucet is opt-in because it exposes a spending - // endpoint. + // index endpoints. // +optional ChainAPI *ChainAPISpec `json:"chainAPI,omitempty"` } @@ -303,10 +302,6 @@ type ChainAPISpec struct { // kupo configures the Kupo sidecar and Service. // +optional Kupo *KupoSpec `json:"kupo,omitempty"` - - // faucet configures the faucet sidecar and Service. - // +optional - Faucet *FaucetSpec `json:"faucet,omitempty"` } // OgmiosSpec configures the default Ogmios chain API. @@ -357,50 +352,6 @@ type KupoSpec struct { Resources *corev1.ResourceRequirements `json:"resources,omitempty"` } -// FaucetSpec configures the local development faucet API. -type FaucetSpec struct { - // enabled controls whether the faucet sidecar is deployed. - // +kubebuilder:default=false - // +required - Enabled bool `json:"enabled"` - - // image optionally overrides the faucet image reference. When omitted, the - // controller uses its configured default faucet image. Overrides must use the - // same repository as the controller's configured default faucet image; tag or - // digest may vary. - // +optional - Image *string `json:"image,omitempty"` - - // port is the faucet service port. - // +kubebuilder:validation:Minimum=1 - // +kubebuilder:validation:Maximum=65535 - // +kubebuilder:default=8080 - // +required - Port int32 `json:"port"` - - // defaultSource is the generated cardano-testnet UTxO source used when a - // request does not select one explicitly. - // +kubebuilder:default="utxo1" - // +required - DefaultSource string `json:"defaultSource"` - - // minTopUpLovelace is the minimum exact top-up amount. - // +kubebuilder:validation:Minimum=1 - // +kubebuilder:default=1000000 - // +required - MinTopUpLovelace int64 `json:"minTopUpLovelace"` - - // maxTopUpLovelace is the maximum exact top-up amount. - // +kubebuilder:validation:Minimum=1 - // +kubebuilder:default=10000000000 - // +required - MaxTopUpLovelace int64 `json:"maxTopUpLovelace"` - - // resources configures the faucet container resources. - // +optional - Resources *corev1.ResourceRequirements `json:"resources,omitempty"` -} - // CardanoNetworkStatus defines the observed state of CardanoNetwork. type CardanoNetworkStatus struct { // observedGeneration is the most recent generation observed by the @@ -418,10 +369,6 @@ type CardanoNetworkStatus struct { // +optional Endpoints *CardanoNetworkEndpointsStatus `json:"endpoints,omitempty"` - // faucet publishes faucet-specific runtime details. - // +optional - Faucet *FaucetStatus `json:"faucet,omitempty"` - // sync reports the primary node's chain synchronization status as inferred // from in-cluster sources. // +optional @@ -439,7 +386,6 @@ type CardanoNetworkStatus struct { // - "ArtifactsReady": the network artifact bundle is staged and served over HTTP // - "OgmiosReady": Ogmios is enabled and connected to the primary node // - "KupoReady": Kupo is enabled and synchronized enough to serve its API - // - "FaucetReady": the faucet is enabled and available through its Service // - "Progressing": the resource is being created or updated // - "Degraded": the resource failed to reach or maintain its desired state // @@ -562,24 +508,12 @@ type CardanoNetworkEndpointsStatus struct { // +optional Kupo *ServiceEndpointStatus `json:"kupo,omitempty"` - // faucet is the local development faucet HTTP endpoint. - // +optional - Faucet *ServiceEndpointStatus `json:"faucet,omitempty"` - // artifacts is the cardano-tools serve HTTP endpoint that exposes the // staged network artifact files and manifest.json. // +optional Artifacts *ServiceEndpointStatus `json:"artifacts,omitempty"` } -// FaucetStatus reports faucet-specific runtime details. -type FaucetStatus struct { - // authSecretName is the same-namespace Secret containing the bearer token - // used by mutating faucet requests. - // +optional - AuthSecretName string `json:"authSecretName,omitempty"` -} - // ServiceEndpointStatus reports a cluster-local Service endpoint. type ServiceEndpointStatus struct { // serviceName is the Kubernetes Service name. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index b6cebf2b..e5e32c60 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -821,11 +821,6 @@ func (in *CardanoNetworkEndpointsStatus) DeepCopyInto(out *CardanoNetworkEndpoin *out = new(ServiceEndpointStatus) **out = **in } - if in.Faucet != nil { - in, out := &in.Faucet, &out.Faucet - *out = new(ServiceEndpointStatus) - **out = **in - } if in.Artifacts != nil { in, out := &in.Artifacts, &out.Artifacts *out = new(ServiceEndpointStatus) @@ -949,11 +944,6 @@ func (in *CardanoNetworkStatus) DeepCopyInto(out *CardanoNetworkStatus) { *out = new(CardanoNetworkEndpointsStatus) (*in).DeepCopyInto(*out) } - if in.Faucet != nil { - in, out := &in.Faucet, &out.Faucet - *out = new(FaucetStatus) - **out = **in - } if in.Sync != nil { in, out := &in.Sync, &out.Sync *out = new(CardanoNetworkSyncStatus) @@ -1094,11 +1084,6 @@ func (in *ChainAPISpec) DeepCopyInto(out *ChainAPISpec) { *out = new(KupoSpec) (*in).DeepCopyInto(*out) } - if in.Faucet != nil { - in, out := &in.Faucet, &out.Faucet - *out = new(FaucetSpec) - (*in).DeepCopyInto(*out) - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ChainAPISpec. @@ -1111,46 +1096,6 @@ func (in *ChainAPISpec) DeepCopy() *ChainAPISpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FaucetSpec) DeepCopyInto(out *FaucetSpec) { - *out = *in - if in.Image != nil { - in, out := &in.Image, &out.Image - *out = new(string) - **out = **in - } - if in.Resources != nil { - in, out := &in.Resources, &out.Resources - *out = new(v1.ResourceRequirements) - (*in).DeepCopyInto(*out) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FaucetSpec. -func (in *FaucetSpec) DeepCopy() *FaucetSpec { - if in == nil { - return nil - } - out := new(FaucetSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *FaucetStatus) DeepCopyInto(out *FaucetStatus) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FaucetStatus. -func (in *FaucetStatus) DeepCopy() *FaucetStatus { - if in == nil { - return nil - } - out := new(FaucetStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KupoSpec) DeepCopyInto(out *KupoSpec) { *out = *in diff --git a/charts/yacd/crds/yacd.meigma.io_cardanonetworks.yaml b/charts/yacd/crds/yacd.meigma.io_cardanonetworks.yaml index 45bd7fe6..ed0f737f 100644 --- a/charts/yacd/crds/yacd.meigma.io_cardanonetworks.yaml +++ b/charts/yacd/crds/yacd.meigma.io_cardanonetworks.yaml @@ -43,117 +43,8 @@ spec: description: |- chainAPI configures network-facing APIs exposed next to the primary node. Ogmios and Kupo are enabled by default as the first chain API and chain - index endpoints. The faucet is opt-in because it exposes a spending - endpoint. + index endpoints. properties: - faucet: - description: faucet configures the faucet sidecar and Service. - properties: - defaultSource: - default: utxo1 - description: |- - defaultSource is the generated cardano-testnet UTxO source used when a - request does not select one explicitly. - type: string - enabled: - default: false - description: enabled controls whether the faucet sidecar is - deployed. - type: boolean - image: - description: |- - image optionally overrides the faucet image reference. When omitted, the - controller uses its configured default faucet image. Overrides must use the - same repository as the controller's configured default faucet image; tag or - digest may vary. - type: string - maxTopUpLovelace: - default: 10000000000 - description: maxTopUpLovelace is the maximum exact top-up - amount. - format: int64 - minimum: 1 - type: integer - minTopUpLovelace: - default: 1000000 - description: minTopUpLovelace is the minimum exact top-up - amount. - format: int64 - minimum: 1 - type: integer - port: - default: 8080 - description: port is the faucet service port. - format: int32 - maximum: 65535 - minimum: 1 - type: integer - resources: - description: resources configures the faucet container resources. - properties: - claims: - description: |- - Claims lists the names of resources, defined in spec.resourceClaims, - that are used by this container. - - This field depends on the - DynamicResourceAllocation feature gate. - - This field is immutable. It can only be set for containers. - items: - description: ResourceClaim references one entry in PodSpec.ResourceClaims. - properties: - name: - description: |- - Name must match the name of one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes that resource available - inside a container. - type: string - request: - description: |- - Request is the name chosen for a request in the referenced claim. - If empty, everything from the claim is made available, otherwise - only the result of this request. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Limits describes the maximum amount of compute resources allowed. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: |- - Requests describes the minimum amount of compute resources required. - If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. Requests cannot exceed Limits. - More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ - type: object - type: object - required: - - defaultSource - - enabled - - maxTopUpLovelace - - minTopUpLovelace - - port - type: object kupo: description: kupo configures the Kupo sidecar and Service. properties: @@ -653,7 +544,6 @@ spec: - "ArtifactsReady": the network artifact bundle is staged and served over HTTP - "OgmiosReady": Ogmios is enabled and connected to the primary node - "KupoReady": Kupo is enabled and synchronized enough to serve its API - - "FaucetReady": the faucet is enabled and available through its Service - "Progressing": the resource is being created or updated - "Degraded": the resource failed to reach or maintain its desired state @@ -738,21 +628,6 @@ spec: stable URL shape. type: string type: object - faucet: - description: faucet is the local development faucet HTTP endpoint. - properties: - port: - description: port is the Service port. - format: int32 - type: integer - serviceName: - description: serviceName is the Kubernetes Service name. - type: string - url: - description: url is a convenience URL for protocols with a - stable URL shape. - type: string - type: object kupo: description: kupo is the Kupo chain index HTTP endpoint. properties: @@ -799,15 +674,6 @@ spec: type: string type: object type: object - faucet: - description: faucet publishes faucet-specific runtime details. - properties: - authSecretName: - description: |- - authSecretName is the same-namespace Secret containing the bearer token - used by mutating faucet requests. - type: string - type: object network: description: |- network captures resolved network identity once the controller has diff --git a/charts/yacd/templates/_helpers.tpl b/charts/yacd/templates/_helpers.tpl index 4c12ab6d..2aab302d 100644 --- a/charts/yacd/templates/_helpers.tpl +++ b/charts/yacd/templates/_helpers.tpl @@ -101,15 +101,6 @@ app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end -}} {{- end -}} -{{- define "yacd.faucetImage" -}} -{{- if .Values.faucet.image.digest -}} -{{- printf "%s@%s" .Values.faucet.image.repository .Values.faucet.image.digest -}} -{{- else -}} -{{- $tag := default .Chart.AppVersion .Values.faucet.image.tag -}} -{{- printf "%s:%s" .Values.faucet.image.repository $tag -}} -{{- end -}} -{{- end -}} - {{/* yacd.cardanoTestnetImage renders the optional cardano-testnet image override passed to the manager via --default-cardano-testnet-image. The diff --git a/charts/yacd/templates/controller-deployment.yaml b/charts/yacd/templates/controller-deployment.yaml index f636c9eb..b1115d22 100644 --- a/charts/yacd/templates/controller-deployment.yaml +++ b/charts/yacd/templates/controller-deployment.yaml @@ -63,7 +63,6 @@ spec: {{- end }} - {{ printf "--log-format=%s" .Values.manager.logFormat | quote }} - {{ printf "--log-level=%s" .Values.manager.logLevel | quote }} - - {{ printf "--default-faucet-image=%s" (include "yacd.faucetImage" .) | quote }} {{- with (include "yacd.cardanoTestnetImage" .) }} - {{ printf "--default-cardano-testnet-image=%s" . | quote }} {{- end }} diff --git a/charts/yacd/templates/kyverno-image-policy.yaml b/charts/yacd/templates/kyverno-image-policy.yaml index 5835623a..43462f43 100644 --- a/charts/yacd/templates/kyverno-image-policy.yaml +++ b/charts/yacd/templates/kyverno-image-policy.yaml @@ -12,7 +12,7 @@ metadata: annotations: pod-policies.kyverno.io/autogen-controllers: none policies.kyverno.io/title: Verify YACD Image Attestations - policies.kyverno.io/description: Verify release attestations for YACD manager and faucet images. + policies.kyverno.io/description: Verify release attestations for YACD manager images. policies.kyverno.io/category: Software Supply Chain Security policies.kyverno.io/severity: high policies.kyverno.io/subject: Pod @@ -34,7 +34,7 @@ spec: - imageReferences: {{- $imageReferences := $verification.imageReferences }} {{- if not $imageReferences }} - {{- $imageReferences = list (printf "%s:*" .Values.image.repository) (printf "%s@*" .Values.image.repository) (printf "%s:*" .Values.faucet.image.repository) (printf "%s@*" .Values.faucet.image.repository) }} + {{- $imageReferences = list (printf "%s:*" .Values.image.repository) (printf "%s@*" .Values.image.repository) }} {{- end }} {{- range $imageReferences }} - {{ . | quote }} diff --git a/charts/yacd/values.schema.json b/charts/yacd/values.schema.json index 90d07353..479db17d 100644 --- a/charts/yacd/values.schema.json +++ b/charts/yacd/values.schema.json @@ -29,30 +29,6 @@ } } }, - "faucet": { - "type": "object", - "additionalProperties": false, - "required": ["image"], - "properties": { - "image": { - "type": "object", - "additionalProperties": false, - "required": ["repository", "tag", "digest"], - "properties": { - "repository": { - "type": "string", - "minLength": 1 - }, - "tag": { - "type": "string" - }, - "digest": { - "type": "string" - } - } - } - } - }, "cardanoTestnet": { "type": "object", "additionalProperties": false, diff --git a/charts/yacd/values.yaml b/charts/yacd/values.yaml index e648843a..0b1b9b53 100644 --- a/charts/yacd/values.yaml +++ b/charts/yacd/values.yaml @@ -6,16 +6,9 @@ image: digest: "" pullPolicy: IfNotPresent -faucet: - image: - repository: ghcr.io/meigma/yacd/faucet - tag: "" - digest: "" - # cardanoTestnet.image overrides the cardano-testnet tools image used for -# the create-env init container, the faucet source-address init container, -# and (when CardanoNetwork.spec.node.image is unset) the primary -# cardano-node container. Leave repository empty to let the operator use +# the create-env init container and (when CardanoNetwork.spec.node.image is +# unset) the primary cardano-node container. Leave repository empty to let the operator use # its built-in ghcr.io/meigma/yacd/cardano-testnet:-yacd.N # reference. Override when running pre-release publisher changes that the # published cardano-testnet tag does not yet contain. diff --git a/cli/internal/cli/connect.go b/cli/internal/cli/connect.go index 9aafc39b..3925f2b4 100644 --- a/cli/internal/cli/connect.go +++ b/cli/internal/cli/connect.go @@ -43,9 +43,8 @@ func newConnectCommand(commandContext *commandContext) *cobra.Command { the loopback URLs to .yacd//endpoints.json (or .yacd///endpoints.json when --namespace is set), and hold them open until interrupted (Ctrl-C). Run it in one terminal and your tools in -another. Dropped forwards are re-established automatically. The endpoints file -never contains the faucet token, and its ports are only live while connect is -running.`, +another. Dropped forwards are re-established automatically. The endpoints file's +ports are only live while connect is running.`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { runtimeConfig, err := loadRuntimeConfig(commandContext.viper) @@ -193,9 +192,6 @@ func printConnectStatus(out io.Writer, doc endpointsDocument, path string) error if doc.KupoURL != "" { writer.printf(" %s=%s\n", envKupoURL, doc.KupoURL) } - if doc.FaucetURL != "" { - writer.printf(" %s=%s\n", envFaucetURL, doc.FaucetURL) - } writer.printf("Wrote %s — Ctrl-C to disconnect.\n", path) return writer.err diff --git a/cli/internal/cli/connect_test.go b/cli/internal/cli/connect_test.go index ba0038f4..a9b74116 100644 --- a/cli/internal/cli/connect_test.go +++ b/cli/internal/cli/connect_test.go @@ -29,7 +29,6 @@ func TestWriteEndpointsFile(t *testing.T) { NetworkMagic: &magic, OgmiosURL: "ws://127.0.0.1:40001", KupoURL: "http://127.0.0.1:40002", - FaucetURL: "http://127.0.0.1:40003", } path, err := writeEndpointsFile("devnet", "devnet", doc) @@ -91,7 +90,6 @@ func TestPrintConnectStatus(t *testing.T) { assert.Contains(t, text, "Forwarding devnet (namespace devnet)") assert.Contains(t, text, "YACD_OGMIOS_URL=ws://127.0.0.1:40001") assert.Contains(t, text, "YACD_KUPO_URL=http://127.0.0.1:40002") - assert.NotContains(t, text, "YACD_FAUCET_URL", "an unpublished faucet URL must be omitted") assert.Contains(t, text, ".yacd/devnet/endpoints.json") } @@ -115,7 +113,6 @@ func TestRunConnectWritesFileAndExitsOnCancel(t *testing.T) { session := mocks.NewForwardSession(t) session.EXPECT().LocalPort(int32(1337)).Return(40001, true) session.EXPECT().LocalPort(int32(1442)).Return(40002, true) - session.EXPECT().LocalPort(int32(8080)).Return(40003, true) session.EXPECT().Done().Return(make(chan struct{})) // never drops on its own session.EXPECT().Close().Return(nil) @@ -123,7 +120,6 @@ func TestRunConnectWritesFileAndExitsOnCancel(t *testing.T) { client.EXPECT().GetCardanoNetwork(mock.Anything, "devnet", "devnet").Return(network, nil) client.EXPECT().PrimaryPodName(mock.Anything, "devnet", "devnet").Return("devnet-node-abcde", nil) client.EXPECT().Forward(mock.Anything, "devnet", "devnet-node-abcde", mock.Anything).Return(session, nil) - client.EXPECT().GetSecretValue(mock.Anything, "devnet", "devnet-faucet-auth", faucetAuthTokenKey).Return("faucet-token", nil) ctx, cancel := context.WithCancel(context.Background()) commandContext := &commandContext{out: io.Discard, err: io.Discard} @@ -175,7 +171,6 @@ func TestRunConnectReEstablishesAfterDrop(t *testing.T) { first := mocks.NewForwardSession(t) first.EXPECT().LocalPort(int32(1337)).Return(40001, true) first.EXPECT().LocalPort(int32(1442)).Return(40002, true) - first.EXPECT().LocalPort(int32(8080)).Return(40003, true) first.EXPECT().Done().Return(dropFirst) first.EXPECT().Err().Return(errors.New("connection lost")) first.EXPECT().Close().Return(nil) @@ -183,7 +178,6 @@ func TestRunConnectReEstablishesAfterDrop(t *testing.T) { second := mocks.NewForwardSession(t) second.EXPECT().LocalPort(int32(1337)).Return(50001, true) second.EXPECT().LocalPort(int32(1442)).Return(50002, true) - second.EXPECT().LocalPort(int32(8080)).Return(50003, true) second.EXPECT().Done().Return(make(chan struct{})) // stays up until cancel second.EXPECT().Close().Return(nil) @@ -192,7 +186,6 @@ func TestRunConnectReEstablishesAfterDrop(t *testing.T) { client.EXPECT().PrimaryPodName(mock.Anything, "devnet", "devnet").Return("devnet-node-abcde", nil) client.EXPECT().Forward(mock.Anything, "devnet", "devnet-node-abcde", mock.Anything).Return(first, nil).Once() client.EXPECT().Forward(mock.Anything, "devnet", "devnet-node-abcde", mock.Anything).Return(second, nil).Once() - client.EXPECT().GetSecretValue(mock.Anything, "devnet", "devnet-faucet-auth", faucetAuthTokenKey).Return("faucet-token", nil) ctx, cancel := context.WithCancel(context.Background()) var stderr bytes.Buffer @@ -242,7 +235,6 @@ func TestRunConnectBacksOffThenRecovers(t *testing.T) { first := mocks.NewForwardSession(t) first.EXPECT().LocalPort(int32(1337)).Return(40001, true) first.EXPECT().LocalPort(int32(1442)).Return(40002, true) - first.EXPECT().LocalPort(int32(8080)).Return(40003, true) first.EXPECT().Done().Return(dropFirst) first.EXPECT().Err().Return(errors.New("connection lost")) first.EXPECT().Close().Return(nil) @@ -250,7 +242,6 @@ func TestRunConnectBacksOffThenRecovers(t *testing.T) { recovered := mocks.NewForwardSession(t) recovered.EXPECT().LocalPort(int32(1337)).Return(50001, true) recovered.EXPECT().LocalPort(int32(1442)).Return(50002, true) - recovered.EXPECT().LocalPort(int32(8080)).Return(50003, true) recovered.EXPECT().Done().Return(make(chan struct{})) recovered.EXPECT().Close().Return(nil) @@ -262,7 +253,6 @@ func TestRunConnectBacksOffThenRecovers(t *testing.T) { client.EXPECT().Forward(mock.Anything, "devnet", "devnet-node-abcde", mock.Anything).Return(first, nil).Once() client.EXPECT().Forward(mock.Anything, "devnet", "devnet-node-abcde", mock.Anything).Return(nil, errors.New("dial tcp: connection refused")).Once() client.EXPECT().Forward(mock.Anything, "devnet", "devnet-node-abcde", mock.Anything).Return(recovered, nil).Once() - client.EXPECT().GetSecretValue(mock.Anything, "devnet", "devnet-faucet-auth", faucetAuthTokenKey).Return("faucet-token", nil) ctx, cancel := context.WithCancel(context.Background()) var stderr bytes.Buffer @@ -312,7 +302,6 @@ func TestRunConnectReturnsWhenNetworkDeletedDuringReconnect(t *testing.T) { first := mocks.NewForwardSession(t) first.EXPECT().LocalPort(int32(1337)).Return(40001, true) first.EXPECT().LocalPort(int32(1442)).Return(40002, true) - first.EXPECT().LocalPort(int32(8080)).Return(40003, true) first.EXPECT().Done().Return(dropFirst) first.EXPECT().Err().Return(errors.New("connection lost")) first.EXPECT().Close().Return(nil) @@ -323,7 +312,6 @@ func TestRunConnectReturnsWhenNetworkDeletedDuringReconnect(t *testing.T) { Return(nil, fmt.Errorf("cardanonetwork devnet/devnet %w", kube.ErrNotFound)).Once() client.EXPECT().PrimaryPodName(mock.Anything, "devnet", "devnet").Return("devnet-node-abcde", nil).Once() client.EXPECT().Forward(mock.Anything, "devnet", "devnet-node-abcde", mock.Anything).Return(first, nil).Once() - client.EXPECT().GetSecretValue(mock.Anything, "devnet", "devnet-faucet-auth", faucetAuthTokenKey).Return("faucet-token", nil).Once() commandContext := &commandContext{out: io.Discard, err: io.Discard} diff --git a/cli/internal/cli/devnet.yaml b/cli/internal/cli/devnet.yaml index 2cdd37ef..c3d7ff7f 100644 --- a/cli/internal/cli/devnet.yaml +++ b/cli/internal/cli/devnet.yaml @@ -8,16 +8,9 @@ spec: port: 3001 storage: size: 2Gi - chainAPI: - # Enabling the faucet on a local network makes the controller generate a - # genesis-funded `faucet` wallet, which is the network's funded wallet and - # the default source for `yacd wallet topup`. - faucet: - enabled: true - port: 8080 - defaultSource: utxo1 - minTopUpLovelace: 1000000 - maxTopUpLovelace: 100000000000 + # Ogmios and Kupo are enabled by default. A local network is automatically + # given a genesis-funded `faucet` wallet — the network's funded wallet and + # the default source for `yacd wallet topup`. local: networkMagic: 42 era: conway diff --git a/cli/internal/cli/devnet_test.go b/cli/internal/cli/devnet_test.go index dc505424..179512e7 100644 --- a/cli/internal/cli/devnet_test.go +++ b/cli/internal/cli/devnet_test.go @@ -214,9 +214,8 @@ func TestDevnetRejectsNonPositiveTimeout(t *testing.T) { } // TestDefaultDevnetEnvIsValid guards the embedded default environment against -// drift from examples/local/yacd.yaml: it must parse, be a local network, and -// enable the faucet (which drives the genesis-funded wallet the devnet UX -// promises). +// drift from examples/local/yacd.yaml: it must parse and be a local network +// (which automatically gets the genesis-funded wallet the devnet UX promises). func TestDefaultDevnetEnvIsValid(t *testing.T) { env, err := devconfig.Load(bytes.NewReader(defaultDevnetEnvYAML)) require.NoError(t, err) @@ -224,7 +223,4 @@ func TestDefaultDevnetEnvIsValid(t *testing.T) { assert.Equal(t, yacdv1alpha1.CardanoNetworkModeLocal, env.Spec.Network.Mode) require.NotNil(t, env.Spec.Network.Local) assert.Equal(t, int64(42), env.Spec.Network.Local.NetworkMagic) - require.NotNil(t, env.Spec.Network.ChainAPI) - require.NotNil(t, env.Spec.Network.ChainAPI.Faucet) - assert.True(t, env.Spec.Network.ChainAPI.Faucet.Enabled) } diff --git a/cli/internal/cli/embed.go b/cli/internal/cli/embed.go index dd050e93..a017fe6e 100644 --- a/cli/internal/cli/embed.go +++ b/cli/internal/cli/embed.go @@ -3,9 +3,9 @@ package cli import _ "embed" // defaultDevnetEnvYAML is the developer environment `yacd devnet` applies by -// default: a local network with Ogmios, Kupo, and a faucet, which makes the -// controller generate a genesis-funded wallet. It is a byte copy of -// examples/local/yacd.yaml (go:embed cannot reach outside the package +// default: a local network with Ogmios and Kupo. The controller automatically +// generates a genesis-funded faucet wallet for local networks. It is a byte copy +// of examples/local/yacd.yaml (go:embed cannot reach outside the package // directory); devnet_test.go guards the copy against drift. // //go:embed devnet.yaml diff --git a/cli/internal/cli/envcontract.go b/cli/internal/cli/envcontract.go index b4365c9d..cbca4139 100644 --- a/cli/internal/cli/envcontract.go +++ b/cli/internal/cli/envcontract.go @@ -25,8 +25,6 @@ const ( envNetworkMagic = "YACD_NETWORK_MAGIC" envOgmiosURL = "YACD_OGMIOS_URL" envKupoURL = "YACD_KUPO_URL" - envFaucetURL = "YACD_FAUCET_URL" - envFaucetToken = "YACD_FAUCET_TOKEN" envNodeSocketPath = "CARDANO_NODE_SOCKET_PATH" ) @@ -39,8 +37,8 @@ type chainEndpoint struct { endpoint *yacdv1alpha1.ServiceEndpointStatus } -// chainEndpoints returns the published Ogmios/Kupo/faucet endpoints paired with -// their env keys and short names. node-to-node is excluded: it is a TCP peer +// chainEndpoints returns the published Ogmios/Kupo endpoints paired with their +// env keys and short names. node-to-node is excluded: it is a TCP peer // protocol, not something host or in-pod test tooling speaks. func chainEndpoints(network *yacdv1alpha1.CardanoNetwork) []chainEndpoint { if network.Status.Endpoints == nil { @@ -51,7 +49,6 @@ func chainEndpoints(network *yacdv1alpha1.CardanoNetwork) []chainEndpoint { return []chainEndpoint{ {key: envOgmiosURL, name: "ogmios", endpoint: endpoints.Ogmios}, {key: envKupoURL, name: "kupo", endpoint: endpoints.Kupo}, - {key: envFaucetURL, name: "faucet", endpoint: endpoints.Faucet}, } } @@ -90,9 +87,8 @@ func hostBindings(network *yacdv1alpha1.CardanoNetwork, localPort func(remote in } // hostEnv assembles the YACD_* environment for a host process (run/connect): -// the identity variables, a loopback URL per forwarded chain endpoint, and the -// faucet token when non-empty. -func hostEnv(network *yacdv1alpha1.CardanoNetwork, localPort func(remote int32) (int, bool), faucetToken string) ([]string, error) { +// the identity variables and a loopback URL per forwarded chain endpoint. +func hostEnv(network *yacdv1alpha1.CardanoNetwork, localPort func(remote int32) (int, bool)) ([]string, error) { bindings, err := hostBindings(network, localPort) if err != nil { return nil, err @@ -102,25 +98,19 @@ func hostEnv(network *yacdv1alpha1.CardanoNetwork, localPort func(remote int32) for _, binding := range bindings { env = append(env, binding.key+"="+binding.url) } - if strings.TrimSpace(faucetToken) != "" { - env = append(env, envFaucetToken+"="+faucetToken) - } return env, nil } -// endpointsDocument is the token-free connection info connect writes to +// endpointsDocument is the connection info connect writes to // .yacd//endpoints.json and prints. Field names are stable across -// releases. It deliberately never carries the faucet token: the file is a -// checked-out, tool-readable artifact, and the loopback faucet URL is already -// trust-gate-exempt for yacd topup, which reads the token from the cluster. +// releases. type endpointsDocument struct { Network string `json:"network"` Namespace string `json:"namespace"` NetworkMagic *int64 `json:"networkMagic,omitempty"` OgmiosURL string `json:"ogmiosUrl,omitempty"` KupoURL string `json:"kupoUrl,omitempty"` - FaucetURL string `json:"faucetUrl,omitempty"` } // newEndpointsDocument builds the token-free connect document from the forwarded @@ -141,8 +131,6 @@ func newEndpointsDocument(network *yacdv1alpha1.CardanoNetwork, localPort func(r doc.OgmiosURL = binding.url case "kupo": doc.KupoURL = binding.url - case "faucet": - doc.FaucetURL = binding.url } } @@ -151,9 +139,7 @@ func newEndpointsDocument(network *yacdv1alpha1.CardanoNetwork, localPort func(r // podEnv assembles the YACD_* environment for an in-pod process (exec): the // published ClusterIP URLs verbatim, the network magic, and the node socket -// path. It intentionally omits YACD_FAUCET_TOKEN — a Bearer token injected into -// the exec argv would land in apiserver audit logs and /proc, and in-pod -// tooling does not need it. +// path. func podEnv(network *yacdv1alpha1.CardanoNetwork, socketPath string) []string { env := identityEnv(network) for _, chain := range chainEndpoints(network) { diff --git a/cli/internal/cli/envcontract_test.go b/cli/internal/cli/envcontract_test.go index 38c78d47..9adcf40b 100644 --- a/cli/internal/cli/envcontract_test.go +++ b/cli/internal/cli/envcontract_test.go @@ -61,14 +61,14 @@ func TestHostEnvBuildsLoopbackContract(t *testing.T) { t.Parallel() network := readyNetwork("devnet") - local := map[int32]int{1337: 40001, 1442: 40002, 8080: 40003} + local := map[int32]int{1337: 40001, 1442: 40002} lookup := func(remote int32) (int, bool) { port, ok := local[remote] return port, ok } - env, err := hostEnv(network, lookup, "faucet-token") + env, err := hostEnv(network, lookup) require.NoError(t, err) assert.Equal(t, []string{ "YACD_NETWORK=devnet", @@ -76,16 +76,14 @@ func TestHostEnvBuildsLoopbackContract(t *testing.T) { "YACD_NETWORK_MAGIC=42", "YACD_OGMIOS_URL=ws://127.0.0.1:40001", "YACD_KUPO_URL=http://127.0.0.1:40002", - "YACD_FAUCET_URL=http://127.0.0.1:40003", - "YACD_FAUCET_TOKEN=faucet-token", }, env) } -func TestHostEnvSkipsUnforwardedEndpointsAndEmptyToken(t *testing.T) { +func TestHostEnvSkipsUnforwardedEndpoints(t *testing.T) { t.Parallel() network := readyNetwork("devnet") - // Only Ogmios was forwarded; Kupo/faucet have no local port. + // Only Ogmios was forwarded; Kupo has no local port. lookup := func(remote int32) (int, bool) { if remote == 1337 { return 40001, true @@ -94,7 +92,7 @@ func TestHostEnvSkipsUnforwardedEndpointsAndEmptyToken(t *testing.T) { return 0, false } - env, err := hostEnv(network, lookup, "") + env, err := hostEnv(network, lookup) require.NoError(t, err) assert.Equal(t, []string{ "YACD_NETWORK=devnet", @@ -102,14 +100,13 @@ func TestHostEnvSkipsUnforwardedEndpointsAndEmptyToken(t *testing.T) { "YACD_NETWORK_MAGIC=42", "YACD_OGMIOS_URL=ws://127.0.0.1:40001", }, env) - assert.NotContains(t, env, "YACD_FAUCET_TOKEN=") } func TestNewEndpointsDocumentIsTokenFree(t *testing.T) { t.Parallel() network := readyNetwork("devnet") - local := map[int32]int{1337: 40001, 1442: 40002, 8080: 40003} + local := map[int32]int{1337: 40001, 1442: 40002} lookup := func(remote int32) (int, bool) { port, ok := local[remote] @@ -124,7 +121,6 @@ func TestNewEndpointsDocumentIsTokenFree(t *testing.T) { assert.Equal(t, int64(42), *doc.NetworkMagic) assert.Equal(t, "ws://127.0.0.1:40001", doc.OgmiosURL) assert.Equal(t, "http://127.0.0.1:40002", doc.KupoURL) - assert.Equal(t, "http://127.0.0.1:40003", doc.FaucetURL) // The marshaled document carries no token field of any casing. data, err := json.MarshalIndent(doc, "", " ") @@ -132,7 +128,7 @@ func TestNewEndpointsDocumentIsTokenFree(t *testing.T) { assert.NotContains(t, strings.ToLower(string(data)), "token") } -func TestPodEnvUsesClusterURLsAndOmitsToken(t *testing.T) { +func TestPodEnvUsesClusterURLs(t *testing.T) { t.Parallel() network := readyNetwork("devnet") @@ -144,10 +140,6 @@ func TestPodEnvUsesClusterURLsAndOmitsToken(t *testing.T) { "YACD_NETWORK_MAGIC=42", "YACD_OGMIOS_URL=ws://devnet-ogmios.devnet.svc.cluster.local:1337", "YACD_KUPO_URL=http://devnet-kupo.devnet.svc.cluster.local:1442", - "YACD_FAUCET_URL=http://devnet-faucet.devnet.svc.cluster.local:8080", "CARDANO_NODE_SOCKET_PATH=/ipc/node.socket", }, env) - for _, entry := range env { - assert.NotContains(t, entry, "YACD_FAUCET_TOKEN", "the in-pod contract must never carry the faucet token") - } } diff --git a/cli/internal/cli/exec_test.go b/cli/internal/cli/exec_test.go index 8060d8e2..470d1161 100644 --- a/cli/internal/cli/exec_test.go +++ b/cli/internal/cli/exec_test.go @@ -47,13 +47,9 @@ func TestExecRunsInPodWithArgvOnlyEnv(t *testing.T) { "YACD_NETWORK_MAGIC=42", "YACD_OGMIOS_URL=ws://devnet-ogmios.devnet.svc.cluster.local:1337", "YACD_KUPO_URL=http://devnet-kupo.devnet.svc.cluster.local:1442", - "YACD_FAUCET_URL=http://devnet-faucet.devnet.svc.cluster.local:8080", "CARDANO_NODE_SOCKET_PATH=/ipc/node.socket", "cardano-cli", "query", "tip", }, captured.Command) - for _, arg := range captured.Command { - assert.NotContains(t, arg, "YACD_FAUCET_TOKEN", "the in-pod argv must never carry the faucet token") - } } func TestExecPropagatesRemoteExitCode(t *testing.T) { diff --git a/cli/internal/cli/forward.go b/cli/internal/cli/forward.go index 4ceeca92..a15149f0 100644 --- a/cli/internal/cli/forward.go +++ b/cli/internal/cli/forward.go @@ -10,16 +10,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// faucetAuthTokenKey is the Secret data key under which the in-cluster faucet -// publishes its auth Bearer token. run and connect read it to set -// YACD_FAUCET_TOKEN for host tooling; the wallet funding path does not use the -// faucet HTTP service and so never reads it. -const faucetAuthTokenKey = "token" - // connectedSession is a live host-access session shared by run and connect: the -// chain-API port-forwards, the YACD_* environment a host process consumes (env, -// which carries the faucet token), and the token-free document connect writes -// and prints (endpoints). The caller owns its lifetime and must Close it. +// chain-API port-forwards, the YACD_* environment a host process consumes (env), +// and the document connect writes and prints (endpoints). The caller owns its +// lifetime and must Close it. type connectedSession struct { session kube.ForwardSession env []string @@ -39,9 +33,8 @@ func (c *connectedSession) Err() error { return c.session.Err() } // connectNetwork establishes the shared host-access session for a ready // network: it gates on readiness so callers get a clear "not ready" message // instead of opaque forward errors, resolves the primary Pod, forwards the -// published chain-API endpoints, reads the faucet token when a faucet is -// published, and builds the loopback YACD_* environment. The returned session -// is live; the caller closes it. +// published chain-API endpoints, and builds the loopback YACD_* environment. +// The returned session is live; the caller closes it. func connectNetwork(ctx context.Context, kubeClient kube.Client, namespace string, name string) (*connectedSession, error) { network, err := kubeClient.GetCardanoNetwork(ctx, namespace, name) if err != nil { @@ -56,13 +49,7 @@ func connectNetwork(ctx context.Context, kubeClient kube.Client, namespace strin return nil, err } - faucetToken, err := faucetTokenForHost(ctx, kubeClient, network, namespace, name) - if err != nil { - _ = session.Close() - return nil, err - } - - env, err := hostEnv(network, session.LocalPort, faucetToken) + env, err := hostEnv(network, session.LocalPort) if err != nil { _ = session.Close() return nil, err @@ -155,28 +142,3 @@ func requireReady(network *yacdv1alpha1.CardanoNetwork, namespace string, name s return nil } - -// faucetTokenForHost reads the faucet auth token so YACD_FAUCET_TOKEN can be set -// for host tooling, but only once the faucet is actually usable: it returns an -// empty token (and no error) when the network has no faucet or its FaucetReady -// condition is not fresh-and-True, so run/connect degrade gracefully on a -// not-yet-ready faucet instead of hard-failing. A faucet that reports ready but -// publishes no usable auth Secret is a real error. -func faucetTokenForHost(ctx context.Context, kubeClient kube.Client, network *yacdv1alpha1.CardanoNetwork, namespace string, name string) (string, error) { - if network.Status.Endpoints == nil || network.Status.Endpoints.Faucet == nil { - return "", nil - } - if ready := kube.FreshCondition(network, kube.ConditionFaucetReady); ready == nil || ready.Status != metav1.ConditionTrue { - return "", nil - } - if network.Status.Faucet == nil || strings.TrimSpace(network.Status.Faucet.AuthSecretName) == "" { - return "", fmt.Errorf("cardanonetwork %s/%s publishes a faucet endpoint but no auth Secret", namespace, name) - } - - token, err := kubeClient.GetSecretValue(ctx, namespace, network.Status.Faucet.AuthSecretName, faucetAuthTokenKey) - if err != nil { - return "", err - } - - return strings.TrimSpace(token), nil -} diff --git a/cli/internal/cli/forward_test.go b/cli/internal/cli/forward_test.go index 3249ae12..bb50017e 100644 --- a/cli/internal/cli/forward_test.go +++ b/cli/internal/cli/forward_test.go @@ -21,14 +21,12 @@ func TestConnectNetworkForwardsAndBuildsEnv(t *testing.T) { session := mocks.NewForwardSession(t) session.EXPECT().LocalPort(int32(1337)).Return(40001, true) session.EXPECT().LocalPort(int32(1442)).Return(40002, true) - session.EXPECT().LocalPort(int32(8080)).Return(40003, true) session.EXPECT().Close().Return(nil) client := newKubeMock(t) client.EXPECT().GetCardanoNetwork(mock.Anything, "devnet", "devnet").Return(network, nil) client.EXPECT().PrimaryPodName(mock.Anything, "devnet", "devnet").Return("devnet-node-abcde", nil) client.EXPECT().Forward(mock.Anything, "devnet", "devnet-node-abcde", mock.Anything).Return(session, nil) - client.EXPECT().GetSecretValue(mock.Anything, "devnet", "devnet-faucet-auth", faucetAuthTokenKey).Return("faucet-token", nil) connected, err := connectNetwork(ctx, client, "devnet", "devnet") require.NoError(t, err) @@ -38,47 +36,10 @@ func TestConnectNetworkForwardsAndBuildsEnv(t *testing.T) { "YACD_NETWORK_MAGIC=42", "YACD_OGMIOS_URL=ws://127.0.0.1:40001", "YACD_KUPO_URL=http://127.0.0.1:40002", - "YACD_FAUCET_URL=http://127.0.0.1:40003", - "YACD_FAUCET_TOKEN=faucet-token", }, connected.env) require.NoError(t, connected.Close()) } -func TestConnectNetworkOmitsTokenWhenFaucetNotReady(t *testing.T) { - t.Parallel() - - ctx := context.Background() - network := readyNetwork("devnet") - // The faucet endpoint stays published, but FaucetReady is not True, so the - // token must be omitted rather than read — and no Secret read is attempted. - for i := range network.Status.Conditions { - if network.Status.Conditions[i].Type == "FaucetReady" { - network.Status.Conditions[i].Status = metav1.ConditionFalse - } - } - - session := mocks.NewForwardSession(t) - session.EXPECT().LocalPort(int32(1337)).Return(40001, true) - session.EXPECT().LocalPort(int32(1442)).Return(40002, true) - session.EXPECT().LocalPort(int32(8080)).Return(40003, true) - session.EXPECT().Close().Return(nil) - - client := newKubeMock(t) - client.EXPECT().GetCardanoNetwork(mock.Anything, "devnet", "devnet").Return(network, nil) - client.EXPECT().PrimaryPodName(mock.Anything, "devnet", "devnet").Return("devnet-node-abcde", nil) - client.EXPECT().Forward(mock.Anything, "devnet", "devnet-node-abcde", mock.Anything).Return(session, nil) - // Deliberately no GetSecretValue expectation: reading the token here would - // be an unexpected call and fail the mock's cleanup assertion. - - connected, err := connectNetwork(ctx, client, "devnet", "devnet") - require.NoError(t, err) - assert.Contains(t, connected.env, "YACD_FAUCET_URL=http://127.0.0.1:40003") - for _, entry := range connected.env { - assert.NotContains(t, entry, "YACD_FAUCET_TOKEN") - } - require.NoError(t, connected.Close()) -} - func TestConnectNetworkRejectsNotReady(t *testing.T) { t.Parallel() @@ -128,7 +89,7 @@ func TestForwardSpecsExcludesNodeToNode(t *testing.T) { names = append(names, spec.Name) assert.NotEqual(t, int32(3001), spec.Remote, "node-to-node must not be forwarded") } - assert.ElementsMatch(t, []string{"ogmios", "kupo", "faucet"}, names) + assert.ElementsMatch(t, []string{"ogmios", "kupo"}, names) } func TestRequireReady(t *testing.T) { diff --git a/cli/internal/cli/info.go b/cli/internal/cli/info.go index d85a5899..c41ab16e 100644 --- a/cli/internal/cli/info.go +++ b/cli/internal/cli/info.go @@ -74,7 +74,6 @@ type infoOutput struct { ObservedGeneration int64 `json:"observedGeneration,omitempty"` Network networkOutput `json:"network"` Endpoints endpointsOutput `json:"endpoints"` - Faucet *faucetOutput `json:"faucet,omitempty"` Wallet *walletOutput `json:"wallet,omitempty"` Conditions []conditionOutput `json:"conditions"` } @@ -102,7 +101,6 @@ type endpointsOutput struct { NodeToNode *endpointOutput `json:"nodeToNode,omitempty"` Ogmios *endpointOutput `json:"ogmios,omitempty"` Kupo *endpointOutput `json:"kupo,omitempty"` - Faucet *endpointOutput `json:"faucet,omitempty"` } // endpointOutput projects a single ServiceEndpointStatus. @@ -112,11 +110,6 @@ type endpointOutput struct { URL string `json:"url,omitempty"` } -// faucetOutput projects the optional faucet status sub-resource. -type faucetOutput struct { - AuthSecretName string `json:"authSecretName,omitempty"` -} - // conditionOutput projects a single metav1.Condition with the timestamp // formatted as RFC3339 for JSON stability. type conditionOutput struct { @@ -154,12 +147,6 @@ func newInfo(network *yacdv1alpha1.CardanoNetwork) infoOutput { info.Endpoints.NodeToNode = endpointInfo(network.Status.Endpoints.NodeToNode) info.Endpoints.Ogmios = endpointInfo(network.Status.Endpoints.Ogmios) info.Endpoints.Kupo = endpointInfo(network.Status.Endpoints.Kupo) - info.Endpoints.Faucet = endpointInfo(network.Status.Endpoints.Faucet) - } - if network.Status.Faucet != nil { - info.Faucet = &faucetOutput{ - AuthSecretName: network.Status.Faucet.AuthSecretName, - } } info.Conditions = make([]conditionOutput, 0, len(network.Status.Conditions)) diff --git a/cli/internal/cli/info_print.go b/cli/internal/cli/info_print.go index 36e3d6d5..b16b584f 100644 --- a/cli/internal/cli/info_print.go +++ b/cli/internal/cli/info_print.go @@ -34,7 +34,7 @@ func (i *infoWriter) println(s string) { // printInfo renders an infoOutput to out in the human-readable text format // used when --json is not set. The sections (header, network, conditions, -// endpoints, faucet) are written in a fixed order to keep output stable +// endpoints, wallet) are written in a fixed order to keep output stable // across releases. func printInfo(out io.Writer, info infoOutput) error { w := &infoWriter{w: out} @@ -42,7 +42,6 @@ func printInfo(out io.Writer, info infoOutput) error { printNetworkInfo(w, info.Network) printConditionsInfo(w, info.Conditions) printEndpointsInfo(w, info.Endpoints) - printFaucetInfo(w, info.Faucet) printWalletInfo(w, info.Wallet) return w.err } @@ -109,18 +108,6 @@ func printEndpointsInfo(w *infoWriter, endpoints endpointsOutput) { printEndpointInfo(w, "node-to-node", endpoints.NodeToNode) printEndpointInfo(w, "ogmios", endpoints.Ogmios) printEndpointInfo(w, "kupo", endpoints.Kupo) - printEndpointInfo(w, "faucet", endpoints.Faucet) -} - -// printFaucetInfo writes the Faucet section when the controller has -// published the auth Secret name. It is suppressed entirely otherwise to -// keep non-faucet networks' output compact. -func printFaucetInfo(w *infoWriter, faucet *faucetOutput) { - if faucet == nil || faucet.AuthSecretName == "" { - return - } - w.println("\nFaucet:") - w.printf(" Auth Secret: %s\n", faucet.AuthSecretName) } // printWalletInfo writes the Wallet section when the network has a diff --git a/cli/internal/cli/info_test.go b/cli/internal/cli/info_test.go index cfa95360..8ddac392 100644 --- a/cli/internal/cli/info_test.go +++ b/cli/internal/cli/info_test.go @@ -50,8 +50,6 @@ func TestInfoReadsGlobalKubeEnvironment(t *testing.T) { `"type": "Ready"`, `"url": "ws://devnet-ogmios.env-ns.svc.cluster.local:1337"`, `"url": "http://devnet-kupo.env-ns.svc.cluster.local:1442"`, - `"url": "http://devnet-faucet.env-ns.svc.cluster.local:8080"`, - `"authSecretName": "devnet-faucet-auth"`, } { assert.Contains(t, stdout.String(), want) } diff --git a/cli/internal/cli/init.go b/cli/internal/cli/init.go index c44e42c1..cffa1387 100644 --- a/cli/internal/cli/init.go +++ b/cli/internal/cli/init.go @@ -16,8 +16,8 @@ func newInitCommand(commandContext *commandContext) *cobra.Command { Short: "Print a commented yacd.yaml environment template", Long: `Print a fully-commented developer environment template to stdout. -The active configuration is a ready-to-run local devnet (faucet + funded -wallet); commented blocks document the rest of the API. Redirect it to a file +The active configuration is a ready-to-run local devnet with a genesis-funded +wallet; commented blocks document the rest of the API. Redirect it to a file and apply it: yacd init > yacd.yaml diff --git a/cli/internal/cli/init.yaml b/cli/internal/cli/init.yaml index b2ce3546..7f6c69bd 100644 --- a/cli/internal/cli/init.yaml +++ b/cli/internal/cli/init.yaml @@ -4,10 +4,10 @@ # yacd up NAME -f yacd.yaml # NAME is also the namespace by default # (or let `yacd devnet` manage a local cluster + a default network for you). # -# The active config below is a ready-to-run LOCAL devnet. Enabling the faucet -# gives it a genesis-funded `faucet` wallet you spend from with `yacd wallet -# topup`. Commented blocks show the rest of the API — uncomment a WHOLE block at -# a time (every field shown in a block is required together). +# The active config below is a ready-to-run LOCAL devnet. Every local network +# automatically gets a genesis-funded `faucet` wallet you spend from with `yacd +# wallet topup`. Commented blocks show the rest of the API — uncomment a WHOLE +# block at a time (every field shown in a block is required together). # More: https://github.com/meigma/yacd and docs/host-access.md. apiVersion: yacd.meigma.io/devconfig/v1alpha1 @@ -29,29 +29,18 @@ spec: # requests: {cpu: "1", memory: 2Gi} # limits: {cpu: "2", memory: 4Gi} - # Network-facing chain APIs deployed next to the node. - chainAPI: - # Ogmios (WebSocket bridge) and Kupo (chain indexer) are ENABLED by default. - # Uncomment to pin image/port (keep all three fields together). Kupo needs Ogmios. - # ogmios: - # enabled: true - # image: cardanosolutions/ogmios:v6.14.0 - # port: 1337 - # kupo: - # enabled: true - # image: cardanosolutions/kupo:v2.11.0 - # port: 1442 - - # Faucet (local mode only): makes the controller generate a genesis-funded - # `faucet` wallet that holds the initial supply. It is the network's funded - # wallet and the default source for `yacd wallet topup NAME WALLET LOVELACE`. - faucet: - enabled: true - port: 8080 - defaultSource: utxo1 # generated UTxO source the genesis wallet derives from. - minTopUpLovelace: 1000000 # 1 ADA = 1_000_000 lovelace. - maxTopUpLovelace: 100000000000 - # image: ghcr.io/my/faucet:tag # override the faucet image (else controller default). + # Network-facing chain APIs deployed next to the node. Ogmios (WebSocket + # bridge) and Kupo (chain indexer) are ENABLED by default; uncomment to pin + # image/port (keep all three fields together). Kupo needs Ogmios. + # chainAPI: + # ogmios: + # enabled: true + # image: cardanosolutions/ogmios:v6.14.0 + # port: 1337 + # kupo: + # enabled: true + # image: cardanosolutions/kupo:v2.11.0 + # port: 1442 # ---- LOCAL mode (required when mode: local; remove when mode: public) ---- local: @@ -68,7 +57,7 @@ spec: # ---- PUBLIC mode (alternative to LOCAL) ---------------------------------- # To join a public network: set `mode: public`, delete the `local:` block - # above, and uncomment this. Public mode rejects explicit kupo/faucet + # above, and uncomment this. Public mode rejects explicit kupo # `enabled: true`. Mainnet also needs `bootstrap.mithril` and # `node.storage.size` >= 300Gi. # public: diff --git a/cli/internal/cli/init_test.go b/cli/internal/cli/init_test.go index d75930e5..bb606492 100644 --- a/cli/internal/cli/init_test.go +++ b/cli/internal/cli/init_test.go @@ -15,8 +15,8 @@ import ( // TestInitTemplateLoadsAndValidates guards the embedded init template against // drift from the real schema: its active (uncommented) portion must parse and // validate through the same devconfig.Load `yacd up` uses, and must be the -// batteries-included local network `init` promises (faucet enabled, which drives -// the genesis-funded wallet). +// batteries-included local network `init` promises (a local network +// automatically gets the genesis-funded wallet). func TestInitTemplateLoadsAndValidates(t *testing.T) { t.Parallel() @@ -25,9 +25,6 @@ func TestInitTemplateLoadsAndValidates(t *testing.T) { assert.Equal(t, yacdv1alpha1.CardanoNetworkModeLocal, env.Spec.Network.Mode) require.NotNil(t, env.Spec.Network.Local) - require.NotNil(t, env.Spec.Network.ChainAPI) - require.NotNil(t, env.Spec.Network.ChainAPI.Faucet) - assert.True(t, env.Spec.Network.ChainAPI.Faucet.Enabled) } // TestInitCommandPrintsTemplate proves `yacd init` writes the embedded template diff --git a/cli/internal/cli/install.go b/cli/internal/cli/install.go index 3c7334f8..f8afceb0 100644 --- a/cli/internal/cli/install.go +++ b/cli/internal/cli/install.go @@ -85,9 +85,9 @@ func newInstallCommand(commandContext *commandContext) *cobra.Command { return err } - // Model A: the pinned typed Image/FaucetImage stay, and the user - // overrides ride in Extra, which ToHelmValues deep-merges on top. The - // embedded digest shadows a --set image.tag (the chart renders + // Model A: the pinned typed Image stays, and the user overrides ride + // in Extra, which ToHelmValues deep-merges on top. The embedded + // digest shadows a --set image.tag (the chart renders // repository@digest), so the default install stays digest-pinned for // operational knobs; image.digest/image.repository overrides do still win // through the merge and are an unsupported configuration, not blocked here. diff --git a/cli/internal/cli/install_test.go b/cli/internal/cli/install_test.go index 33f7ceb1..bc170203 100644 --- a/cli/internal/cli/install_test.go +++ b/cli/internal/cli/install_test.go @@ -512,16 +512,14 @@ func TestInstallOverridesPreserveImagePins(t *testing.T) { // Model A invariant: user overrides ride in Extra and never clobber the // typed Default() pins. With a --set override present, the spec still carries - // the digest-pinned manager and faucet images byte-for-byte from Default(). + // the digest-pinned manager image byte-for-byte from Default(). spec := captureSpec(installer) require.NoError(t, run("install", "--set", "replicaCount=2")) wantDefault := operator.Default() assert.Equal(t, wantDefault.Image, spec.Values.Image) - assert.Equal(t, wantDefault.FaucetImage, spec.Values.FaucetImage) assert.NotEmpty(t, spec.Values.Image.Digest) - assert.NotEmpty(t, spec.Values.FaucetImage.Digest) // The override is isolated to Extra. assert.Equal(t, map[string]any{"replicaCount": int64(2)}, spec.Values.Extra) } diff --git a/cli/internal/cli/list.go b/cli/internal/cli/list.go index cd01d506..a5dbb9ec 100644 --- a/cli/internal/cli/list.go +++ b/cli/internal/cli/list.go @@ -97,9 +97,6 @@ type listEndpoints struct { // Kupo is the Kupo HTTP endpoint URL. Kupo string `json:"kupo,omitempty"` - - // Faucet is the faucet HTTP endpoint URL. - Faucet string `json:"faucet,omitempty"` } // summary returns a compact comma-separated list of the published endpoint @@ -115,9 +112,6 @@ func (e listEndpoints) summary() string { if e.Kupo != "" { present = append(present, "kupo") } - if e.Faucet != "" { - present = append(present, "faucet") - } if len(present) == 0 { return "-" } @@ -141,7 +135,6 @@ func newListItem(network *yacdv1alpha1.CardanoNetwork) listItem { NodeToNode: endpointURL(network.Status.Endpoints.NodeToNode), Ogmios: endpointURL(network.Status.Endpoints.Ogmios), Kupo: endpointURL(network.Status.Endpoints.Kupo), - Faucet: endpointURL(network.Status.Endpoints.Faucet), } } diff --git a/cli/internal/cli/run_test.go b/cli/internal/cli/run_test.go index ba359f9b..b31a9657 100644 --- a/cli/internal/cli/run_test.go +++ b/cli/internal/cli/run_test.go @@ -34,7 +34,6 @@ func runMock(t *testing.T, done <-chan struct{}) *mocks.Client { session := mocks.NewForwardSession(t) session.EXPECT().LocalPort(int32(1337)).Return(40001, true) session.EXPECT().LocalPort(int32(1442)).Return(40002, true) - session.EXPECT().LocalPort(int32(8080)).Return(40003, true) session.EXPECT().Done().Return(done) session.EXPECT().Close().Return(nil) @@ -42,7 +41,6 @@ func runMock(t *testing.T, done <-chan struct{}) *mocks.Client { client.EXPECT().GetCardanoNetwork(mock.Anything, "devnet", "devnet").Return(network, nil) client.EXPECT().PrimaryPodName(mock.Anything, "devnet", "devnet").Return("devnet-node-abcde", nil) client.EXPECT().Forward(mock.Anything, "devnet", "devnet-node-abcde", mock.Anything).Return(session, nil) - client.EXPECT().GetSecretValue(mock.Anything, "devnet", "devnet-faucet-auth", faucetAuthTokenKey).Return("faucet-token", nil) return client } @@ -101,7 +99,6 @@ func TestRunReportsDroppedForward(t *testing.T) { session := mocks.NewForwardSession(t) session.EXPECT().LocalPort(int32(1337)).Return(40001, true) session.EXPECT().LocalPort(int32(1442)).Return(40002, true) - session.EXPECT().LocalPort(int32(8080)).Return(40003, true) session.EXPECT().Done().Return(dropped) session.EXPECT().Err().Return(assert.AnError) session.EXPECT().Close().Return(nil) @@ -110,7 +107,6 @@ func TestRunReportsDroppedForward(t *testing.T) { client.EXPECT().GetCardanoNetwork(mock.Anything, "devnet", "devnet").Return(network, nil) client.EXPECT().PrimaryPodName(mock.Anything, "devnet", "devnet").Return("devnet-node-abcde", nil) client.EXPECT().Forward(mock.Anything, "devnet", "devnet-node-abcde", mock.Anything).Return(session, nil) - client.EXPECT().GetSecretValue(mock.Anything, "devnet", "devnet-faucet-auth", faucetAuthTokenKey).Return("faucet-token", nil) // A long sleep would hang without the drop-driven cancellation. _, err := runRoot(t, client, "run", "devnet", "--", "sleep", "30") diff --git a/cli/internal/cli/testhelpers_test.go b/cli/internal/cli/testhelpers_test.go index 24c8375c..d6bf340f 100644 --- a/cli/internal/cli/testhelpers_test.go +++ b/cli/internal/cli/testhelpers_test.go @@ -93,9 +93,9 @@ func newKubeMock(t *testing.T) *mocks.Client { return mocks.NewClient(t) } -// readyNetwork builds a CardanoNetwork in a Ready / FaucetReady state with -// the published Ogmios/Kupo/Faucet endpoints and faucet auth Secret name. -// Tests that need a different shape mutate the returned object. +// readyNetwork builds a CardanoNetwork in a Ready state with the published +// Ogmios/Kupo endpoints. Tests that need a different shape mutate the returned +// object. func readyNetwork(namespace string) *yacdv1alpha1.CardanoNetwork { networkMagic := int64(42) era := yacdv1alpha1.CardanoEraConway @@ -125,14 +125,6 @@ func readyNetwork(namespace string) *yacdv1alpha1.CardanoNetwork { Port: 1442, URL: "http://" + name + "-kupo." + namespace + ".svc.cluster.local:1442", }, - Faucet: &yacdv1alpha1.ServiceEndpointStatus{ - ServiceName: name + "-faucet", - Port: 8080, - URL: "http://" + name + "-faucet." + namespace + ".svc.cluster.local:8080", - }, - }, - Faucet: &yacdv1alpha1.FaucetStatus{ - AuthSecretName: name + "-faucet-auth", }, Conditions: []metav1.Condition{ { @@ -143,14 +135,6 @@ func readyNetwork(namespace string) *yacdv1alpha1.CardanoNetwork { ObservedGeneration: 1, LastTransitionTime: metav1.Now(), }, - { - Type: "FaucetReady", - Status: metav1.ConditionTrue, - Reason: "FaucetReady", - Message: "ready", - ObservedGeneration: 1, - LastTransitionTime: metav1.Now(), - }, }, }, } diff --git a/cli/internal/cli/wallet_test.go b/cli/internal/cli/wallet_test.go index b94d1dff..ef0522fa 100644 --- a/cli/internal/cli/wallet_test.go +++ b/cli/internal/cli/wallet_test.go @@ -806,16 +806,15 @@ func exportSecretWithout(t *testing.T, missingKey string) corev1.Secret { // walletForwardMock wires a mock kube.Client with a ForwardSession for the // funding self-forward: a ready network's primary Pod, a forward mapping the -// published Ogmios/Kupo/faucet container ports to fixed loopback ports, and a -// Close on teardown. The funding path reads loopback URLs but never supervises -// the session, so only LocalPort and Close are exercised. +// published Ogmios/Kupo container ports to fixed loopback ports, and a Close on +// teardown. The funding path reads loopback URLs but never supervises the +// session, so only LocalPort and Close are exercised. func walletForwardMock(t *testing.T) *mocks.Client { t.Helper() session := mocks.NewForwardSession(t) session.EXPECT().LocalPort(int32(1337)).Return(40001, true) session.EXPECT().LocalPort(int32(1442)).Return(40002, true) - session.EXPECT().LocalPort(int32(8080)).Return(40003, true).Maybe() session.EXPECT().Close().Return(nil) client := newKubeMock(t) diff --git a/cli/internal/devconfig/config.go b/cli/internal/devconfig/config.go index 5e4adb99..8c3eb2a2 100644 --- a/cli/internal/devconfig/config.go +++ b/cli/internal/devconfig/config.go @@ -200,15 +200,6 @@ func validateExplicitFields(data []byte, environment Environment) error { []string{"spec", "network", "chainAPI", "kupo", "port"}, ) } - if environment.Spec.Network.ChainAPI != nil && environment.Spec.Network.ChainAPI.Faucet != nil { - requiredPaths = append(requiredPaths, - []string{"spec", "network", "chainAPI", "faucet", "enabled"}, - []string{"spec", "network", "chainAPI", "faucet", "port"}, - []string{"spec", "network", "chainAPI", "faucet", "defaultSource"}, - []string{"spec", "network", "chainAPI", "faucet", "minTopUpLovelace"}, - []string{"spec", "network", "chainAPI", "faucet", "maxTopUpLovelace"}, - ) - } for _, path := range requiredPaths { if !hasPath(document, path...) { diff --git a/cli/internal/devconfig/config_test.go b/cli/internal/devconfig/config_test.go index d244d663..e213234e 100644 --- a/cli/internal/devconfig/config_test.go +++ b/cli/internal/devconfig/config_test.go @@ -141,17 +141,6 @@ func TestLoadRejectsOmittedConcreteCRDDefaults(t *testing.T) { `, wantErr: "spec.network.chainAPI.kupo.image", }, - { - name: "faucet port", - config: validConfig + ` chainAPI: - faucet: - enabled: true - defaultSource: utxo1 - minTopUpLovelace: 1000000 - maxTopUpLovelace: 10000000000 -`, - wantErr: "spec.network.chainAPI.faucet.port", - }, } for _, tc := range tests { @@ -284,18 +273,6 @@ func TestLoadRejectsUnsupportedRuntimeConfigs(t *testing.T) { `, wantErr: "spec.network.chainAPI.kupo.enabled=true is not supported for public networks", }, - { - name: "public faucet explicitly enabled", - config: validPublicPreviewConfig + ` chainAPI: - faucet: - enabled: true - port: 8080 - defaultSource: utxo1 - minTopUpLovelace: 1000000 - maxTopUpLovelace: 10000000000 -`, - wantErr: "spec.network.chainAPI.faucet.enabled=true is not supported for public networks", - }, { name: "kupo without ogmios", config: validConfig + ` chainAPI: @@ -310,22 +287,6 @@ func TestLoadRejectsUnsupportedRuntimeConfigs(t *testing.T) { `, wantErr: "spec.network.chainAPI.kupo.enabled=true requires spec.network.chainAPI.ogmios.enabled=true", }, - { - name: "faucet without kupo", - config: validConfig + ` chainAPI: - kupo: - enabled: false - image: cardanosolutions/kupo:v2.11.0 - port: 1442 - faucet: - enabled: true - port: 8080 - defaultSource: utxo1 - minTopUpLovelace: 1000000 - maxTopUpLovelace: 10000000000 -`, - wantErr: "spec.network.chainAPI.faucet.enabled=true requires spec.network.chainAPI.kupo.enabled=true", - }, { name: "unsupported kupo image", config: validConfig + ` chainAPI: @@ -351,43 +312,6 @@ func TestLoadRejectsUnsupportedRuntimeConfigs(t *testing.T) { config: strings.Replace(validConfig, `version: "11.0.1"`, `version: "10.1.4"`, 1), wantErr: "is not supported with spec.network.node.version", }, - { - name: "invalid faucet source", - config: validConfig + ` chainAPI: - faucet: - enabled: true - port: 8080 - defaultSource: wallet1 - minTopUpLovelace: 1000000 - maxTopUpLovelace: 10000000000 -`, - wantErr: "spec.network.chainAPI.faucet.defaultSource", - }, - { - name: "invalid faucet range", - config: validConfig + ` chainAPI: - faucet: - enabled: true - port: 8080 - defaultSource: utxo1 - minTopUpLovelace: 2000000 - maxTopUpLovelace: 1000000 -`, - wantErr: "spec.network.chainAPI.faucet.minTopUpLovelace", - }, - { - name: "blank faucet image override", - config: validConfig + ` chainAPI: - faucet: - enabled: true - image: " " - port: 8080 - defaultSource: utxo1 - minTopUpLovelace: 1000000 - maxTopUpLovelace: 10000000000 -`, - wantErr: "spec.network.chainAPI.faucet.image", - }, { name: "node port conflicts with default ogmios", config: strings.Replace(validConfig, "port: 3001", "port: 1337", 1), @@ -418,23 +342,6 @@ func TestLoadRejectsUnsupportedRuntimeConfigs(t *testing.T) { } } -func TestLoadAllowsFaucetWithoutImageOverride(t *testing.T) { - t.Parallel() - - environment, err := Load(strings.NewReader(validConfig + ` chainAPI: - faucet: - enabled: true - port: 8080 - defaultSource: utxo1 - minTopUpLovelace: 1000000 - maxTopUpLovelace: 10000000000 -`)) - require.NoError(t, err) - require.NotNil(t, environment.Spec.Network.ChainAPI) - require.NotNil(t, environment.Spec.Network.ChainAPI.Faucet) - assert.Nil(t, environment.Spec.Network.ChainAPI.Faucet.Image) -} - func TestValidateRequiresEnvelope(t *testing.T) { t.Parallel() diff --git a/cli/internal/devconfig/runtime_support.go b/cli/internal/devconfig/runtime_support.go index ec12d835..ee555cee 100644 --- a/cli/internal/devconfig/runtime_support.go +++ b/cli/internal/devconfig/runtime_support.go @@ -17,8 +17,6 @@ const ( defaultKupoImage = "cardanosolutions/kupo:v2.11.0" defaultKupoPort int32 = 1442 - defaultFaucetPort int32 = 8080 - minimumMainnetNodeStorageSize = "300Gi" ) @@ -36,7 +34,6 @@ var supportedOgmiosNodeVersions = map[string][]string{ type chainAPISettings struct { ogmios componentSettings kupo componentSettings - faucet componentSettings } type componentSettings struct { @@ -113,9 +110,6 @@ func validatePublicRuntimeSupport(network yacdv1alpha1.CardanoNetworkSpec) error if publicKupoExplicitlyEnabled(network) { return fmt.Errorf("spec.network.chainAPI.kupo.enabled=true is not supported for public networks") } - if publicFaucetExplicitlyEnabled(network) { - return fmt.Errorf("spec.network.chainAPI.faucet.enabled=true is not supported for public networks") - } return nil } @@ -144,12 +138,8 @@ func resolveChainAPIRuntimeSupport(network yacdv1alpha1.CardanoNetworkSpec) (cha if err := validateKupoRuntimeImage(kupo); err != nil { return chainAPISettings{}, err } - faucet, err := resolveFaucetRuntimeSupport(network, ogmios, kupo) - if err != nil { - return chainAPISettings{}, err - } - return chainAPISettings{ogmios: ogmios, kupo: kupo, faucet: faucet}, nil + return chainAPISettings{ogmios: ogmios, kupo: kupo}, nil } func resolveOgmiosRuntimeSupport(network yacdv1alpha1.CardanoNetworkSpec) (componentSettings, error) { @@ -206,48 +196,6 @@ func resolveKupoRuntimeSupport(network yacdv1alpha1.CardanoNetworkSpec) (compone return settings, true, nil } -func resolveFaucetRuntimeSupport(network yacdv1alpha1.CardanoNetworkSpec, ogmios componentSettings, kupo componentSettings) (componentSettings, error) { - settings := componentSettings{ - enabled: false, - port: defaultFaucetPort, - } - if network.ChainAPI == nil || network.ChainAPI.Faucet == nil { - return settings, nil - } - spec := network.ChainAPI.Faucet - if !spec.Enabled { - return settings, nil - } - if !ogmios.enabled { - return componentSettings{}, fmt.Errorf("spec.network.chainAPI.faucet.enabled=true requires spec.network.chainAPI.ogmios.enabled=true") - } - if !kupo.enabled { - return componentSettings{}, fmt.Errorf("spec.network.chainAPI.faucet.enabled=true requires spec.network.chainAPI.kupo.enabled=true") - } - if spec.Image != nil && strings.TrimSpace(*spec.Image) == "" { - return componentSettings{}, fmt.Errorf("spec.network.chainAPI.faucet.image must not be blank") - } - if spec.Port < 1 || spec.Port > 65535 { - return componentSettings{}, fmt.Errorf("spec.network.chainAPI.faucet.port must be between 1 and 65535") - } - if err := validateFaucetSourceName(spec.DefaultSource); err != nil { - return componentSettings{}, err - } - if spec.MinTopUpLovelace < 1 { - return componentSettings{}, fmt.Errorf("spec.network.chainAPI.faucet.minTopUpLovelace must be greater than 0") - } - if spec.MaxTopUpLovelace < 1 { - return componentSettings{}, fmt.Errorf("spec.network.chainAPI.faucet.maxTopUpLovelace must be greater than 0") - } - if spec.MinTopUpLovelace > spec.MaxTopUpLovelace { - return componentSettings{}, fmt.Errorf("spec.network.chainAPI.faucet.minTopUpLovelace must not exceed maxTopUpLovelace") - } - settings.enabled = true - settings.port = spec.Port - - return settings, nil -} - func validateOgmiosRuntimeCompatibility(nodeVersion string, settings componentSettings) error { if !settings.enabled { return nil @@ -287,7 +235,6 @@ func validateRuntimePortConflicts(nodePort int32, settings chainAPISettings) err }{ {name: "ogmios", settings: settings.ogmios}, {name: "kupo", settings: settings.kupo}, - {name: "faucet", settings: settings.faucet}, } { if !component.settings.enabled { continue @@ -301,32 +248,10 @@ func validateRuntimePortConflicts(nodePort int32, settings chainAPISettings) err return nil } -func validateFaucetSourceName(sourceName string) error { - sourceName = strings.TrimSpace(sourceName) - if !strings.HasPrefix(sourceName, "utxo") || len(sourceName) < len("utxo1") { - return fmt.Errorf("spec.network.chainAPI.faucet.defaultSource must use the utxoN source name format") - } - digits := sourceName[len("utxo"):] - if digits[0] == '0' { - return fmt.Errorf("spec.network.chainAPI.faucet.defaultSource must use the utxoN source name format") - } - for _, char := range digits { - if char < '0' || char > '9' { - return fmt.Errorf("spec.network.chainAPI.faucet.defaultSource must use the utxoN source name format") - } - } - - return nil -} - func publicKupoExplicitlyEnabled(network yacdv1alpha1.CardanoNetworkSpec) bool { return network.ChainAPI != nil && network.ChainAPI.Kupo != nil && network.ChainAPI.Kupo.Enabled } -func publicFaucetExplicitlyEnabled(network yacdv1alpha1.CardanoNetworkSpec) bool { - return network.ChainAPI != nil && network.ChainAPI.Faucet != nil && network.ChainAPI.Faucet.Enabled -} - func ogmiosCompatibilityKey(image string) (string, error) { tag, ok := containerImageTag(image) if !ok { diff --git a/cli/internal/kube/client_envtest_test.go b/cli/internal/kube/client_envtest_test.go index 3722d3e0..cb6ed3bb 100644 --- a/cli/internal/kube/client_envtest_test.go +++ b/cli/internal/kube/client_envtest_test.go @@ -67,7 +67,7 @@ func TestRuntimeClientGetsSecretValue(t *testing.T) { secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: "devnet-faucet-auth", + Name: "devnet-example-secret", Namespace: namespace, }, Data: map[string][]byte{ @@ -76,7 +76,7 @@ func TestRuntimeClientGetsSecretValue(t *testing.T) { } require.NoError(t, apiClient.Create(ctx, secret)) - got, err := kubeClient.GetSecretValue(ctx, namespace, "devnet-faucet-auth", "token") + got, err := kubeClient.GetSecretValue(ctx, namespace, "devnet-example-secret", "token") require.NoError(t, err) assert.Equal(t, "super-secret-token-which-is-long-enough", got) } diff --git a/cli/internal/kube/conditions.go b/cli/internal/kube/conditions.go index 245e1f2d..62a2ef1a 100644 --- a/cli/internal/kube/conditions.go +++ b/cli/internal/kube/conditions.go @@ -18,10 +18,6 @@ const ( // ConditionDegraded indicates the controller stopped reconciling and is // surfacing a terminal-for-now failure. ConditionDegraded ConditionType = "Degraded" - - // ConditionFaucetReady indicates the faucet sidecar is reachable and the - // auth Secret is published. - ConditionFaucetReady ConditionType = "FaucetReady" ) // FreshCondition returns the named status condition only when it observes the diff --git a/cli/internal/lifecycle/manager_test.go b/cli/internal/lifecycle/manager_test.go index b0528647..b5186d25 100644 --- a/cli/internal/lifecycle/manager_test.go +++ b/cli/internal/lifecycle/manager_test.go @@ -34,13 +34,6 @@ spec: port: 3001 storage: size: 2Gi - chainAPI: - faucet: - enabled: true - port: 8080 - defaultSource: utxo1 - minTopUpLovelace: 1000000 - maxTopUpLovelace: 100000000000 local: networkMagic: 42 era: conway diff --git a/cli/internal/operator/ssa/render_test.go b/cli/internal/operator/ssa/render_test.go index a3c0293f..c13e42c3 100644 --- a/cli/internal/operator/ssa/render_test.go +++ b/cli/internal/operator/ssa/render_test.go @@ -56,13 +56,9 @@ func TestRenderDefaultObjectSet(t *testing.T) { } // TestRenderDigestPinnedImages asserts the default render is digest-pinned: the -// manager container image carries the manager digest and the -// --default-faucet-image arg carries the faucet digest. +// manager container image carries the manager digest. func TestRenderDigestPinnedImages(t *testing.T) { - const ( - wantManagerImage = "ghcr.io/meigma/yacd@sha256:5d53ca824dacad39c482dc93edfd2db4a65d5803f43dce5b18b1a7482b0f8e21" - wantFaucetArg = "--default-faucet-image=ghcr.io/meigma/yacd/faucet@sha256:826f8d52f0a4b0f607e2293cf72a8217de27700b5e5f1b35e1af86ef18fd3f66" - ) + const wantManagerImage = "ghcr.io/meigma/yacd@sha256:5d53ca824dacad39c482dc93edfd2db4a65d5803f43dce5b18b1a7482b0f8e21" deployment := findObject(t, renderDefault(t, installNamespace), "Deployment", "yacd-controller-manager") @@ -74,11 +70,6 @@ func TestRenderDigestPinnedImages(t *testing.T) { manager, ok := containers[0].(map[string]any) require.True(t, ok) assert.Equal(t, wantManagerImage, manager["image"], "manager image is digest-pinned") - - args, found, err := unstructured.NestedStringSlice(manager, "args") - require.NoError(t, err) - require.True(t, found, "manager must carry args") - assert.Contains(t, args, wantFaucetArg, "faucet digest is threaded into --default-faucet-image") } // TestRenderPresenceOfCoreObjects double-checks the metrics Service and the diff --git a/cli/internal/operator/values.go b/cli/internal/operator/values.go index 024e52f4..61f91aac 100644 --- a/cli/internal/operator/values.go +++ b/cli/internal/operator/values.go @@ -2,22 +2,15 @@ package operator // Default image references that pin the operator install to the published // release. They reproduce the digest pinning the render script used to inject -// via --set-string image.digest / faucet.image.digest, moved into Go so the -// default install stays digest-pinned and tamper-evident, offline. When cutting -// a new operator release, bump these alongside charts/yacd/Chart.yaml appVersion -// and re-sync the embedded chart copy. +// via --set-string image.digest, moved into Go so the default install stays +// digest-pinned and tamper-evident, offline. When cutting a new operator +// release, bump these alongside charts/yacd/Chart.yaml appVersion. const ( // defaultManagerRepository is the published operator manager image. defaultManagerRepository = "ghcr.io/meigma/yacd" // defaultManagerDigest pins the manager image to the v0.1.1 release digest. defaultManagerDigest = "sha256:5d53ca824dacad39c482dc93edfd2db4a65d5803f43dce5b18b1a7482b0f8e21" - - // defaultFaucetRepository is the published faucet image. - defaultFaucetRepository = "ghcr.io/meigma/yacd/faucet" - - // defaultFaucetDigest pins the faucet image to the v0.1.1 release digest. - defaultFaucetDigest = "sha256:826f8d52f0a4b0f607e2293cf72a8217de27700b5e5f1b35e1af86ef18fd3f66" ) // Image identifies a container image by repository plus an optional tag or @@ -60,10 +53,6 @@ type Values struct { // Image is the operator manager image (chart key "image"). Image Image - // FaucetImage is the faucet image the manager hands to faucet workloads - // via --default-faucet-image (chart key "faucet.image"). - FaucetImage Image - // Replicas overrides the manager Deployment replica count (chart key // "replicaCount"). nil leaves the chart default. Replicas *int @@ -95,9 +84,6 @@ func (v Values) ToHelmValues() map[string]any { if img := v.Image.toHelmValues(); len(img) > 0 { out["image"] = img } - if fimg := v.FaucetImage.toHelmValues(); len(fimg) > 0 { - out["faucet"] = map[string]any{"image": fimg} - } if v.Replicas != nil { out["replicaCount"] = *v.Replicas } @@ -137,21 +123,17 @@ func mergeValues(dst, src map[string]any) { } // Default returns the pinned, offline baseline that reproduces today's install: -// digest-pinned manager and faucet images, leader election on, secure metrics, -// and json/info logging. It mirrors the chart's own defaults (which already set +// the digest-pinned manager image, leader election on, secure metrics, and +// json/info logging. It mirrors the chart's own defaults (which already set // secure metrics, leader election on, and json/info logging) and only adds the -// two release digests, so the rendered Deployment is byte-equivalent to a -// `helm template … --set-string image.digest=… --set-string -// faucet.image.digest=…` render at the chart's default values. +// release digest, so the rendered Deployment is byte-equivalent to a +// `helm template … --set-string image.digest=…` render at the chart's default +// values. func Default() Values { return Values{ Image: Image{ Repository: defaultManagerRepository, Digest: defaultManagerDigest, }, - FaucetImage: Image{ - Repository: defaultFaucetRepository, - Digest: defaultFaucetDigest, - }, } } diff --git a/cli/internal/operator/values_test.go b/cli/internal/operator/values_test.go index bf3bdf99..548bb780 100644 --- a/cli/internal/operator/values_test.go +++ b/cli/internal/operator/values_test.go @@ -12,7 +12,7 @@ import ( ) // TestDefaultPinsReleaseDigests proves the offline default install stays -// digest-pinned to the published release on both the manager and faucet images. +// digest-pinned to the published manager release. func TestDefaultPinsReleaseDigests(t *testing.T) { values := Default() @@ -20,22 +20,12 @@ func TestDefaultPinsReleaseDigests(t *testing.T) { assert.Equal(t, defaultManagerDigest, values.Image.Digest) assert.Empty(t, values.Image.Tag, "digest pin leaves tag empty") - assert.Equal(t, defaultFaucetRepository, values.FaucetImage.Repository) - assert.Equal(t, defaultFaucetDigest, values.FaucetImage.Digest) - assert.Empty(t, values.FaucetImage.Tag, "digest pin leaves tag empty") - helmValues := values.ToHelmValues() image, ok := helmValues["image"].(map[string]any) require.True(t, ok, "image sub-tree must be present") assert.Equal(t, defaultManagerDigest, image["digest"]) _, hasTag := image["tag"] assert.False(t, hasTag, "empty tag is omitted so the chart default applies") - - faucet, ok := helmValues["faucet"].(map[string]any) - require.True(t, ok) - faucetImage, ok := faucet["image"].(map[string]any) - require.True(t, ok) - assert.Equal(t, defaultFaucetDigest, faucetImage["digest"]) } // TestToHelmValuesMergesExtraOverTypedFields proves Extra is deep-merged last so @@ -44,11 +34,7 @@ func TestDefaultPinsReleaseDigests(t *testing.T) { func TestToHelmValuesMergesExtraOverTypedFields(t *testing.T) { replicas := 3 values := Values{ - Image: Image{Repository: "example.com/mgr", Digest: "sha256:aaa"}, - FaucetImage: Image{ - Repository: "example.com/faucet", - Digest: "sha256:bbb", - }, + Image: Image{Repository: "example.com/mgr", Digest: "sha256:aaa"}, Replicas: &replicas, LogFormat: "json", LogLevel: "info", @@ -77,12 +63,6 @@ func TestToHelmValuesMergesExtraOverTypedFields(t *testing.T) { require.True(t, ok) assert.Equal(t, "json", manager["logFormat"]) assert.Equal(t, "info", manager["logLevel"]) - - faucet, ok := got["faucet"].(map[string]any) - require.True(t, ok) - faucetImage, ok := faucet["image"].(map[string]any) - require.True(t, ok) - assert.Equal(t, "sha256:bbb", faucetImage["digest"], "untouched typed fields survive the merge") } // TestZeroValuesFallThroughToChartDefaults proves an unset typed field is diff --git a/cmd/options.go b/cmd/options.go index cf9cc6af..5b801531 100644 --- a/cmd/options.go +++ b/cmd/options.go @@ -65,18 +65,12 @@ type managerOptions struct { // logger. Allowed values are "debug", "info", "warn", and "error". LogLevel string `name:"log-level" enum:"debug,info,warn,error" default:"info" help:"Minimum log level."` - // DefaultFaucetImage is the faucet image used when a CardanoNetwork does - // not provide spec.chainAPI.faucet.image. - //nolint:lll // Kong option tags are intentionally kept on a single struct field line. - DefaultFaucetImage string `name:"default-faucet-image" default:"ghcr.io/meigma/yacd/faucet:dev" help:"Default faucet image for CardanoNetwork faucet sidecars."` - // DefaultCardanoTestnetImage overrides the cardano-testnet container image - // used for the create-env init container, the faucet source-address init - // container, and (when spec.node.image is unset) the primary cardano-node - // container. Empty leaves the built-in + // used for the create-env init container and (when spec.node.image is unset) + // the primary cardano-node container. Empty leaves the built-in // ":-" formula in place. //nolint:lll // Kong option tags are intentionally kept on a single struct field line. - DefaultCardanoTestnetImage string `name:"default-cardano-testnet-image" default:"" help:"Override the cardano-testnet image used for init/source-address containers and the default cardano-node container; empty uses the built-in versioned reference."` + DefaultCardanoTestnetImage string `name:"default-cardano-testnet-image" default:"" help:"Override the cardano-testnet image used for the create-env init container and the default cardano-node container; empty uses the built-in versioned reference."` // DefaultCardanoToolsImage overrides the cardano-tools container image used // for artifact staging (fetch/generate/serve) in both controllers. Empty diff --git a/cmd/options_test.go b/cmd/options_test.go index d348336b..ae9eaef2 100644 --- a/cmd/options_test.go +++ b/cmd/options_test.go @@ -32,7 +32,6 @@ func TestParseManagerOptions(t *testing.T) { assert.False(t, options.EnableHTTP2) assert.Equal(t, "json", options.LogFormat) assert.Equal(t, "info", options.LogLevel) - assert.Equal(t, "ghcr.io/meigma/yacd/faucet:dev", options.DefaultFaucetImage) assert.Empty(t, options.DefaultCardanoTestnetImage) assert.Empty(t, options.DefaultCardanoToolsImage) }, @@ -72,13 +71,6 @@ func TestParseManagerOptions(t *testing.T) { assert.Equal(t, "debug", options.LogLevel) }, }, - { - name: "accepts default faucet image", - args: []string{"--default-faucet-image=example.com/yacd-faucet:test"}, - assert: func(t *testing.T, options managerOptions) { - assert.Equal(t, "example.com/yacd-faucet:test", options.DefaultFaucetImage) - }, - }, { name: "accepts default cardano-testnet image override", args: []string{"--default-cardano-testnet-image=example.com/yacd-cardano-testnet:tilt"}, diff --git a/cmd/setup.go b/cmd/setup.go index f32af569..836c331e 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -42,7 +42,6 @@ func registerControllers(mgr manager.Manager, options managerOptions) error { Client: mgr.GetClient(), Reader: mgr.GetAPIReader(), Scheme: mgr.GetScheme(), - DefaultFaucetImage: options.DefaultFaucetImage, DefaultCardanoTestnetImage: options.DefaultCardanoTestnetImage, DefaultCardanoToolsImage: options.DefaultCardanoToolsImage, }).SetupWithManager(mgr) diff --git a/examples/local/yacd.yaml b/examples/local/yacd.yaml index 2cdd37ef..c3d7ff7f 100644 --- a/examples/local/yacd.yaml +++ b/examples/local/yacd.yaml @@ -8,16 +8,9 @@ spec: port: 3001 storage: size: 2Gi - chainAPI: - # Enabling the faucet on a local network makes the controller generate a - # genesis-funded `faucet` wallet, which is the network's funded wallet and - # the default source for `yacd wallet topup`. - faucet: - enabled: true - port: 8080 - defaultSource: utxo1 - minTopUpLovelace: 1000000 - maxTopUpLovelace: 100000000000 + # Ogmios and Kupo are enabled by default. A local network is automatically + # given a genesis-funded `faucet` wallet — the network's funded wallet and + # the default source for `yacd wallet topup`. local: networkMagic: 42 era: conway diff --git a/internal/cardano/primarypod/primarypod.go b/internal/cardano/primarypod/primarypod.go index 6dda8368..9c82f7b3 100644 --- a/internal/cardano/primarypod/primarypod.go +++ b/internal/cardano/primarypod/primarypod.go @@ -29,12 +29,7 @@ const ( // DefaultKupoPort is the Kupo sidecar port default. DefaultKupoPort int32 = 1442 - // DefaultFaucetPort is the faucet sidecar port default. - DefaultFaucetPort int32 = 8080 - - // DefaultServePort is the cardano-tools serve sidecar port default. It is - // deliberately not 8080 (the faucet default) so the always-on serve - // container can coexist with the faucet on the primary Pod. + // DefaultServePort is the cardano-tools serve sidecar port default. // // 8090 IS registered in PortOwners now that the serve sidecar is exposed // on an owned Service: PortOwners feeds the CardanoDBSync sidecar placement @@ -75,9 +70,6 @@ const ( // PortNameKupo is the Kupo container port name. PortNameKupo = "kupo" - // PortNameFaucet is the faucet container port name. - PortNameFaucet = "faucet" - // PortNameServe is the cardano-tools serve container port name. PortNameServe = "serve" ) @@ -123,9 +115,6 @@ func PortOwners(network *yacdv1alpha1.CardanoNetwork) map[int32]string { if kupoEnabled(network) { ports[kupoPort(network)] = PortNameKupo } - if faucetEnabled(network) { - ports[faucetPort(network)] = PortNameFaucet - } return ports } @@ -150,14 +139,6 @@ func kupoEnabled(network *yacdv1alpha1.CardanoNetwork) bool { return network.Spec.ChainAPI.Kupo.Enabled } -// faucetEnabled returns the effective faucet sidecar enablement for the -// primary Pod. -func faucetEnabled(network *yacdv1alpha1.CardanoNetwork) bool { - return network.Spec.ChainAPI != nil && - network.Spec.ChainAPI.Faucet != nil && - network.Spec.ChainAPI.Faucet.Enabled -} - // ogmiosPort returns the effective Ogmios container port for the primary Pod. func ogmiosPort(network *yacdv1alpha1.CardanoNetwork) int32 { if network.Spec.ChainAPI == nil || network.Spec.ChainAPI.Ogmios == nil || !network.Spec.ChainAPI.Ogmios.Enabled { @@ -181,15 +162,3 @@ func kupoPort(network *yacdv1alpha1.CardanoNetwork) int32 { return network.Spec.ChainAPI.Kupo.Port } - -// faucetPort returns the effective faucet container port for the primary Pod. -func faucetPort(network *yacdv1alpha1.CardanoNetwork) int32 { - if network.Spec.ChainAPI == nil || network.Spec.ChainAPI.Faucet == nil || !network.Spec.ChainAPI.Faucet.Enabled { - return DefaultFaucetPort - } - if network.Spec.ChainAPI.Faucet.Port == 0 { - return DefaultFaucetPort - } - - return network.Spec.ChainAPI.Faucet.Port -} diff --git a/internal/cardano/primarypod/primarypod_test.go b/internal/cardano/primarypod/primarypod_test.go index acfcb817..9921abd0 100644 --- a/internal/cardano/primarypod/primarypod_test.go +++ b/internal/cardano/primarypod/primarypod_test.go @@ -63,14 +63,12 @@ func TestPortOwners(t *testing.T) { network: chainAPINetwork(&yacdv1alpha1.ChainAPISpec{ Ogmios: &yacdv1alpha1.OgmiosSpec{Enabled: true, Port: 1338}, Kupo: &yacdv1alpha1.KupoSpec{Enabled: true, Port: 1443}, - Faucet: &yacdv1alpha1.FaucetSpec{Enabled: true, Port: 8081}, }), want: map[int32]string{ DefaultNodePort: PortNameNodeToNode, DefaultServePort: PortNameServe, 1338: PortNameOgmios, 1443: PortNameKupo, - 8081: PortNameFaucet, }, }, } diff --git a/internal/controller/cardanodbsync/controller_test.go b/internal/controller/cardanodbsync/controller_test.go index bd757d10..3aed9542 100644 --- a/internal/controller/cardanodbsync/controller_test.go +++ b/internal/controller/cardanodbsync/controller_test.go @@ -662,29 +662,6 @@ func TestCardanoDBSyncReconcilerReconcileRejectsPublicMainnetPrimarySidecar(t *t assertMissingObject(t, ctx, reconciler, client.ObjectKey{Namespace: dbSync.Namespace, Name: dbSyncFollowerPVCName(dbSync)}, &corev1.PersistentVolumeClaim{}) } -func TestCardanoDBSyncReconcilerReconcileRejectsPrimarySidecarPortConflict(t *testing.T) { - ctx := context.Background() - dbSync := primarySidecarCardanoDBSync(localCardanoDBSync("dbsync", "ready-network")) - network := readyCardanoNetwork("ready-network") - network.Spec.ChainAPI = &yacdv1alpha1.ChainAPISpec{ - Faucet: &yacdv1alpha1.FaucetSpec{ - Enabled: true, - Port: 8080, - }, - } - reconciler := newTestReconciler(t, dbSync, externalDatabaseSecretFor(dbSync), network) - - result, err := reconciler.Reconcile(ctx, reconcileRequestFor(dbSync)) - - require.NoError(t, err) - assert.Empty(t, result) - assertCondition(t, ctx, reconciler, dbSync, conditionTypeDegraded, metav1.ConditionTrue, conditionReasonUnsupportedSpec) - assertCondition(t, ctx, reconciler, dbSync, conditionTypeReady, metav1.ConditionFalse, conditionReasonUnsupportedSpec) - assertCondition(t, ctx, reconciler, dbSync, conditionTypeSidecarMaterialReady, metav1.ConditionFalse, conditionReasonUnsupportedSpec) - assertPlacementStatus(t, requireDBSync(t, ctx, reconciler, dbSync), yacdv1alpha1.CardanoDBSyncPlacementModePrimarySidecar, false) - assertDegradedMessage(t, ctx, reconciler, dbSync, "db-sync metrics port 8080 conflicts with faucet port in the primary Pod") -} - func TestCardanoDBSyncReconcilerReconcileAllowsPrimarySidecarIncumbentWithNewerPeer(t *testing.T) { ctx := context.Background() dbSync := primarySidecarCardanoDBSync(localCardanoDBSync("dbsync", "shared-network")) diff --git a/internal/controller/cardanonetwork/annotations.go b/internal/controller/cardanonetwork/annotations.go index c7fec524..27107b02 100644 --- a/internal/controller/cardanonetwork/annotations.go +++ b/internal/controller/cardanonetwork/annotations.go @@ -16,11 +16,6 @@ const ( // accepted network fingerprint on owned resources. networkFingerprintAnno = ctrlannotations.NetworkFingerprint - // faucetAuthTokenHashAnno carries the hash of the live faucet auth token - // on the Deployment pod template. Token creation or rotation must roll the - // primary Pod so the mounted token and advertised Secret cannot diverge. - faucetAuthTokenHashAnno = "yacd.meigma.io/faucet-auth-token-hash" - dbSyncSidecarRevisionAnno = ctrlannotations.DBSyncSidecarRevision ) @@ -35,7 +30,6 @@ var cardanoNetworkOwnedAnnotations = []string{ localnetFingerprintAnno, networkFingerprintAnno, ctrlannotations.RequestedStorageClass, - faucetAuthTokenHashAnno, dbSyncSidecarRevisionAnno, } diff --git a/internal/controller/cardanonetwork/builder.go b/internal/controller/cardanonetwork/builder.go index 0ef4ff0e..98fb7886 100644 --- a/internal/controller/cardanonetwork/builder.go +++ b/internal/controller/cardanonetwork/builder.go @@ -23,9 +23,7 @@ type primaryWorkloadResources struct { Service *corev1.Service OgmiosService *corev1.Service KupoService *corev1.Service - FaucetService *corev1.Service ArtifactsService *corev1.Service - FaucetAuthSecret *corev1.Secret FaucetWalletSecret *corev1.Secret FaucetWallet faucetWalletSettings DBSyncAttached bool @@ -34,7 +32,6 @@ type primaryWorkloadResources struct { type chainAPISettings struct { Ogmios ogmiosSettings Kupo kupoSettings - Faucet faucetSettings FaucetWallet faucetWalletSettings } @@ -42,11 +39,11 @@ type chainAPISettings struct { // wallet. The faucet wallet is a local-only, genesis-funded payment key the // controller generates and writes directly (it is not a pod sidecar), so it // carries no image or port — only whether to bootstrap one, the genesis -// funding amount, and the owned Secret name. It is enabled implicitly whenever -// the faucet is enabled on a local network; there is no separate spec opt-in. +// funding amount, and the owned Secret name. It is enabled implicitly on every +// local network; there is no spec opt-in. type faucetWalletSettings struct { // enabled is whether the controller bootstraps the faucet wallet. Requires - // local mode with the faucet enabled. + // local mode. enabled bool // fundingLovelace is the genesis allocation granted to the faucet wallet. fundingLovelace int64 @@ -63,19 +60,12 @@ type primaryWorkloadBuilder struct { // scheme is required to set controller references on owned children. scheme *runtime.Scheme - // defaultFaucetImage is the Reconciler-injected faucet image used when - // the CardanoNetwork spec does not override it. The local dev stack's - // ko-built image flows in through here; see defaults.go for the final - // fallback constant. - defaultFaucetImage string - // defaultCardanoTestnetImage is the Reconciler-injected override for // the cardano-testnet container image. When non-empty it replaces the // computed ":-" reference used by the - // create-env init container, the faucet source-address init - // container, and the default cardano-node container. The local dev - // stack's docker-built image flows in through here so manual testing - // picks up post-release publisher changes that the published + // create-env init container and the default cardano-node container. The + // local dev stack's docker-built image flows in through here so manual + // testing picks up post-release publisher changes that the published // cardano-testnet tag does not yet contain. defaultCardanoTestnetImage string @@ -113,7 +103,7 @@ type primaryWorkloadBuilder struct { // 1. validate the spec into a runtime plan the planner can accept // 2. compute the network plan (fingerprint, paths, invocation args) // 3. build the localnet cardano-testnet create-env init container fragment -// 4. resolve effective sidecar settings (ogmios/kupo/faucet) and run the +// 4. resolve effective sidecar settings (ogmios/kupo) and run the // cross-component validations // 5. assemble the Deployment, PVC, and Services // @@ -141,7 +131,7 @@ func (b primaryWorkloadBuilder) Build(network *yacdv1alpha1.CardanoNetwork) (*pr return nil, err } - deployment, err := b.deployment(network, networkPlan, createEnvInitContainer, chainAPI.Ogmios, chainAPI.Kupo, chainAPI.Faucet, chainAPI.FaucetWallet) + deployment, err := b.deployment(network, networkPlan, createEnvInitContainer, chainAPI.Ogmios, chainAPI.Kupo, chainAPI.FaucetWallet) if err != nil { return nil, err } @@ -167,18 +157,6 @@ func (b primaryWorkloadBuilder) Build(network *yacdv1alpha1.CardanoNetwork) (*pr return nil, err } } - var faucetService *corev1.Service - var faucetAuthSecret *corev1.Secret - if chainAPI.Faucet.enabled { - faucetService, err = b.faucetService(network, chainAPI.Faucet) - if err != nil { - return nil, err - } - faucetAuthSecret, err = b.faucetAuthSecret(network, chainAPI.Faucet) - if err != nil { - return nil, err - } - } var faucetWalletSecret *corev1.Secret if chainAPI.FaucetWallet.enabled { faucetWalletSecret, err = b.faucetWalletSecret(network, chainAPI.FaucetWallet) @@ -204,9 +182,7 @@ func (b primaryWorkloadBuilder) Build(network *yacdv1alpha1.CardanoNetwork) (*pr Service: service, OgmiosService: ogmiosService, KupoService: kupoService, - FaucetService: faucetService, ArtifactsService: artifactsService, - FaucetAuthSecret: faucetAuthSecret, FaucetWalletSecret: faucetWalletSecret, FaucetWallet: chainAPI.FaucetWallet, DBSyncAttached: b.dbSyncAttachment != nil, @@ -239,9 +215,6 @@ func (b primaryWorkloadBuilder) chainAPISettings(network *yacdv1alpha1.CardanoNe if kupoExplicitlyEnabled(network) { return chainAPISettings{}, unsupportedSpec("kupo is not supported for public networks") } - if faucetExplicitlyEnabled(network) { - return chainAPISettings{}, unsupportedSpec("faucet is not supported for public networks") - } } kupo, kupoMentioned, err := resolveKupoSettings(network) if err != nil { @@ -257,10 +230,6 @@ func (b primaryWorkloadBuilder) chainAPISettings(network *yacdv1alpha1.CardanoNe if kupo.enabled && !ogmios.enabled { return chainAPISettings{}, unsupportedSpec("kupo requires ogmios to be enabled") } - faucet, err := b.resolveFaucetSettings(network, ogmios, kupo) - if err != nil { - return chainAPISettings{}, err - } // Skip the ogmios/cardano-node compatibility check when the CR is going // to be rejected as UnsupportedNetworkChange anyway; surface that specific // error instead. @@ -276,31 +245,30 @@ func (b primaryWorkloadBuilder) chainAPISettings(network *yacdv1alpha1.CardanoNe // The serve sidecar runs (and owns its fixed port) only for LOCAL and // CURATED PUBLIC networks; custom-public has no serve port to reserve. serveEnabled := plan.isLocal() || isCuratedPublicProfile(plan) - if err := validatePrimaryWorkloadPorts(network.Spec.Node.Port, ogmios, kupo, faucet, serveEnabled); err != nil { + if err := validatePrimaryWorkloadPorts(network.Spec.Node.Port, ogmios, kupo, serveEnabled); err != nil { return chainAPISettings{}, err } - faucetWallet := resolveFaucetWalletSettings(network, plan, faucet) + faucetWallet := resolveFaucetWalletSettings(network, plan) - return chainAPISettings{Ogmios: ogmios, Kupo: kupo, Faucet: faucet, FaucetWallet: faucetWallet}, nil + return chainAPISettings{Ogmios: ogmios, Kupo: kupo, FaucetWallet: faucetWallet}, nil } // faucetWalletEnabled reports whether the well-known faucet wallet should be -// bootstrapped. It is a spec-only predicate (local mode plus the faucet -// enabled) shared by the Reconciler's pre-build ensure step and the builder so -// both agree on the gate without re-resolving the full faucet settings. +// bootstrapped. It is a spec-only predicate (local mode) shared by the +// Reconciler's pre-build ensure step and the builder so both agree on the gate. +// Every local network gets a genesis-funded faucet wallet; it is the local +// funding source the CLI spends from. func faucetWalletEnabled(network *yacdv1alpha1.CardanoNetwork) bool { return network != nil && - network.Spec.Mode == yacdv1alpha1.CardanoNetworkModeLocal && - faucetExplicitlyEnabled(network) + network.Spec.Mode == yacdv1alpha1.CardanoNetworkModeLocal } -// resolveFaucetWalletSettings resolves the faucet wallet configuration. The -// faucet wallet is enabled implicitly whenever the faucet is enabled on a local -// network: it is the genesis-funded source the local faucet (and the CLI) spend -// from. Returns the disabled zero value otherwise. -func resolveFaucetWalletSettings(network *yacdv1alpha1.CardanoNetwork, plan primaryNetworkPlan, faucet faucetSettings) faucetWalletSettings { - if !plan.isLocal() || !faucet.enabled { +// resolveFaucetWalletSettings resolves the faucet wallet configuration. Every +// local network gets a genesis-funded faucet wallet (the funding source the CLI +// spends from); non-local networks get the disabled zero value. +func resolveFaucetWalletSettings(network *yacdv1alpha1.CardanoNetwork, plan primaryNetworkPlan) faucetWalletSettings { + if !plan.isLocal() { return faucetWalletSettings{} } @@ -466,9 +434,3 @@ func kupoExplicitlyEnabled(network *yacdv1alpha1.CardanoNetwork) bool { network.Spec.ChainAPI.Kupo != nil && network.Spec.ChainAPI.Kupo.Enabled } - -func faucetExplicitlyEnabled(network *yacdv1alpha1.CardanoNetwork) bool { - return network.Spec.ChainAPI != nil && - network.Spec.ChainAPI.Faucet != nil && - network.Spec.ChainAPI.Faucet.Enabled -} diff --git a/internal/controller/cardanonetwork/builder_test.go b/internal/controller/cardanonetwork/builder_test.go index 04d91a68..87179efa 100644 --- a/internal/controller/cardanonetwork/builder_test.go +++ b/internal/controller/cardanonetwork/builder_test.go @@ -214,118 +214,6 @@ func TestPrimaryWorkloadBuilderRejectsUnsupportedInput(t *testing.T) { }, wantErr: "kupo port 1337 conflicts with ogmios port", }, - { - name: "blank faucet image", - mutate: func(network *yacdv1alpha1.CardanoNetwork) { - image := " " - network.Spec.ChainAPI = &yacdv1alpha1.ChainAPISpec{ - Faucet: &yacdv1alpha1.FaucetSpec{ - Enabled: true, - Image: &image, - Port: defaultFaucetPort, - DefaultSource: defaultFaucetSource, - MinTopUpLovelace: defaultFaucetMinLovelace, - MaxTopUpLovelace: defaultFaucetMaxLovelace, - }, - } - }, - wantErr: "faucet image is required", - }, - { - name: "faucet image from different repository", - mutate: func(network *yacdv1alpha1.CardanoNetwork) { - image := "example.com/yacd-faucet:test" - network.Spec.ChainAPI = &yacdv1alpha1.ChainAPISpec{ - Faucet: &yacdv1alpha1.FaucetSpec{ - Enabled: true, - Image: &image, - Port: defaultFaucetPort, - DefaultSource: defaultFaucetSource, - MinTopUpLovelace: defaultFaucetMinLovelace, - MaxTopUpLovelace: defaultFaucetMaxLovelace, - }, - } - }, - wantErr: `faucet image repository must match the configured default faucet image repository "ghcr.io/meigma/yacd/faucet"`, - }, - { - name: "invalid faucet port", - mutate: func(network *yacdv1alpha1.CardanoNetwork) { - network.Spec.ChainAPI = &yacdv1alpha1.ChainAPISpec{ - Faucet: &yacdv1alpha1.FaucetSpec{ - Enabled: true, - Port: 65536, - DefaultSource: defaultFaucetSource, - MinTopUpLovelace: defaultFaucetMinLovelace, - MaxTopUpLovelace: defaultFaucetMaxLovelace, - }, - } - }, - wantErr: "faucet port must be between 1 and 65535", - }, - { - name: "invalid faucet default source", - mutate: func(network *yacdv1alpha1.CardanoNetwork) { - network.Spec.ChainAPI = &yacdv1alpha1.ChainAPISpec{ - Faucet: &yacdv1alpha1.FaucetSpec{ - Enabled: true, - Port: defaultFaucetPort, - DefaultSource: "../utxo1", - MinTopUpLovelace: defaultFaucetMinLovelace, - MaxTopUpLovelace: defaultFaucetMaxLovelace, - }, - } - }, - wantErr: "faucet defaultSource must use the utxoN source name format", - }, - { - name: "faucet min above max", - mutate: func(network *yacdv1alpha1.CardanoNetwork) { - network.Spec.ChainAPI = &yacdv1alpha1.ChainAPISpec{ - Faucet: &yacdv1alpha1.FaucetSpec{ - Enabled: true, - Port: defaultFaucetPort, - DefaultSource: defaultFaucetSource, - MinTopUpLovelace: 3_000_000, - MaxTopUpLovelace: 2_000_000, - }, - } - }, - wantErr: "faucet minTopUpLovelace must not exceed maxTopUpLovelace", - }, - { - name: "explicit faucet with kupo disabled", - mutate: func(network *yacdv1alpha1.CardanoNetwork) { - network.Spec.ChainAPI = &yacdv1alpha1.ChainAPISpec{ - Kupo: &yacdv1alpha1.KupoSpec{ - Enabled: false, - }, - Faucet: &yacdv1alpha1.FaucetSpec{ - Enabled: true, - Port: defaultFaucetPort, - DefaultSource: defaultFaucetSource, - MinTopUpLovelace: defaultFaucetMinLovelace, - MaxTopUpLovelace: defaultFaucetMaxLovelace, - }, - } - }, - wantErr: "faucet requires kupo to be enabled", - }, - { - name: "faucet port conflicts with kupo port", - mutate: func(network *yacdv1alpha1.CardanoNetwork) { - network.Spec.ChainAPI = &yacdv1alpha1.ChainAPISpec{ - Faucet: &yacdv1alpha1.FaucetSpec{ - Enabled: true, - Port: defaultKupoPort, - DefaultSource: defaultFaucetSource, - MinTopUpLovelace: defaultFaucetMinLovelace, - MaxTopUpLovelace: defaultFaucetMaxLovelace, - }, - } - }, - wantErr: "faucet port 1442 conflicts with kupo port", - }, { name: "unsupported ogmios image tag", mutate: func(network *yacdv1alpha1.CardanoNetwork) { @@ -413,14 +301,6 @@ func TestPrimaryWorkloadBuilderRejectsUnsupportedInput(t *testing.T) { }, wantErr: "public mainnet node storage must be at least 300Gi", }, - { - name: "public faucet", - mutate: func(network *yacdv1alpha1.CardanoNetwork) { - *network = *publicPreviewCardanoNetwork("public-faucet") - enableFaucet(network) - }, - wantErr: "faucet is not supported for public networks", - }, { name: "public kupo", mutate: func(network *yacdv1alpha1.CardanoNetwork) { @@ -509,7 +389,6 @@ func TestPrimaryWorkloadBuilderRejectsNilInputAndScheme(t *testing.T) { // singleton primary node workload shape with the default Ogmios sidecar. func TestPrimaryWorkloadBuilderBuildsPrimaryWorkload(t *testing.T) { network := localCardanoNetwork("devnet") - enableFaucet(network) resources, err := newTestPrimaryWorkloadBuilder(t).Build(network) require.NoError(t, err) @@ -518,14 +397,10 @@ func TestPrimaryWorkloadBuilderBuildsPrimaryWorkload(t *testing.T) { service := resources.Service ogmiosService := resources.OgmiosService kupoService := resources.KupoService - faucetService := resources.FaucetService artifactsService := resources.ArtifactsService - faucetAuthSecret := resources.FaucetAuthSecret require.NotNil(t, ogmiosService) require.NotNil(t, kupoService) - require.NotNil(t, faucetService) require.NotNil(t, artifactsService) - require.NotNil(t, faucetAuthSecret) assert.Equal(t, "devnet-node", deployment.Name) assert.Equal(t, "default", deployment.Namespace) @@ -555,18 +430,10 @@ func TestPrimaryWorkloadBuilderBuildsPrimaryWorkload(t *testing.T) { require.NotNil(t, kupoServiceController) assert.Equal(t, "devnet", kupoServiceController.Name) assert.Equal(t, "CardanoNetwork", kupoServiceController.Kind) - faucetServiceController := metav1.GetControllerOf(faucetService) - require.NotNil(t, faucetServiceController) - assert.Equal(t, "devnet", faucetServiceController.Name) - assert.Equal(t, "CardanoNetwork", faucetServiceController.Kind) artifactsServiceController := metav1.GetControllerOf(artifactsService) require.NotNil(t, artifactsServiceController) assert.Equal(t, "devnet", artifactsServiceController.Name) assert.Equal(t, "CardanoNetwork", artifactsServiceController.Kind) - faucetSecretController := metav1.GetControllerOf(faucetAuthSecret) - require.NotNil(t, faucetSecretController) - assert.Equal(t, "devnet", faucetSecretController.Name) - assert.Equal(t, "CardanoNetwork", faucetSecretController.Kind) expectedSelector := map[string]string{ labelAppName: labelPrimaryNodeName, @@ -588,7 +455,7 @@ func TestPrimaryWorkloadBuilderBuildsPrimaryWorkload(t *testing.T) { require.NotNil(t, deployment.Spec.Template.Spec.AutomountServiceAccountToken) assert.False(t, *deployment.Spec.Template.Spec.AutomountServiceAccountToken) - require.Len(t, deployment.Spec.Template.Spec.InitContainers, 4) + require.Len(t, deployment.Spec.Template.Spec.InitContainers, 3) initContainer := deployment.Spec.Template.Spec.InitContainers[0] assert.Equal(t, localnetCreateEnvInitContainerName, initContainer.Name) assert.Equal(t, corev1.TerminationMessagePathDefault, initContainer.TerminationMessagePath) @@ -631,20 +498,7 @@ func TestPrimaryWorkloadBuilderBuildsPrimaryWorkload(t *testing.T) { }, stageInitContainer.VolumeMounts) assertRestrictedContainerSecurityContext(t, stageInitContainer.SecurityContext) - addressInitContainer := deployment.Spec.Template.Spec.InitContainers[3] - assert.Equal(t, faucetSourceAddressInitContainerName, addressInitContainer.Name) - assert.Equal(t, "ghcr.io/meigma/yacd/cardano-testnet:11.0.1-yacd.5", addressInitContainer.Image) - assert.Equal(t, []string{faucetSourceAddressCommand}, addressInitContainer.Command) - addressInitArgs := strings.Join(addressInitContainer.Args, " ") - assert.Contains(t, addressInitArgs, "cardano-cli address build") - assert.Contains(t, addressInitArgs, "--testnet-magic 42") - assert.Contains(t, addressInitArgs, "utxo.vkey") - assert.Contains(t, addressInitArgs, "utxo.addr") - assert.Equal(t, []corev1.VolumeMount{ - {Name: localnetStateVolumeName, MountPath: "/state"}, - }, addressInitContainer.VolumeMounts) - - require.Len(t, deployment.Spec.Template.Spec.Containers, 5) + require.Len(t, deployment.Spec.Template.Spec.Containers, 4) nodeContainer := deployment.Spec.Template.Spec.Containers[0] assert.Equal(t, cardanoNodeContainerName, nodeContainer.Name) assert.Equal(t, "ghcr.io/meigma/yacd/cardano-testnet:11.0.1-yacd.5", nodeContainer.Image) @@ -741,42 +595,9 @@ func TestPrimaryWorkloadBuilderBuildsPrimaryWorkload(t *testing.T) { }, kupoContainer.VolumeMounts) assert.Equal(t, defaultKupoResources(), kupoContainer.Resources) - faucetContainer := deployment.Spec.Template.Spec.Containers[3] - assert.Equal(t, faucetContainerName, faucetContainer.Name) - assert.Equal(t, defaultFaucetImage, faucetContainer.Image) - assert.Empty(t, faucetContainer.Command) - assert.Equal(t, []string{ - "--listen-address", "0.0.0.0:8080", - "--utxo-keys-dir", "/state/env/utxo-keys", - "--default-source", "utxo1", - "--ogmios-url", "ws://127.0.0.1:1337", - "--kupo-url", "http://127.0.0.1:1442", - "--auth-token-file", "/var/run/yacd-faucet/token", - "--allow-remote-listen", - "--min-topup-lovelace", "1000000", - "--max-topup-lovelace", "10000000000", - }, faucetContainer.Args) - assert.Equal(t, []corev1.ContainerPort{ - { - Name: faucetPortName, - ContainerPort: defaultFaucetPort, - Protocol: corev1.ProtocolTCP, - }, - }, faucetContainer.Ports) - require.NotNil(t, faucetContainer.ReadinessProbe) - require.NotNil(t, faucetContainer.StartupProbe) - require.NotNil(t, faucetContainer.LivenessProbe) - assert.Equal(t, faucetReadinessPath, faucetContainer.ReadinessProbe.HTTPGet.Path) - assert.Equal(t, faucetHealthPath, faucetContainer.StartupProbe.HTTPGet.Path) - assert.Equal(t, faucetHealthPath, faucetContainer.LivenessProbe.HTTPGet.Path) - assert.Equal(t, []corev1.VolumeMount{ - {Name: localnetStateVolumeName, MountPath: "/state/env/utxo-keys", SubPath: "env/utxo-keys", ReadOnly: true}, - {Name: faucetAuthVolumeName, MountPath: "/var/run/yacd-faucet", ReadOnly: true}, - }, faucetContainer.VolumeMounts) - // The always-on serve sidecar is appended last; it exposes the staged // served-artifact directory read-only over HTTP on port 8090. - serveContainer := deployment.Spec.Template.Spec.Containers[4] + serveContainer := deployment.Spec.Template.Spec.Containers[3] assert.Equal(t, serveContainerName, serveContainer.Name) assert.Equal(t, toolsimage.Reference("", "11.0.1"), serveContainer.Image) assert.Equal(t, []string{cardanoToolsCommand}, serveContainer.Command) @@ -801,7 +622,7 @@ func TestPrimaryWorkloadBuilderBuildsPrimaryWorkload(t *testing.T) { }, serveContainer.VolumeMounts) assertRestrictedContainerSecurityContext(t, serveContainer.SecurityContext) - require.Len(t, deployment.Spec.Template.Spec.Volumes, 5) + require.Len(t, deployment.Spec.Template.Spec.Volumes, 4) stateVolume := deployment.Spec.Template.Spec.Volumes[0] assert.Equal(t, localnetStateVolumeName, stateVolume.Name) require.NotNil(t, stateVolume.PersistentVolumeClaim) @@ -819,10 +640,6 @@ func TestPrimaryWorkloadBuilderBuildsPrimaryWorkload(t *testing.T) { require.NotNil(t, kupoTmpVolume.EmptyDir) require.NotNil(t, kupoTmpVolume.EmptyDir.SizeLimit) assert.Zero(t, kupoTmpVolume.EmptyDir.SizeLimit.Cmp(resource.MustParse(defaultKupoTmpSizeLimit))) - faucetAuthVolume := deployment.Spec.Template.Spec.Volumes[4] - assert.Equal(t, faucetAuthVolumeName, faucetAuthVolume.Name) - require.NotNil(t, faucetAuthVolume.Secret) - assert.Equal(t, "devnet-faucet-auth", faucetAuthVolume.Secret.SecretName) assert.Equal(t, "devnet-node-state", persistentVolumeClaim.Name) assert.Equal(t, "default", persistentVolumeClaim.Namespace) @@ -873,20 +690,6 @@ func TestPrimaryWorkloadBuilderBuildsPrimaryWorkload(t *testing.T) { }, }, kupoService.Spec.Ports) - assert.Equal(t, "devnet-faucet", faucetService.Name) - assert.Equal(t, "default", faucetService.Namespace) - assert.Equal(t, "yacd", faucetService.Labels[labelAppManagedBy]) - assert.Equal(t, corev1.ServiceTypeClusterIP, faucetService.Spec.Type) - assert.Equal(t, expectedSelector, faucetService.Spec.Selector) - assert.Equal(t, []corev1.ServicePort{ - { - Name: faucetPortName, - Protocol: corev1.ProtocolTCP, - Port: defaultFaucetPort, - TargetPort: intstr.FromString(faucetPortName), - }, - }, faucetService.Spec.Ports) - assert.Equal(t, "devnet-artifacts", artifactsService.Name) assert.Equal(t, "default", artifactsService.Namespace) assert.Equal(t, "yacd", artifactsService.Labels[labelAppManagedBy]) @@ -901,16 +704,10 @@ func TestPrimaryWorkloadBuilderBuildsPrimaryWorkload(t *testing.T) { }, }, artifactsService.Spec.Ports) - assert.Equal(t, "devnet-faucet-auth", faucetAuthSecret.Name) - assert.Equal(t, "default", faucetAuthSecret.Namespace) - assert.Equal(t, "yacd", faucetAuthSecret.Labels[labelAppManagedBy]) - assert.Equal(t, corev1.SecretTypeOpaque, faucetAuthSecret.Type) - assertPodSecurityContext(t, deployment.Spec.Template.Spec.SecurityContext) assertRestrictedContainerSecurityContext(t, nodeContainer.SecurityContext) assertRestrictedContainerSecurityContext(t, ogmiosContainer.SecurityContext) assertRestrictedContainerSecurityContext(t, kupoContainer.SecurityContext) - assertRestrictedContainerSecurityContext(t, faucetContainer.SecurityContext) } func TestPrimaryWorkloadBuilderBuildsPublicWorkload(t *testing.T) { @@ -968,8 +765,7 @@ func TestPrimaryWorkloadBuilderBuildsPublicWorkload(t *testing.T) { } assert.NotNil(t, resources.OgmiosService) assert.Nil(t, resources.KupoService) - assert.Nil(t, resources.FaucetService) - assert.Nil(t, resources.FaucetAuthSecret) + assert.Nil(t, resources.FaucetWalletSecret) // The artifacts Service fronts the always-on serve sidecar. require.NotNil(t, resources.ArtifactsService) assert.Equal(t, tc.name+"-artifacts", resources.ArtifactsService.Name) @@ -1116,30 +912,8 @@ func TestPrimaryWorkloadBuilderBuildsPublicWorkload(t *testing.T) { } } -func TestPrimaryWorkloadBuilderLeavesFaucetDisabledByDefault(t *testing.T) { - network := localCardanoNetwork("faucet-default-disabled") - - resources, err := newTestPrimaryWorkloadBuilder(t).Build(network) - require.NoError(t, err) - - require.Len(t, resources.Deployment.Spec.Template.Spec.Containers, 4) - assert.Equal(t, cardanoNodeContainerName, resources.Deployment.Spec.Template.Spec.Containers[0].Name) - assert.Equal(t, ogmiosContainerName, resources.Deployment.Spec.Template.Spec.Containers[1].Name) - assert.Equal(t, kupoContainerName, resources.Deployment.Spec.Template.Spec.Containers[2].Name) - assert.Equal(t, serveContainerName, resources.Deployment.Spec.Template.Spec.Containers[3].Name) - require.Len(t, resources.Deployment.Spec.Template.Spec.InitContainers, 2) - assert.Equal(t, localnetCreateEnvInitContainerName, resources.Deployment.Spec.Template.Spec.InitContainers[0].Name) - assert.Equal(t, servedArtifactsInitContainerName, resources.Deployment.Spec.Template.Spec.InitContainers[1].Name) - require.Len(t, resources.Deployment.Spec.Template.Spec.Volumes, 4) - assert.NotNil(t, resources.OgmiosService) - assert.NotNil(t, resources.KupoService) - assert.Nil(t, resources.FaucetService) - assert.Nil(t, resources.FaucetAuthSecret) -} - func TestPrimaryWorkloadBuilderUsesSafeNamesAndLabels(t *testing.T) { network := localCardanoNetwork("devnet." + strings.Repeat("a", 80)) - enableFaucet(network) resources, err := newTestPrimaryWorkloadBuilder(t).Build(network) require.NoError(t, err) @@ -1154,12 +928,10 @@ func TestPrimaryWorkloadBuilderUsesSafeNamesAndLabels(t *testing.T) { assert.LessOrEqual(t, len(resources.KupoService.Name), ctrlnames.MaxLabelValueLength) assert.True(t, strings.HasSuffix(resources.KupoService.Name, "-kupo")) assert.NotContains(t, resources.KupoService.Name, ".") - assert.LessOrEqual(t, len(resources.FaucetService.Name), ctrlnames.MaxLabelValueLength) - assert.True(t, strings.HasSuffix(resources.FaucetService.Name, "-faucet")) - assert.NotContains(t, resources.FaucetService.Name, ".") - assert.LessOrEqual(t, len(resources.FaucetAuthSecret.Name), ctrlnames.MaxLabelValueLength) - assert.True(t, strings.HasSuffix(resources.FaucetAuthSecret.Name, "-faucet-auth")) - assert.NotContains(t, resources.FaucetAuthSecret.Name, ".") + require.NotNil(t, resources.FaucetWalletSecret) + assert.LessOrEqual(t, len(resources.FaucetWalletSecret.Name), ctrlnames.MaxLabelValueLength) + assert.True(t, strings.HasSuffix(resources.FaucetWalletSecret.Name, "-wallet-faucet")) + assert.NotContains(t, resources.FaucetWalletSecret.Name, ".") assert.LessOrEqual(t, len(resources.PersistentVolumeClaim.Name), ctrlnames.MaxLabelValueLength) assert.True(t, strings.HasSuffix(resources.PersistentVolumeClaim.Name, "-node-state")) assert.NotContains(t, resources.PersistentVolumeClaim.Name, ".") @@ -1173,11 +945,9 @@ func TestPrimaryWorkloadBuilderUsesSafeNamesAndLabels(t *testing.T) { func TestPrimaryWorkloadBuilderAvoidsSanitizedNameCollisions(t *testing.T) { dottedNetwork := localCardanoNetwork("foo.bar") - enableFaucet(dottedNetwork) dotted, err := newTestPrimaryWorkloadBuilder(t).Build(dottedNetwork) require.NoError(t, err) dashedNetwork := localCardanoNetwork("foo-bar") - enableFaucet(dashedNetwork) dashed, err := newTestPrimaryWorkloadBuilder(t).Build(dashedNetwork) require.NoError(t, err) @@ -1186,8 +956,9 @@ func TestPrimaryWorkloadBuilderAvoidsSanitizedNameCollisions(t *testing.T) { assert.NotEqual(t, dotted.Service.Name, dashed.Service.Name) assert.NotEqual(t, dotted.OgmiosService.Name, dashed.OgmiosService.Name) assert.NotEqual(t, dotted.KupoService.Name, dashed.KupoService.Name) - assert.NotEqual(t, dotted.FaucetService.Name, dashed.FaucetService.Name) - assert.NotEqual(t, dotted.FaucetAuthSecret.Name, dashed.FaucetAuthSecret.Name) + require.NotNil(t, dotted.FaucetWalletSecret) + require.NotNil(t, dashed.FaucetWalletSecret) + assert.NotEqual(t, dotted.FaucetWalletSecret.Name, dashed.FaucetWalletSecret.Name) } func TestPrimaryWorkloadBuilderAppliesNodeOverrides(t *testing.T) { @@ -1237,13 +1008,12 @@ func TestPrimaryWorkloadBuilderAppliesOgmiosOverrides(t *testing.T) { }, }, } - enableFaucet(network) resources, err := newTestPrimaryWorkloadBuilder(t).Build(network) require.NoError(t, err) require.NotNil(t, resources.OgmiosService) - require.Len(t, resources.Deployment.Spec.Template.Spec.Containers, 5) + require.Len(t, resources.Deployment.Spec.Template.Spec.Containers, 4) ogmiosContainer := resources.Deployment.Spec.Template.Spec.Containers[1] assert.Equal(t, "example.com/ogmios:v6.14.0", ogmiosContainer.Image) assert.Contains(t, ogmiosContainer.Args, "1444") @@ -1269,13 +1039,12 @@ func TestPrimaryWorkloadBuilderAppliesKupoPortAndResourceOverrides(t *testing.T) }, }, } - enableFaucet(network) resources, err := newTestPrimaryWorkloadBuilder(t).Build(network) require.NoError(t, err) require.NotNil(t, resources.KupoService) - require.Len(t, resources.Deployment.Spec.Template.Spec.Containers, 5) + require.Len(t, resources.Deployment.Spec.Template.Spec.Containers, 4) kupoContainer := resources.Deployment.Spec.Template.Spec.Containers[2] assert.Equal(t, defaultKupoImage, kupoContainer.Image) assert.Contains(t, kupoContainer.Args, "2442") @@ -1285,42 +1054,6 @@ func TestPrimaryWorkloadBuilderAppliesKupoPortAndResourceOverrides(t *testing.T) assert.Equal(t, intstr.FromString(kupoPortName), resources.KupoService.Spec.Ports[0].TargetPort) } -func TestPrimaryWorkloadBuilderAppliesFaucetOverrides(t *testing.T) { - network := localCardanoNetwork("custom-faucet") - image := "ghcr.io/meigma/yacd/faucet:test" - network.Spec.ChainAPI = &yacdv1alpha1.ChainAPISpec{ - Faucet: &yacdv1alpha1.FaucetSpec{ - Enabled: true, - Image: &image, - Port: 18080, - DefaultSource: "utxo2", - MinTopUpLovelace: 2_000_000, - MaxTopUpLovelace: 5_000_000, - Resources: &corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceMemory: resource.MustParse("128Mi"), - }, - }, - }, - } - - resources, err := newTestPrimaryWorkloadBuilder(t).Build(network) - require.NoError(t, err) - - require.NotNil(t, resources.FaucetService) - require.NotNil(t, resources.FaucetAuthSecret) - require.Len(t, resources.Deployment.Spec.Template.Spec.Containers, 5) - faucetContainer := resources.Deployment.Spec.Template.Spec.Containers[3] - assert.Equal(t, image, faucetContainer.Image) - assert.Contains(t, faucetContainer.Args, "0.0.0.0:18080") - assert.Contains(t, faucetContainer.Args, "utxo2") - assert.Contains(t, faucetContainer.Args, "2000000") - assert.Contains(t, faucetContainer.Args, "5000000") - assert.Equal(t, *network.Spec.ChainAPI.Faucet.Resources, faucetContainer.Resources) - assert.Equal(t, int32(18080), resources.FaucetService.Spec.Ports[0].Port) - assert.Equal(t, intstr.FromString(faucetPortName), resources.FaucetService.Spec.Ports[0].TargetPort) -} - func TestPrimaryWorkloadBuilderDisablesOgmios(t *testing.T) { network := localCardanoNetwork("ogmios-disabled") network.Spec.ChainAPI = &yacdv1alpha1.ChainAPISpec{ @@ -1337,8 +1070,6 @@ func TestPrimaryWorkloadBuilderDisablesOgmios(t *testing.T) { assert.Equal(t, serveContainerName, resources.Deployment.Spec.Template.Spec.Containers[1].Name) assert.Nil(t, resources.OgmiosService) assert.Nil(t, resources.KupoService) - assert.Nil(t, resources.FaucetService) - assert.Nil(t, resources.FaucetAuthSecret) require.Len(t, resources.Deployment.Spec.Template.Spec.Volumes, 2) } @@ -1359,36 +1090,12 @@ func TestPrimaryWorkloadBuilderDisablesKupo(t *testing.T) { assert.Equal(t, serveContainerName, resources.Deployment.Spec.Template.Spec.Containers[2].Name) assert.NotNil(t, resources.OgmiosService) assert.Nil(t, resources.KupoService) - assert.Nil(t, resources.FaucetService) - assert.Nil(t, resources.FaucetAuthSecret) - require.Len(t, resources.Deployment.Spec.Template.Spec.InitContainers, 2) + // Local networks bootstrap the genesis-funded faucet wallet, so the + // init containers are create-env, genesis-funding, and stage. + require.Len(t, resources.Deployment.Spec.Template.Spec.InitContainers, 3) require.Len(t, resources.Deployment.Spec.Template.Spec.Volumes, 2) } -func TestPrimaryWorkloadBuilderDisablesFaucet(t *testing.T) { - network := localCardanoNetwork("faucet-disabled") - network.Spec.ChainAPI = &yacdv1alpha1.ChainAPISpec{ - Faucet: &yacdv1alpha1.FaucetSpec{ - Enabled: false, - }, - } - - resources, err := newTestPrimaryWorkloadBuilder(t).Build(network) - require.NoError(t, err) - - require.Len(t, resources.Deployment.Spec.Template.Spec.Containers, 4) - assert.Equal(t, cardanoNodeContainerName, resources.Deployment.Spec.Template.Spec.Containers[0].Name) - assert.Equal(t, ogmiosContainerName, resources.Deployment.Spec.Template.Spec.Containers[1].Name) - assert.Equal(t, kupoContainerName, resources.Deployment.Spec.Template.Spec.Containers[2].Name) - assert.Equal(t, serveContainerName, resources.Deployment.Spec.Template.Spec.Containers[3].Name) - assert.NotNil(t, resources.OgmiosService) - assert.NotNil(t, resources.KupoService) - assert.Nil(t, resources.FaucetService) - assert.Nil(t, resources.FaucetAuthSecret) - require.Len(t, resources.Deployment.Spec.Template.Spec.InitContainers, 2) - require.Len(t, resources.Deployment.Spec.Template.Spec.Volumes, 4) -} - // testFaucetWalletAddress is a deterministic, valid bech32 testnet address used // to stand in for the faucet wallet address the Reconciler threads into the // builder before Build. It matches the cardano-cli golden in the wallet diff --git a/internal/controller/cardanonetwork/conditions.go b/internal/controller/cardanonetwork/conditions.go index 8ebef1f6..37183e14 100644 --- a/internal/controller/cardanonetwork/conditions.go +++ b/internal/controller/cardanonetwork/conditions.go @@ -27,7 +27,6 @@ const ( conditionTypeNodeProgressing conditionType = "NodeProgressing" conditionTypeOgmiosReady conditionType = "OgmiosReady" conditionTypeKupoReady conditionType = "KupoReady" - conditionTypeFaucetReady conditionType = "FaucetReady" conditionTypeArtifactsReady conditionType = "ArtifactsReady" ) @@ -67,8 +66,6 @@ const ( conditionReasonOgmiosDisabled conditionReason = "OgmiosDisabled" conditionReasonKupoReady conditionReason = "KupoReady" conditionReasonKupoDisabled conditionReason = "KupoDisabled" - conditionReasonFaucetReady conditionReason = "FaucetReady" - conditionReasonFaucetDisabled conditionReason = "FaucetDisabled" conditionReasonArtifactsReady conditionReason = "ArtifactsReady" conditionReasonArtifactsPending conditionReason = "ArtifactsPending" conditionReasonDBSyncAttachmentReady conditionReason = "DBSyncAttachmentReady" @@ -91,8 +88,6 @@ const ( conditionMessageOgmiosDisabled = "Ogmios chain API is disabled" conditionMessageKupoReady = "Kupo sidecar is available through its Service" conditionMessageKupoDisabled = "Kupo chain index API is disabled" - conditionMessageFaucetReady = "Faucet sidecar is available through its Service" - conditionMessageFaucetDisabled = "Faucet API is disabled" conditionMessageArtifactsReady = "Network artifacts are served and available through the artifacts Service" ) @@ -106,8 +101,8 @@ type primaryDeploymentConditionFunc func(metav1.ConditionStatus, conditionReason // single Ready condition. Optional sidecar conditions only contribute when // the corresponding sidecar is enabled, so disabling a sidecar does not // hold Ready back. -func readyCondition(dbSyncAttachmentReady metav1.Condition, nodeReady metav1.Condition, ogmiosReady metav1.Condition, kupoReady metav1.Condition, faucetReady metav1.Condition, artifactsReady metav1.Condition, dbSyncAttached bool, kupoEnabled bool, faucetEnabled bool) metav1.Condition { - dependencies := make([]metav1.Condition, 0, 6) +func readyCondition(dbSyncAttachmentReady metav1.Condition, nodeReady metav1.Condition, ogmiosReady metav1.Condition, kupoReady metav1.Condition, artifactsReady metav1.Condition, dbSyncAttached bool, kupoEnabled bool) metav1.Condition { + dependencies := make([]metav1.Condition, 0, 5) if dbSyncAttached { dependencies = append(dependencies, dbSyncAttachmentReady) } @@ -115,9 +110,6 @@ func readyCondition(dbSyncAttachmentReady metav1.Condition, nodeReady metav1.Con if kupoEnabled { dependencies = append(dependencies, kupoReady) } - if faucetEnabled { - dependencies = append(dependencies, faucetReady) - } dependencies = append(dependencies, artifactsReady) return ctrlstatus.AggregateReady(string(conditionTypeReady), string(conditionReasonReady), conditionMessageReady, dependencies...) @@ -159,11 +151,6 @@ func kupoReadyCondition(status metav1.ConditionStatus, reason conditionReason, m return ctrlstatus.Condition(string(conditionTypeKupoReady), status, string(reason), message) } -// faucetReadyCondition constructs a FaucetReady condition. -func faucetReadyCondition(status metav1.ConditionStatus, reason conditionReason, message string) metav1.Condition { - return ctrlstatus.Condition(string(conditionTypeFaucetReady), status, string(reason), message) -} - // artifactsReadyCondition constructs an ArtifactsReady condition. func artifactsReadyCondition(status metav1.ConditionStatus, reason conditionReason, message string) metav1.Condition { return ctrlstatus.Condition(string(conditionTypeArtifactsReady), status, string(reason), message) diff --git a/internal/controller/cardanonetwork/containers.go b/internal/controller/cardanonetwork/containers.go index a9971d86..8b6c42e4 100644 --- a/internal/controller/cardanonetwork/containers.go +++ b/internal/controller/cardanonetwork/containers.go @@ -1,7 +1,6 @@ package cardanonetwork import ( - "fmt" "path" "strconv" "strings" @@ -12,10 +11,10 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" ) -// Container-construction internals shared by the four primary workload -// containers. Names appear in Deployment containers, Service port targets, -// readiness queries (status.go), and the faucet-revocation patch (apply.go); -// hence the package-private const block instead of inlined string literals. +// Container-construction internals shared by the primary workload containers. +// Names appear in Deployment containers, Service port targets, and readiness +// queries (status.go); hence the package-private const block instead of inlined +// string literals. const ( // cardano-node container. cardanoNodeContainerName = primarypod.CardanoNodeContainerName @@ -41,19 +40,6 @@ const ( kupoWorkDir = "/kupo" kupoTmpDir = "/tmp" - // faucet sidecar. - faucetContainerName = "faucet" - faucetPortName = primarypod.PortNameFaucet - faucetHostAddress = "0.0.0.0" - faucetChainHostAddress = "127.0.0.1" - faucetAuthTokenMountDir = "/var/run/yacd-faucet" - faucetAuthTokenPath = "/var/run/yacd-faucet/token" - faucetUTXOKeysDir = "/state/env/utxo-keys" - faucetOgmiosURLScheme = "ws" - faucetKupoURLScheme = "http" - faucetHealthPath = "/healthz" - faucetReadinessPath = "/readyz" - // serve sidecar. serveContainerName = "serve" servePortName = primarypod.PortNameServe @@ -279,76 +265,9 @@ func (b primaryWorkloadBuilder) kupoContainer(settings kupoSettings, ogmios ogmi return container } -// faucetContainer builds the optional faucet sidecar. It calls into ogmios -// and kupo through the Pod's loopback interface, reads its auth token from -// a Secret-backed projection, and reads UTXO source keys (read-only) from a -// subpath of the localnet state mount populated by the -// faucetSourceAddressInitContainer. -func (b primaryWorkloadBuilder) faucetContainer(settings faucetSettings, ogmios ogmiosSettings, kupo kupoSettings) corev1.Container { - container := corev1.Container{ - Name: faucetContainerName, - Image: settings.image, - ImagePullPolicy: corev1.PullIfNotPresent, - Args: []string{ - "--listen-address", fmt.Sprintf("%s:%d", faucetHostAddress, settings.port), - "--utxo-keys-dir", faucetUTXOKeysDir, - "--default-source", settings.defaultSource, - "--ogmios-url", fmt.Sprintf("%s://%s:%d", faucetOgmiosURLScheme, faucetChainHostAddress, ogmios.port), - "--kupo-url", fmt.Sprintf("%s://%s:%d", faucetKupoURLScheme, faucetChainHostAddress, kupo.port), - "--auth-token-file", settings.authTokenFilePath, - "--allow-remote-listen", - "--min-topup-lovelace", strconv.FormatInt(settings.minTopUpLovelace, 10), - "--max-topup-lovelace", strconv.FormatInt(settings.maxTopUpLovelace, 10), - }, - Ports: []corev1.ContainerPort{ - { - Name: faucetPortName, - ContainerPort: settings.port, - Protocol: corev1.ProtocolTCP, - }, - }, - StartupProbe: faucetHTTPProbe(faucetHealthPath, settings.port, 5, 2, 60), - LivenessProbe: faucetHTTPProbe(faucetHealthPath, settings.port, 10, 5, 12), - ReadinessProbe: faucetHTTPProbe(faucetReadinessPath, settings.port, 5, 2, 3), - VolumeMounts: []corev1.VolumeMount{ - { - Name: localnetStateVolumeName, - MountPath: faucetUTXOKeysDir, - SubPath: "env/utxo-keys", - ReadOnly: true, - }, - { - Name: faucetAuthVolumeName, - MountPath: faucetAuthTokenMountDir, - ReadOnly: true, - }, - }, - SecurityContext: &corev1.SecurityContext{ - AllowPrivilegeEscalation: new(false), - Capabilities: &corev1.Capabilities{ - Drop: []corev1.Capability{"ALL"}, - }, - ReadOnlyRootFilesystem: new(true), - RunAsGroup: new(localnetToolsRunAsID), - RunAsNonRoot: new(true), - RunAsUser: new(localnetToolsRunAsID), - SeccompProfile: &corev1.SeccompProfile{ - Type: corev1.SeccompProfileTypeRuntimeDefault, - }, - }, - TerminationMessagePath: corev1.TerminationMessagePathDefault, - TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, - } - if settings.resources != nil { - container.Resources = *settings.resources.DeepCopy() - } - - return container -} - // serveContainer builds the always-on cardano-tools serve sidecar. It exposes // the flat served-artifact directory (servedArtifactsDir) read-only over HTTP -// on serve port 8090 (8080 collides with the faucet). The directory is +// on serve port 8090. The directory is // populated on the node-state PVC by the served-artifact init container, so // serve mounts that PVC read-only at localnetStateDir. Unlike the producer it // is a regular always-on container (not a RestartPolicy:Always init container), @@ -429,23 +348,6 @@ func serveManifestProbe(port int32, periodSeconds int32, timeoutSeconds int32, f } } -// faucetHTTPProbe builds an HTTP GET probe against the faucet's health or -// readiness endpoint. periodSeconds, timeoutSeconds, and failureThreshold are -// tuned per probe phase (startup vs. liveness vs. readiness). -func faucetHTTPProbe(probePath string, port int32, periodSeconds int32, timeoutSeconds int32, failureThreshold int32) *corev1.Probe { - return &corev1.Probe{ - ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: probePath, - Port: intstr.FromInt(int(port)), - }, - }, - PeriodSeconds: periodSeconds, - TimeoutSeconds: timeoutSeconds, - FailureThreshold: failureThreshold, - } -} - // ogmiosHealthProbe builds an exec-based probe that runs ogmios's own // health-check subcommand. ogmios does not expose a usable HTTP health // endpoint, so an exec probe is the canonical option. diff --git a/internal/controller/cardanonetwork/controller.go b/internal/controller/cardanonetwork/controller.go index 076485c7..4c96f630 100644 --- a/internal/controller/cardanonetwork/controller.go +++ b/internal/controller/cardanonetwork/controller.go @@ -28,7 +28,6 @@ const ( primaryWorkloadReadinessRequeueAfter = 15 * time.Second resourceConflictRequeueAfter = time.Minute - faucetSecretRepairRequeueAfter = 10 * time.Minute disabledChildResourceLogValue = "disabled" ) @@ -45,14 +44,9 @@ type CardanoNetworkReconciler struct { // owned child resources. Scheme *runtime.Scheme - // DefaultFaucetImage is the image used for faucet sidecars when the - // CardanoNetwork spec does not provide an override. - DefaultFaucetImage string - // DefaultCardanoTestnetImage overrides the cardano-testnet container - // image used for the create-env init container, the faucet - // source-address init container, and (when spec.node.image is unset) - // the primary cardano-node container. Empty leaves the built-in + // image used for the create-env init container and (when spec.node.image + // is unset) the primary cardano-node container. Empty leaves the built-in // ":-" formula in place. DefaultCardanoTestnetImage string @@ -79,7 +73,7 @@ type CardanoNetworkReconciler struct { // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch // +kubebuilder:rbac:groups="",resources=persistentvolumeclaims,verbs=get;list;watch;create;update;patch // +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;patch;delete +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;create;patch;delete // +kubebuilder:rbac:groups="",resources=pods,verbs=get;list // Reconcile applies the CardanoNetwork primary workload and publishes runtime status. @@ -120,7 +114,6 @@ func (r *CardanoNetworkReconciler) Reconcile(ctx context.Context, req ctrl.Reque resources, err := (primaryWorkloadBuilder{ scheme: r.Scheme, - defaultFaucetImage: r.DefaultFaucetImage, defaultCardanoTestnetImage: r.DefaultCardanoTestnetImage, defaultCardanoToolsImage: r.DefaultCardanoToolsImage, acceptedIdentity: acceptedIdentity, @@ -134,9 +127,6 @@ func (r *CardanoNetworkReconciler) Reconcile(ctx context.Context, req ctrl.Reque } log.Info("CardanoNetwork primary workload is not supported yet", "error", err) - if revokeErr := r.revokePrimaryFaucetExposure(ctx, network); revokeErr != nil { - return ctrl.Result{}, revokeErr - } dbSyncAttachmentCondition := dbSyncAttachment.statusCondition() if dbSyncAttachment.Attachment != nil { dbSyncAttachmentCondition = dbSyncAttachmentReadyCondition( @@ -145,7 +135,7 @@ func (r *CardanoNetworkReconciler) Reconcile(ctx context.Context, req ctrl.Reque conditionMessagePrimaryWorkloadUnsupported, ) } - if statusErr := r.patchStatusConditionsClearingFaucet(ctx, network, + if statusErr := r.patchStatusConditionsClearingRuntime(ctx, network, primaryNetworkPlan{}, acceptedNetworkIdentity{}, degradedCondition(metav1.ConditionTrue, conditionReasonUnsupportedSpec, err.Error()), @@ -157,7 +147,6 @@ func (r *CardanoNetworkReconciler) Reconcile(ctx context.Context, req ctrl.Reque nodeProgressingCondition(metav1.ConditionFalse, conditionReasonUnsupportedSpec, conditionMessagePrimaryWorkloadUnsupported), ogmiosReadyCondition(metav1.ConditionFalse, conditionReasonUnsupportedSpec, conditionMessagePrimaryWorkloadUnsupported), kupoReadyCondition(metav1.ConditionFalse, conditionReasonUnsupportedSpec, conditionMessagePrimaryWorkloadUnsupported), - faucetReadyCondition(metav1.ConditionFalse, conditionReasonUnsupportedSpec, conditionMessagePrimaryWorkloadUnsupported), artifactsReadyCondition(metav1.ConditionFalse, conditionReasonUnsupportedSpec, conditionMessagePrimaryWorkloadUnsupported), ); statusErr != nil { return ctrl.Result{}, statusErr @@ -178,7 +167,7 @@ func (r *CardanoNetworkReconciler) Reconcile(ctx context.Context, req ctrl.Reque return r.handlePrimaryWorkloadApplyError(ctx, network, resources.NetworkPlan, acceptedIdentity, resources.DBSyncAttached, dbSyncAttachment.statusCondition(), err) } - ready, err := r.patchPrimaryWorkloadAppliedStatus(ctx, network, resources.NetworkPlan, acceptedIdentity, resources.Service, resources.OgmiosService, resources.KupoService, resources.FaucetService, resources.ArtifactsService, resources.FaucetAuthSecret, resources.DBSyncAttached, dbSyncAttachment.statusCondition()) + ready, err := r.patchPrimaryWorkloadAppliedStatus(ctx, network, resources.NetworkPlan, acceptedIdentity, resources.Service, resources.OgmiosService, resources.KupoService, resources.ArtifactsService, resources.DBSyncAttached, dbSyncAttachment.statusCondition()) if err != nil { return ctrl.Result{}, err } @@ -195,18 +184,10 @@ func (r *CardanoNetworkReconciler) Reconcile(ctx context.Context, req ctrl.Reque if resources.KupoService != nil { kupoServiceKey = client.ObjectKeyFromObject(resources.KupoService).String() } - faucetServiceKey := disabledChildResourceLogValue - if resources.FaucetService != nil { - faucetServiceKey = client.ObjectKeyFromObject(resources.FaucetService).String() - } artifactsServiceKey := disabledChildResourceLogValue if resources.ArtifactsService != nil { artifactsServiceKey = client.ObjectKeyFromObject(resources.ArtifactsService).String() } - faucetAuthSecretKey := disabledChildResourceLogValue - if resources.FaucetAuthSecret != nil { - faucetAuthSecretKey = client.ObjectKeyFromObject(resources.FaucetAuthSecret).String() - } resultLog.Info("Applied CardanoNetwork primary workload", "persistentVolumeClaim", client.ObjectKeyFromObject(resources.PersistentVolumeClaim), "persistentVolumeClaimOperation", applyResults.PersistentVolumeClaim, @@ -218,16 +199,12 @@ func (r *CardanoNetworkReconciler) Reconcile(ctx context.Context, req ctrl.Reque "ogmiosServiceOperation", applyResults.OgmiosService, "kupoService", kupoServiceKey, "kupoServiceOperation", applyResults.KupoService, - "faucetService", faucetServiceKey, - "faucetServiceOperation", applyResults.FaucetService, "artifactsService", artifactsServiceKey, "artifactsServiceOperation", applyResults.ArtifactsService, - "faucetAuthSecret", faucetAuthSecretKey, - "faucetAuthSecretOperation", applyResults.FaucetAuthSecret, "faucetWalletSecretOperation", applyResults.FaucetWalletSecret, "networkFingerprint", resources.NetworkPlan.Fingerprint) - if result, requeue := primaryWorkloadRequeueResult(ready, resources.FaucetAuthSecret != nil, resources.OgmiosService != nil); requeue { + if result, requeue := primaryWorkloadRequeueResult(ready, resources.OgmiosService != nil); requeue { return result, nil } @@ -243,7 +220,6 @@ func (r *CardanoNetworkReconciler) now() time.Time { func primaryWorkloadRequeueResult( ready metav1.Condition, - hasFaucetAuthSecret bool, ogmiosEnabled bool, ) (ctrl.Result, bool) { if ready.Status != metav1.ConditionTrue && @@ -254,9 +230,6 @@ func primaryWorkloadRequeueResult( if ogmiosEnabled { return ctrl.Result{RequeueAfter: nodeSyncProbeRequeueAfter}, true } - if hasFaucetAuthSecret { - return ctrl.Result{RequeueAfter: faucetSecretRepairRequeueAfter}, true - } return ctrl.Result{}, false } @@ -271,10 +244,7 @@ type primaryWorkloadApplyResults struct { Service controllerutil.OperationResult OgmiosService controllerutil.OperationResult KupoService controllerutil.OperationResult - FaucetService controllerutil.OperationResult ArtifactsService controllerutil.OperationResult - FaucetAuthSecret controllerutil.OperationResult - FaucetAuthSecretObject *corev1.Secret FaucetWalletSecret controllerutil.OperationResult FaucetWalletSecretObject *corev1.Secret } @@ -288,16 +258,14 @@ func (r primaryWorkloadApplyResults) unchanged() bool { r.Service == controllerutil.OperationResultNone && r.OgmiosService == controllerutil.OperationResultNone && r.KupoService == controllerutil.OperationResultNone && - r.FaucetService == controllerutil.OperationResultNone && r.ArtifactsService == controllerutil.OperationResultNone && - r.FaucetAuthSecret == controllerutil.OperationResultNone && r.FaucetWalletSecret == controllerutil.OperationResultNone } // applyPrimaryWorkloadResources applies the primary workload bundle in -// dependency order: the PVC and faucet auth Secret are created before the -// Deployment so its volumes can mount; the Deployment itself rolls last; -// finally the optional Services are reconciled or deleted to match the spec. +// dependency order: the PVC is created before the Deployment so its volumes can +// mount; the Deployment itself rolls last; finally the optional Services are +// reconciled or deleted to match the spec. func (r *CardanoNetworkReconciler) applyPrimaryWorkloadResources( ctx context.Context, network *yacdv1alpha1.CardanoNetwork, @@ -325,16 +293,6 @@ func (r *CardanoNetworkReconciler) applyPrimaryWorkloadResources( return results, err } - if resources.FaucetAuthSecret != nil { - results.FaucetAuthSecret, results.FaucetAuthSecretObject, err = r.applyPrimaryFaucetAuthSecret(ctx, resources.FaucetAuthSecret) - if err != nil { - return results, err - } - } - - if results.FaucetAuthSecretObject != nil { - setDeploymentFaucetAuthTokenHash(resources.Deployment, results.FaucetAuthSecretObject) - } results.Deployment, err = r.applyPrimaryDeployment(ctx, resources.Deployment) if err != nil { return results, err @@ -355,23 +313,11 @@ func (r *CardanoNetworkReconciler) applyPrimaryWorkloadResources( return results, err } - results.FaucetService, err = r.applyOrDeletePrimaryChainAPIService(ctx, network, resources.FaucetService, r.deletePrimaryFaucetService) - if err != nil { - return results, err - } - results.ArtifactsService, err = r.applyOrDeletePrimaryChainAPIService(ctx, network, resources.ArtifactsService, r.deletePrimaryArtifactsService) if err != nil { return results, err } - if resources.FaucetAuthSecret == nil { - results.FaucetAuthSecret, err = r.deletePrimaryFaucetAuthSecret(ctx, network) - if err != nil { - return results, err - } - } - return results, err } @@ -393,8 +339,8 @@ func (r *CardanoNetworkReconciler) applyOrDeletePrimaryChainAPIService( } // handlePrimaryWorkloadApplyError funnels typed status condition errors -// from any apply step into a Degraded status patch and faucet revocation. -// Untyped errors are returned unchanged so the controller-runtime loop +// from any apply step into a Degraded status patch. Untyped errors are +// returned unchanged so the controller-runtime loop // reschedules with its default backoff. func (r *CardanoNetworkReconciler) handlePrimaryWorkloadApplyError( ctx context.Context, @@ -410,9 +356,6 @@ func (r *CardanoNetworkReconciler) handlePrimaryWorkloadApplyError( return ctrl.Result{}, err } - if revokeErr := r.revokePrimaryFaucetExposure(ctx, network); revokeErr != nil { - return ctrl.Result{}, revokeErr - } // conditionErr.Reason is untyped (it crosses the ctrlstatus boundary as a // plain string); cast to conditionReason once and reuse for the condition // builders below. @@ -427,7 +370,7 @@ func (r *CardanoNetworkReconciler) handlePrimaryWorkloadApplyError( } else if dbSyncAttachmentCondition.Type != "" { dbSyncAttachment = dbSyncAttachmentCondition } - if statusErr := r.patchStatusConditionsClearingFaucet(ctx, network, + if statusErr := r.patchStatusConditionsClearingRuntime(ctx, network, networkPlan, acceptedIdentity, degradedCondition(metav1.ConditionTrue, reason, conditionErr.Message), @@ -439,7 +382,6 @@ func (r *CardanoNetworkReconciler) handlePrimaryWorkloadApplyError( nodeProgressingCondition(metav1.ConditionFalse, reason, conditionErr.Message), ogmiosReadyCondition(metav1.ConditionFalse, reason, conditionErr.Message), kupoReadyCondition(metav1.ConditionFalse, reason, conditionErr.Message), - faucetReadyCondition(metav1.ConditionFalse, reason, conditionErr.Message), artifactsReadyCondition(metav1.ConditionFalse, reason, conditionErr.Message), ); statusErr != nil { return ctrl.Result{}, statusErr @@ -459,7 +401,6 @@ func (r *CardanoNetworkReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&yacdv1alpha1.CardanoNetwork{}, ctrlbuilder.WithPredicates(cardanoNetworkEventPredicate())). Watches(&yacdv1alpha1.CardanoDBSync{}, r.dbSyncPlacementEventHandler()). - Owns(&corev1.Secret{}, ctrlbuilder.WithPredicates(faucetAuthSecretEventPredicate())). Owns(&appsv1.Deployment{}). Owns(&corev1.PersistentVolumeClaim{}). Owns(&corev1.Service{}). diff --git a/internal/controller/cardanonetwork/controller_envtest_test.go b/internal/controller/cardanonetwork/controller_envtest_test.go index 915ceb79..3c4f589b 100644 --- a/internal/controller/cardanonetwork/controller_envtest_test.go +++ b/internal/controller/cardanonetwork/controller_envtest_test.go @@ -81,7 +81,6 @@ func TestCardanoNetworkControllerManagerCreatesAndRecreatesPrimaryWorkload(t *te network := localCardanoNetwork("manager-owned") network.Namespace = namespace.Name - enableFaucet(network) require.NoError(t, apiClient.Create(ctx, network)) deploymentKey := client.ObjectKey{Namespace: network.Namespace, Name: primaryWorkloadName(network)} @@ -109,23 +108,11 @@ func TestCardanoNetworkControllerManagerCreatesAndRecreatesPrimaryWorkload(t *te return apiClient.Get(ctx, kupoServiceKey, &corev1.Service{}) == nil }, 10*time.Second, 100*time.Millisecond) - faucetServiceKey := client.ObjectKey{Namespace: network.Namespace, Name: primaryFaucetServiceName(network)} - require.Eventually(t, func() bool { - return apiClient.Get(ctx, faucetServiceKey, &corev1.Service{}) == nil - }, 10*time.Second, 100*time.Millisecond) - artifactsServiceKey := client.ObjectKey{Namespace: network.Namespace, Name: primaryArtifactsServiceName(network)} require.Eventually(t, func() bool { return apiClient.Get(ctx, artifactsServiceKey, &corev1.Service{}) == nil }, 10*time.Second, 100*time.Millisecond) - faucetAuthSecretKey := client.ObjectKey{Namespace: network.Namespace, Name: primaryFaucetAuthSecretName(network)} - require.Eventually(t, func() bool { - secret := &corev1.Secret{} - return apiClient.Get(ctx, faucetAuthSecretKey, secret) == nil && - validFaucetAuthToken(string(secret.Data[faucetAuthTokenKey])) - }, 10*time.Second, 100*time.Millisecond) - // The well-known faucet wallet Secret is generated before the Deployment so // its genesis-funded address can be injected into the genesis-funding init // container. It is owned by the CardanoNetwork and carries a derived address. @@ -202,17 +189,6 @@ func TestCardanoNetworkControllerManagerCreatesAndRecreatesPrimaryWorkload(t *te return err == nil && got.UID != originalKupoServiceUID }, 10*time.Second, 100*time.Millisecond) - faucetService := &corev1.Service{} - require.NoError(t, apiClient.Get(ctx, faucetServiceKey, faucetService)) - originalFaucetServiceUID := faucetService.UID - require.NoError(t, apiClient.Delete(ctx, faucetService)) - - require.Eventually(t, func() bool { - got := &corev1.Service{} - err := apiClient.Get(ctx, faucetServiceKey, got) - return err == nil && got.UID != originalFaucetServiceUID - }, 10*time.Second, 100*time.Millisecond) - artifactsService := &corev1.Service{} require.NoError(t, apiClient.Get(ctx, artifactsServiceKey, artifactsService)) originalArtifactsServiceUID := artifactsService.UID @@ -239,7 +215,6 @@ func TestCardanoNetworkControllerManagerCreatesAndRecreatesPrimaryWorkload(t *te {Name: cardanoNodeContainerName, Image: "example.com/cardano-node:test"}, {Name: ogmiosContainerName, Image: "example.com/ogmios:test"}, {Name: kupoContainerName, Image: "example.com/kupo:test"}, - {Name: faucetContainerName, Image: "example.com/faucet:test"}, {Name: serveContainerName, Image: "example.com/serve:test"}, }, }, @@ -274,15 +249,6 @@ func TestCardanoNetworkControllerManagerCreatesAndRecreatesPrimaryWorkload(t *te }, }, }, - { - Name: faucetContainerName, - Ready: true, - State: corev1.ContainerState{ - Running: &corev1.ContainerStateRunning{ - StartedAt: metav1.Now(), - }, - }, - }, { Name: serveContainerName, Ready: true, @@ -317,8 +283,6 @@ func TestCardanoNetworkControllerManagerCreatesAndRecreatesPrimaryWorkload(t *te return statusHasReadyConditions(ctx, apiClient, network) }, 10*time.Second, 100*time.Millisecond) - recoverDeletedFaucetAuthSecret(t, ctx, apiClient, network, faucetAuthSecretKey, deploymentKey) - forgedNetwork := &yacdv1alpha1.CardanoNetwork{} require.NoError(t, apiClient.Get(ctx, client.ObjectKeyFromObject(network), forgedNetwork)) require.NotNil(t, forgedNetwork.Status.Network) @@ -340,54 +304,6 @@ func TestCardanoNetworkControllerManagerCreatesAndRecreatesPrimaryWorkload(t *te repaired.Status.Network.LocalnetFingerprint == baselineLocalnetFingerprint && conditionHas(repaired, conditionTypeDegraded, metav1.ConditionFalse, conditionReasonReconcileSucceeded) }, 10*time.Second, 100*time.Millisecond) - - current := &yacdv1alpha1.CardanoNetwork{} - require.NoError(t, apiClient.Get(ctx, client.ObjectKeyFromObject(network), current)) - current.Spec.ChainAPI = &yacdv1alpha1.ChainAPISpec{ - Faucet: &yacdv1alpha1.FaucetSpec{ - Enabled: false, - Port: defaultFaucetPort, - DefaultSource: defaultFaucetSource, - MinTopUpLovelace: defaultFaucetMinLovelace, - MaxTopUpLovelace: defaultFaucetMaxLovelace, - }, - } - require.NoError(t, apiClient.Update(ctx, current)) - - require.Eventually(t, func() bool { - err := apiClient.Get(ctx, faucetServiceKey, &corev1.Service{}) - return apierrors.IsNotFound(err) - }, 10*time.Second, 100*time.Millisecond) - require.Eventually(t, func() bool { - err := apiClient.Get(ctx, faucetAuthSecretKey, &corev1.Secret{}) - return apierrors.IsNotFound(err) - }, 10*time.Second, 100*time.Millisecond) - // Disabling the faucet must also delete the owned genesis-funded faucet - // wallet Secret so no funding key material is left behind. - require.Eventually(t, func() bool { - err := apiClient.Get(ctx, faucetWalletSecretKey, &corev1.Secret{}) - return apierrors.IsNotFound(err) - }, 10*time.Second, 100*time.Millisecond) - require.Eventually(t, func() bool { - got := &appsv1.Deployment{} - if err := apiClient.Get(ctx, deploymentKey, got); err != nil { - return false - } - // node + ogmios + kupo + the always-on serve sidecar (faucet disabled). - return len(got.Spec.Template.Spec.Containers) == 4 - }, 10*time.Second, 100*time.Millisecond) - - require.NoError(t, apiClient.Get(ctx, deploymentKey, deployment)) - deployment.Status.ObservedGeneration = deployment.Generation - deployment.Status.Replicas = 1 - deployment.Status.UpdatedReplicas = 1 - deployment.Status.ReadyReplicas = 1 - deployment.Status.AvailableReplicas = 1 - require.NoError(t, apiClient.Status().Update(ctx, deployment)) - - require.Eventually(t, func() bool { - return statusHasDisabledFaucetReadyConditions(ctx, apiClient, network) - }, 10*time.Second, 100*time.Millisecond) } func TestCardanoNetworkControllerManagerDegradesOnPrimaryPVCDeletion(t *testing.T) { @@ -561,14 +477,11 @@ func statusHasProgressingEndpoints( conditionHas(current, conditionTypeNodeReady, metav1.ConditionFalse, "") && conditionHas(current, conditionTypeOgmiosReady, metav1.ConditionFalse, "") && conditionHas(current, conditionTypeKupoReady, metav1.ConditionFalse, "") && - conditionHas(current, conditionTypeFaucetReady, metav1.ConditionFalse, "") && conditionHas(current, conditionTypeArtifactsReady, metav1.ConditionFalse, "") && nodeToNodeEndpointMatches(current, network) && ogmiosEndpointMatches(current, network) && kupoEndpointMatches(current, network) && - faucetEndpointMatches(current, network) && - artifactsEndpointMatches(current, network) && - faucetStatusMatches(current, network) + artifactsEndpointMatches(current, network) } func statusHasReadyConditions( @@ -586,32 +499,9 @@ func statusHasReadyConditions( conditionHas(current, conditionTypeNodeReady, metav1.ConditionTrue, conditionReasonNodeReady) && conditionHas(current, conditionTypeOgmiosReady, metav1.ConditionTrue, conditionReasonOgmiosReady) && conditionHas(current, conditionTypeKupoReady, metav1.ConditionTrue, conditionReasonKupoReady) && - conditionHas(current, conditionTypeFaucetReady, metav1.ConditionTrue, conditionReasonFaucetReady) && conditionHas(current, conditionTypeArtifactsReady, metav1.ConditionTrue, conditionReasonArtifactsReady) } -func statusHasDisabledFaucetReadyConditions( - ctx context.Context, - apiClient client.Client, - network *yacdv1alpha1.CardanoNetwork, -) bool { - current := &yacdv1alpha1.CardanoNetwork{} - if err := apiClient.Get(ctx, client.ObjectKeyFromObject(network), current); err != nil { - return false - } - - return conditionHas(current, conditionTypeProgressing, metav1.ConditionFalse, conditionReasonReady) && - conditionHas(current, conditionTypeReady, metav1.ConditionTrue, conditionReasonReady) && - conditionHas(current, conditionTypeNodeReady, metav1.ConditionTrue, conditionReasonNodeReady) && - conditionHas(current, conditionTypeOgmiosReady, metav1.ConditionTrue, conditionReasonOgmiosReady) && - conditionHas(current, conditionTypeKupoReady, metav1.ConditionTrue, conditionReasonKupoReady) && - conditionHas(current, conditionTypeFaucetReady, metav1.ConditionFalse, conditionReasonFaucetDisabled) && - conditionHas(current, conditionTypeArtifactsReady, metav1.ConditionTrue, conditionReasonArtifactsReady) && - current.Status.Endpoints != nil && - current.Status.Endpoints.Faucet == nil && - current.Status.Faucet == nil -} - func conditionHas( network *yacdv1alpha1.CardanoNetwork, ct conditionType, @@ -656,16 +546,6 @@ func kupoEndpointMatches(current *yacdv1alpha1.CardanoNetwork, network *yacdv1al current.Status.Endpoints.Kupo.URL == "http://manager-owned-kupo.cardanonetwork-envtest.svc.cluster.local:1442" } -func faucetEndpointMatches(current *yacdv1alpha1.CardanoNetwork, network *yacdv1alpha1.CardanoNetwork) bool { - if current.Status.Endpoints == nil || current.Status.Endpoints.Faucet == nil { - return false - } - - return current.Status.Endpoints.Faucet.ServiceName == primaryFaucetServiceName(network) && - current.Status.Endpoints.Faucet.Port == defaultFaucetPort && - current.Status.Endpoints.Faucet.URL == "http://manager-owned-faucet.cardanonetwork-envtest.svc.cluster.local:8080" -} - func artifactsEndpointMatches(current *yacdv1alpha1.CardanoNetwork, network *yacdv1alpha1.CardanoNetwork) bool { if current.Status.Endpoints == nil || current.Status.Endpoints.Artifacts == nil { return false @@ -675,54 +555,3 @@ func artifactsEndpointMatches(current *yacdv1alpha1.CardanoNetwork, network *yac current.Status.Endpoints.Artifacts.Port == defaultServePort && current.Status.Endpoints.Artifacts.URL == "http://manager-owned-artifacts.cardanonetwork-envtest.svc.cluster.local:8090" } - -func faucetStatusMatches(current *yacdv1alpha1.CardanoNetwork, network *yacdv1alpha1.CardanoNetwork) bool { - return current.Status.Faucet != nil && - current.Status.Faucet.AuthSecretName == primaryFaucetAuthSecretName(network) -} - -func recoverDeletedFaucetAuthSecret( - t *testing.T, - ctx context.Context, - apiClient client.Client, - network *yacdv1alpha1.CardanoNetwork, - faucetAuthSecretKey client.ObjectKey, - deploymentKey client.ObjectKey, -) { - t.Helper() - - secret := &corev1.Secret{} - require.NoError(t, apiClient.Get(ctx, faucetAuthSecretKey, secret)) - originalSecretUID := secret.UID - originalToken := string(secret.Data[faucetAuthTokenKey]) - originalHash := faucetAuthTokenHash(secret) - - deployment := &appsv1.Deployment{} - require.NoError(t, apiClient.Get(ctx, deploymentKey, deployment)) - require.Equal(t, originalHash, deployment.Spec.Template.Annotations[faucetAuthTokenHashAnno]) - - require.NoError(t, apiClient.Delete(ctx, secret)) - - require.Eventually(t, func() bool { - gotSecret := &corev1.Secret{} - if err := apiClient.Get(ctx, faucetAuthSecretKey, gotSecret); err != nil { - return false - } - gotDeployment := &appsv1.Deployment{} - if err := apiClient.Get(ctx, deploymentKey, gotDeployment); err != nil { - return false - } - currentNetwork := &yacdv1alpha1.CardanoNetwork{} - if err := apiClient.Get(ctx, client.ObjectKeyFromObject(network), currentNetwork); err != nil { - return false - } - - repairedHash := faucetAuthTokenHash(gotSecret) - return gotSecret.UID != originalSecretUID && - string(gotSecret.Data[faucetAuthTokenKey]) != originalToken && - gotDeployment.Spec.Template.Annotations[faucetAuthTokenHashAnno] == repairedHash && - repairedHash != originalHash && - conditionHas(currentNetwork, conditionTypeReady, metav1.ConditionFalse, conditionReasonDeploymentProgressing) && - conditionHas(currentNetwork, conditionTypeFaucetReady, metav1.ConditionFalse, conditionReasonDeploymentProgressing) - }, 10*time.Second, 100*time.Millisecond) -} diff --git a/internal/controller/cardanonetwork/controller_test.go b/internal/controller/cardanonetwork/controller_test.go index db2a5478..731b0657 100644 --- a/internal/controller/cardanonetwork/controller_test.go +++ b/internal/controller/cardanonetwork/controller_test.go @@ -73,7 +73,6 @@ func TestCardanoNetworkReconcilerReconcileSkipsTerminatingObject(t *testing.T) { func TestCardanoNetworkReconcilerReconcileCreatesPrimaryWorkload(t *testing.T) { ctx := context.Background() network := localCardanoNetwork("creates-workload") - enableFaucet(network) reconciler := newTestReconciler(t, network) result, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) @@ -85,10 +84,7 @@ func TestCardanoNetworkReconcilerReconcileCreatesPrimaryWorkload(t *testing.T) { service := requirePrimaryService(t, ctx, reconciler, network) ogmiosService := requirePrimaryOgmiosService(t, ctx, reconciler, network) kupoService := requirePrimaryKupoService(t, ctx, reconciler, network) - faucetService := requirePrimaryFaucetService(t, ctx, reconciler, network) artifactsService := requirePrimaryArtifactsService(t, ctx, reconciler, network) - faucetAuthSecret := requirePrimaryFaucetAuthSecret(t, ctx, reconciler, network) - assertDeploymentFaucetAuthTokenHash(t, deployment, faucetAuthSecret) require.NotNil(t, deployment.Spec.Template.Spec.AutomountServiceAccountToken) assert.False(t, *deployment.Spec.Template.Spec.AutomountServiceAccountToken) assert.Empty(t, deployment.Spec.Template.Spec.ServiceAccountName) @@ -116,14 +112,6 @@ func TestCardanoNetworkReconcilerReconcileCreatesPrimaryWorkload(t *testing.T) { TargetPort: intstr.FromString(kupoPortName), }, }, kupoService.Spec.Ports) - assert.Equal(t, []corev1.ServicePort{ - { - Name: faucetPortName, - Protocol: corev1.ProtocolTCP, - Port: defaultFaucetPort, - TargetPort: intstr.FromString(faucetPortName), - }, - }, faucetService.Spec.Ports) assert.Equal(t, []corev1.ServicePort{ { Name: servePortName, @@ -132,7 +120,6 @@ func TestCardanoNetworkReconcilerReconcileCreatesPrimaryWorkload(t *testing.T) { TargetPort: intstr.FromString(servePortName), }, }, artifactsService.Spec.Ports) - assert.True(t, validFaucetAuthToken(string(faucetAuthSecret.Data[faucetAuthTokenKey]))) assert.Equal(t, deployment.Spec.Template.Annotations[localnetFingerprintAnno], requireAcceptedLocalnetFingerprint(t, ctx, reconciler, network)) assertCondition(t, ctx, reconciler, network, conditionTypeDegraded, metav1.ConditionFalse, conditionReasonReconcileSucceeded) assertCondition(t, ctx, reconciler, network, conditionTypeProgressing, metav1.ConditionTrue, conditionReasonDeploymentProgressing) @@ -140,14 +127,11 @@ func TestCardanoNetworkReconcilerReconcileCreatesPrimaryWorkload(t *testing.T) { assertCondition(t, ctx, reconciler, network, conditionTypeNodeReady, metav1.ConditionFalse, conditionReasonDeploymentProgressing) assertCondition(t, ctx, reconciler, network, conditionTypeOgmiosReady, metav1.ConditionFalse, conditionReasonDeploymentProgressing) assertCondition(t, ctx, reconciler, network, conditionTypeKupoReady, metav1.ConditionFalse, conditionReasonDeploymentProgressing) - assertCondition(t, ctx, reconciler, network, conditionTypeFaucetReady, metav1.ConditionFalse, conditionReasonDeploymentProgressing) assertCondition(t, ctx, reconciler, network, conditionTypeArtifactsReady, metav1.ConditionFalse, conditionReasonDeploymentProgressing) assertNodeToNodeEndpoint(t, ctx, reconciler, network, service.Name, network.Spec.Node.Port) assertOgmiosEndpoint(t, ctx, reconciler, network, ogmiosService.Name, defaultOgmiosPort) assertKupoEndpoint(t, ctx, reconciler, network, kupoService.Name, defaultKupoPort) - assertFaucetEndpoint(t, ctx, reconciler, network, faucetService.Name, defaultFaucetPort) assertArtifactsEndpoint(t, ctx, reconciler, network, artifactsService.Name, defaultServePort) - assertFaucetStatus(t, ctx, reconciler, network, faucetAuthSecret.Name) } func TestCardanoNetworkReconcilerReconcileCreatesPublicPreviewWorkload(t *testing.T) { @@ -174,15 +158,12 @@ func TestCardanoNetworkReconcilerReconcileCreatesPublicPreviewWorkload(t *testin assert.Equal(t, ogmiosContainerName, deployment.Spec.Template.Spec.Containers[1].Name) assert.Equal(t, serveContainerName, deployment.Spec.Template.Spec.Containers[2].Name) assertNoContainerNamed(t, deployment.Spec.Template.Spec.Containers, kupoContainerName) - assertNoContainerNamed(t, deployment.Spec.Template.Spec.Containers, faucetContainerName) assert.Equal(t, "3eee469d6200db89fd64fbd032ccbb58a7ba557b920a07bc2f22523b6f009a29", deployment.Spec.Template.Annotations[networkFingerprintAnno]) assert.NotContains(t, deployment.Spec.Template.Annotations, localnetFingerprintAnno) // The public node and ogmios read their config from the fetched // served-artifact directory on the node-state PVC, not a /profile ConfigMap. assertNoVolumeNamed(t, deployment.Spec.Template.Spec.Volumes, "network-artifacts") assertNoPrimaryKupoService(t, ctx, reconciler, network) - assertNoPrimaryFaucetService(t, ctx, reconciler, network) - assertNoPrimaryFaucetAuthSecret(t, ctx, reconciler, network) artifactsService := requirePrimaryArtifactsService(t, ctx, reconciler, network) assert.Equal(t, []corev1.ServicePort{ @@ -210,8 +191,6 @@ func TestCardanoNetworkReconcilerReconcileCreatesPublicPreviewWorkload(t *testin assert.Equal(t, int64(2), *current.Status.Network.NetworkMagic) require.NotNil(t, current.Status.Endpoints) assert.Nil(t, current.Status.Endpoints.Kupo) - assert.Nil(t, current.Status.Endpoints.Faucet) - assert.Nil(t, current.Status.Faucet) } func TestCardanoNetworkReconcilerReconcileRepairsForgedNetworkIdentityStatus(t *testing.T) { @@ -274,25 +253,6 @@ func TestCardanoNetworkReconcilerReconcileCreatesPublicMainnetWorkload(t *testin assert.Equal(t, yacdv1alpha1.PublicNetworkProfileMainnet, *current.Status.Network.Profile) } -func TestCardanoNetworkReconcilerReconcileLeavesFaucetDisabledByDefault(t *testing.T) { - ctx := context.Background() - network := localCardanoNetwork("faucet-default-disabled") - reconciler := newTestReconciler(t, network) - - _, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) - require.NoError(t, err) - - deployment := requirePrimaryDeployment(t, ctx, reconciler, network) - require.Len(t, deployment.Spec.Template.Spec.Containers, 4) - assertNoPrimaryFaucetService(t, ctx, reconciler, network) - assertNoPrimaryFaucetAuthSecret(t, ctx, reconciler, network) - assertCondition(t, ctx, reconciler, network, conditionTypeFaucetReady, metav1.ConditionFalse, conditionReasonFaucetDisabled) - current := requireNetwork(t, ctx, reconciler, network) - require.NotNil(t, current.Status.Endpoints) - assert.Nil(t, current.Status.Endpoints.Faucet) - assert.Nil(t, current.Status.Faucet) -} - func TestCardanoNetworkReconcilerReconcileAttachesPrimarySidecarDBSync(t *testing.T) { ctx := context.Background() network := readyLocalCardanoNetwork() @@ -585,8 +545,10 @@ func TestCardanoNetworkReconcilerReconcileAttachesPrimarySidecarDBSyncWithoutCon func TestCardanoNetworkReconcilerReconcileSkipsPrimarySidecarDBSyncOnPortConflict(t *testing.T) { ctx := context.Background() network := readyLocalCardanoNetwork() - enableFaucet(network) dbSync := readyPrimarySidecarDBSync("dbsync", network) + // Force the db-sync metrics port to collide with the ogmios port already + // owned by the primary Pod so the attachment is rejected as unsupported. + dbSync.Spec.Config.Runtime = &yacdv1alpha1.CardanoDBSyncRuntimeSpec{MetricsPort: defaultOgmiosPort} reconciler := newTestReconciler(t, network, dbSync) storeNetworkStatus(t, ctx, reconciler, network) @@ -602,14 +564,13 @@ func TestCardanoNetworkReconcilerReconcileSkipsPrimarySidecarDBSyncOnPortConflic func TestCardanoNetworkReconcilerReconcileReportsNodeReadyWhenDeploymentAvailable(t *testing.T) { ctx := context.Background() network := localCardanoNetwork("node-ready") - enableFaucet(network) reconciler := newTestReconciler(t, network) _, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) require.NoError(t, err) deployment := requirePrimaryDeployment(t, ctx, reconciler, network) markPrimaryDeploymentAvailable(t, ctx, reconciler, deployment) - markPrimaryPodContainersReady(t, ctx, reconciler, network, cardanoNodeContainerName, ogmiosContainerName, kupoContainerName, faucetContainerName, serveContainerName) + markPrimaryPodContainersReady(t, ctx, reconciler, network, cardanoNodeContainerName, ogmiosContainerName, kupoContainerName, serveContainerName) result, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) require.NoError(t, err) @@ -622,13 +583,11 @@ func TestCardanoNetworkReconcilerReconcileReportsNodeReadyWhenDeploymentAvailabl assertCondition(t, ctx, reconciler, network, conditionTypeNodeReady, metav1.ConditionTrue, conditionReasonNodeReady) assertCondition(t, ctx, reconciler, network, conditionTypeOgmiosReady, metav1.ConditionTrue, conditionReasonOgmiosReady) assertCondition(t, ctx, reconciler, network, conditionTypeKupoReady, metav1.ConditionTrue, conditionReasonKupoReady) - assertCondition(t, ctx, reconciler, network, conditionTypeFaucetReady, metav1.ConditionTrue, conditionReasonFaucetReady) } func TestCardanoNetworkReconcilerReconcileKeepsNodeReadySeparateFromOgmios(t *testing.T) { ctx := context.Background() network := localCardanoNetwork("node-ready-ogmios-waiting") - enableFaucet(network) reconciler := newTestReconciler(t, network) _, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) @@ -644,14 +603,12 @@ func TestCardanoNetworkReconcilerReconcileKeepsNodeReadySeparateFromOgmios(t *te assertCondition(t, ctx, reconciler, network, conditionTypeNodeReady, metav1.ConditionTrue, conditionReasonNodeReady) assertCondition(t, ctx, reconciler, network, conditionTypeOgmiosReady, metav1.ConditionFalse, conditionReasonDeploymentProgressing) assertCondition(t, ctx, reconciler, network, conditionTypeKupoReady, metav1.ConditionFalse, conditionReasonDeploymentProgressing) - assertCondition(t, ctx, reconciler, network, conditionTypeFaucetReady, metav1.ConditionFalse, conditionReasonDeploymentProgressing) assertCondition(t, ctx, reconciler, network, conditionTypeReady, metav1.ConditionFalse, conditionReasonDeploymentProgressing) } func TestCardanoNetworkReconcilerReconcileRequiresKupoReadyWhenEnabled(t *testing.T) { ctx := context.Background() network := localCardanoNetwork("ogmios-ready-kupo-waiting") - enableFaucet(network) reconciler := newTestReconciler(t, network) _, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) @@ -667,7 +624,6 @@ func TestCardanoNetworkReconcilerReconcileRequiresKupoReadyWhenEnabled(t *testin assertCondition(t, ctx, reconciler, network, conditionTypeNodeReady, metav1.ConditionTrue, conditionReasonNodeReady) assertCondition(t, ctx, reconciler, network, conditionTypeOgmiosReady, metav1.ConditionTrue, conditionReasonOgmiosReady) assertCondition(t, ctx, reconciler, network, conditionTypeKupoReady, metav1.ConditionFalse, conditionReasonDeploymentProgressing) - assertCondition(t, ctx, reconciler, network, conditionTypeFaucetReady, metav1.ConditionFalse, conditionReasonDeploymentProgressing) assertCondition(t, ctx, reconciler, network, conditionTypeReady, metav1.ConditionFalse, conditionReasonDeploymentProgressing) } @@ -690,17 +646,12 @@ func TestCardanoNetworkReconcilerReconcileDisablesOgmios(t *testing.T) { assert.Equal(t, serveContainerName, deployment.Spec.Template.Spec.Containers[1].Name) assertNoPrimaryOgmiosService(t, ctx, reconciler, network) assertNoPrimaryKupoService(t, ctx, reconciler, network) - assertNoPrimaryFaucetService(t, ctx, reconciler, network) - assertNoPrimaryFaucetAuthSecret(t, ctx, reconciler, network) assertCondition(t, ctx, reconciler, network, conditionTypeOgmiosReady, metav1.ConditionFalse, conditionReasonOgmiosDisabled) assertCondition(t, ctx, reconciler, network, conditionTypeKupoReady, metav1.ConditionFalse, conditionReasonKupoDisabled) - assertCondition(t, ctx, reconciler, network, conditionTypeFaucetReady, metav1.ConditionFalse, conditionReasonFaucetDisabled) current := requireNetwork(t, ctx, reconciler, network) require.NotNil(t, current.Status.Endpoints) assert.Nil(t, current.Status.Endpoints.Ogmios) assert.Nil(t, current.Status.Endpoints.Kupo) - assert.Nil(t, current.Status.Endpoints.Faucet) - assert.Nil(t, current.Status.Faucet) markPrimaryDeploymentAvailable(t, ctx, reconciler, deployment) markPrimaryPodContainersReady(t, ctx, reconciler, network, cardanoNodeContainerName) @@ -710,7 +661,6 @@ func TestCardanoNetworkReconcilerReconcileDisablesOgmios(t *testing.T) { assertCondition(t, ctx, reconciler, network, conditionTypeNodeReady, metav1.ConditionTrue, conditionReasonNodeReady) assertCondition(t, ctx, reconciler, network, conditionTypeOgmiosReady, metav1.ConditionFalse, conditionReasonOgmiosDisabled) assertCondition(t, ctx, reconciler, network, conditionTypeKupoReady, metav1.ConditionFalse, conditionReasonKupoDisabled) - assertCondition(t, ctx, reconciler, network, conditionTypeFaucetReady, metav1.ConditionFalse, conditionReasonFaucetDisabled) assertCondition(t, ctx, reconciler, network, conditionTypeReady, metav1.ConditionFalse, conditionReasonOgmiosDisabled) assertCondition(t, ctx, reconciler, network, conditionTypeProgressing, metav1.ConditionFalse, conditionReasonOgmiosDisabled) } @@ -718,15 +668,12 @@ func TestCardanoNetworkReconcilerReconcileDisablesOgmios(t *testing.T) { func TestCardanoNetworkReconcilerReconcileDeletesOwnedOgmiosServiceWhenDisabled(t *testing.T) { ctx := context.Background() network := localCardanoNetwork("deletes-ogmios-service") - enableFaucet(network) reconciler := newTestReconciler(t, network) _, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) require.NoError(t, err) requirePrimaryOgmiosService(t, ctx, reconciler, network) requirePrimaryKupoService(t, ctx, reconciler, network) - requirePrimaryFaucetService(t, ctx, reconciler, network) - requirePrimaryFaucetAuthSecret(t, ctx, reconciler, network) current := requireNetwork(t, ctx, reconciler, network) current.Spec.ChainAPI = &yacdv1alpha1.ChainAPISpec{ @@ -741,19 +688,12 @@ func TestCardanoNetworkReconcilerReconcileDeletesOwnedOgmiosServiceWhenDisabled( assertNoPrimaryOgmiosService(t, ctx, reconciler, network) assertNoPrimaryKupoService(t, ctx, reconciler, network) - assertNoPrimaryFaucetService(t, ctx, reconciler, network) - assertNoPrimaryFaucetAuthSecret(t, ctx, reconciler, network) - deployment := requirePrimaryDeployment(t, ctx, reconciler, network) - assert.NotContains(t, deployment.Spec.Template.Annotations, faucetAuthTokenHashAnno) current = requireNetwork(t, ctx, reconciler, network) require.NotNil(t, current.Status.Endpoints) assert.Nil(t, current.Status.Endpoints.Ogmios) assert.Nil(t, current.Status.Endpoints.Kupo) - assert.Nil(t, current.Status.Endpoints.Faucet) - assert.Nil(t, current.Status.Faucet) assertCondition(t, ctx, reconciler, network, conditionTypeOgmiosReady, metav1.ConditionFalse, conditionReasonOgmiosDisabled) assertCondition(t, ctx, reconciler, network, conditionTypeKupoReady, metav1.ConditionFalse, conditionReasonKupoDisabled) - assertCondition(t, ctx, reconciler, network, conditionTypeFaucetReady, metav1.ConditionFalse, conditionReasonFaucetDisabled) } func TestCardanoNetworkReconcilerReconcileDisablesKupo(t *testing.T) { @@ -776,15 +716,10 @@ func TestCardanoNetworkReconcilerReconcileDisablesKupo(t *testing.T) { assert.Equal(t, serveContainerName, deployment.Spec.Template.Spec.Containers[2].Name) requirePrimaryOgmiosService(t, ctx, reconciler, network) assertNoPrimaryKupoService(t, ctx, reconciler, network) - assertNoPrimaryFaucetService(t, ctx, reconciler, network) - assertNoPrimaryFaucetAuthSecret(t, ctx, reconciler, network) assertCondition(t, ctx, reconciler, network, conditionTypeKupoReady, metav1.ConditionFalse, conditionReasonKupoDisabled) - assertCondition(t, ctx, reconciler, network, conditionTypeFaucetReady, metav1.ConditionFalse, conditionReasonFaucetDisabled) current := requireNetwork(t, ctx, reconciler, network) require.NotNil(t, current.Status.Endpoints) assert.Nil(t, current.Status.Endpoints.Kupo) - assert.Nil(t, current.Status.Endpoints.Faucet) - assert.Nil(t, current.Status.Faucet) markPrimaryDeploymentAvailable(t, ctx, reconciler, deployment) markPrimaryPodContainersReady(t, ctx, reconciler, network, cardanoNodeContainerName, ogmiosContainerName, serveContainerName) @@ -794,7 +729,6 @@ func TestCardanoNetworkReconcilerReconcileDisablesKupo(t *testing.T) { assertCondition(t, ctx, reconciler, network, conditionTypeNodeReady, metav1.ConditionTrue, conditionReasonNodeReady) assertCondition(t, ctx, reconciler, network, conditionTypeOgmiosReady, metav1.ConditionTrue, conditionReasonOgmiosReady) assertCondition(t, ctx, reconciler, network, conditionTypeKupoReady, metav1.ConditionFalse, conditionReasonKupoDisabled) - assertCondition(t, ctx, reconciler, network, conditionTypeFaucetReady, metav1.ConditionFalse, conditionReasonFaucetDisabled) assertCondition(t, ctx, reconciler, network, conditionTypeReady, metav1.ConditionTrue, conditionReasonReady) assertCondition(t, ctx, reconciler, network, conditionTypeProgressing, metav1.ConditionFalse, conditionReasonReady) } @@ -802,14 +736,11 @@ func TestCardanoNetworkReconcilerReconcileDisablesKupo(t *testing.T) { func TestCardanoNetworkReconcilerReconcileDeletesOwnedKupoServiceWhenDisabled(t *testing.T) { ctx := context.Background() network := localCardanoNetwork("deletes-kupo-service") - enableFaucet(network) reconciler := newTestReconciler(t, network) _, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) require.NoError(t, err) requirePrimaryKupoService(t, ctx, reconciler, network) - requirePrimaryFaucetService(t, ctx, reconciler, network) - requirePrimaryFaucetAuthSecret(t, ctx, reconciler, network) current := requireNetwork(t, ctx, reconciler, network) current.Spec.ChainAPI = &yacdv1alpha1.ChainAPISpec{ @@ -823,119 +754,15 @@ func TestCardanoNetworkReconcilerReconcileDeletesOwnedKupoServiceWhenDisabled(t require.NoError(t, err) assertNoPrimaryKupoService(t, ctx, reconciler, network) - assertNoPrimaryFaucetService(t, ctx, reconciler, network) - assertNoPrimaryFaucetAuthSecret(t, ctx, reconciler, network) - deployment := requirePrimaryDeployment(t, ctx, reconciler, network) - assert.NotContains(t, deployment.Spec.Template.Annotations, faucetAuthTokenHashAnno) current = requireNetwork(t, ctx, reconciler, network) require.NotNil(t, current.Status.Endpoints) assert.Nil(t, current.Status.Endpoints.Kupo) - assert.Nil(t, current.Status.Endpoints.Faucet) - assert.Nil(t, current.Status.Faucet) assertCondition(t, ctx, reconciler, network, conditionTypeKupoReady, metav1.ConditionFalse, conditionReasonKupoDisabled) - assertCondition(t, ctx, reconciler, network, conditionTypeFaucetReady, metav1.ConditionFalse, conditionReasonFaucetDisabled) -} - -func TestCardanoNetworkReconcilerReconcileRequiresFaucetReadyWhenEnabled(t *testing.T) { - ctx := context.Background() - network := localCardanoNetwork("kupo-ready-faucet-waiting") - enableFaucet(network) - reconciler := newTestReconciler(t, network) - - _, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) - require.NoError(t, err) - deployment := requirePrimaryDeployment(t, ctx, reconciler, network) - markPrimaryDeploymentAvailable(t, ctx, reconciler, deployment) - markPrimaryPodContainersReady(t, ctx, reconciler, network, cardanoNodeContainerName, ogmiosContainerName, kupoContainerName) - - result, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) - require.NoError(t, err) - - assert.Equal(t, ctrl.Result{RequeueAfter: primaryWorkloadReadinessRequeueAfter}, result) - assertCondition(t, ctx, reconciler, network, conditionTypeNodeReady, metav1.ConditionTrue, conditionReasonNodeReady) - assertCondition(t, ctx, reconciler, network, conditionTypeOgmiosReady, metav1.ConditionTrue, conditionReasonOgmiosReady) - assertCondition(t, ctx, reconciler, network, conditionTypeKupoReady, metav1.ConditionTrue, conditionReasonKupoReady) - assertCondition(t, ctx, reconciler, network, conditionTypeFaucetReady, metav1.ConditionFalse, conditionReasonDeploymentProgressing) - assertCondition(t, ctx, reconciler, network, conditionTypeReady, metav1.ConditionFalse, conditionReasonDeploymentProgressing) -} - -func TestCardanoNetworkReconcilerReconcileDisablesFaucet(t *testing.T) { - ctx := context.Background() - network := localCardanoNetwork("faucet-disabled") - network.Spec.ChainAPI = &yacdv1alpha1.ChainAPISpec{ - Faucet: &yacdv1alpha1.FaucetSpec{ - Enabled: false, - }, - } - reconciler := newTestReconciler(t, network) - - _, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) - require.NoError(t, err) - - deployment := requirePrimaryDeployment(t, ctx, reconciler, network) - require.Len(t, deployment.Spec.Template.Spec.Containers, 4) - assert.Equal(t, cardanoNodeContainerName, deployment.Spec.Template.Spec.Containers[0].Name) - assert.Equal(t, ogmiosContainerName, deployment.Spec.Template.Spec.Containers[1].Name) - assert.Equal(t, kupoContainerName, deployment.Spec.Template.Spec.Containers[2].Name) - assert.Equal(t, serveContainerName, deployment.Spec.Template.Spec.Containers[3].Name) - requirePrimaryOgmiosService(t, ctx, reconciler, network) - requirePrimaryKupoService(t, ctx, reconciler, network) - assertNoPrimaryFaucetService(t, ctx, reconciler, network) - assertNoPrimaryFaucetAuthSecret(t, ctx, reconciler, network) - assertCondition(t, ctx, reconciler, network, conditionTypeFaucetReady, metav1.ConditionFalse, conditionReasonFaucetDisabled) - current := requireNetwork(t, ctx, reconciler, network) - require.NotNil(t, current.Status.Endpoints) - assert.Nil(t, current.Status.Endpoints.Faucet) - assert.Nil(t, current.Status.Faucet) - - markPrimaryDeploymentAvailable(t, ctx, reconciler, deployment) - markPrimaryPodContainersReady(t, ctx, reconciler, network, cardanoNodeContainerName, ogmiosContainerName, kupoContainerName, serveContainerName) - _, err = reconciler.Reconcile(ctx, reconcileRequestFor(network)) - require.NoError(t, err) - - assertCondition(t, ctx, reconciler, network, conditionTypeReady, metav1.ConditionTrue, conditionReasonReady) - assertCondition(t, ctx, reconciler, network, conditionTypeFaucetReady, metav1.ConditionFalse, conditionReasonFaucetDisabled) -} - -func TestCardanoNetworkReconcilerReconcileDeletesOwnedFaucetChildrenWhenDisabled(t *testing.T) { - ctx := context.Background() - network := localCardanoNetwork("deletes-faucet-children") - enableFaucet(network) - reconciler := newTestReconciler(t, network) - - _, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) - require.NoError(t, err) - requirePrimaryFaucetService(t, ctx, reconciler, network) - requirePrimaryFaucetAuthSecret(t, ctx, reconciler, network) - requirePrimaryFaucetWalletSecret(t, ctx, reconciler, network) - - current := requireNetwork(t, ctx, reconciler, network) - current.Spec.ChainAPI = &yacdv1alpha1.ChainAPISpec{ - Faucet: &yacdv1alpha1.FaucetSpec{ - Enabled: false, - }, - } - require.NoError(t, reconciler.Update(ctx, current)) - - _, err = reconciler.Reconcile(ctx, reconcileRequestFor(network)) - require.NoError(t, err) - - assertNoPrimaryFaucetService(t, ctx, reconciler, network) - assertNoPrimaryFaucetAuthSecret(t, ctx, reconciler, network) - assertNoPrimaryFaucetWalletSecret(t, ctx, reconciler, network) - deployment := requirePrimaryDeployment(t, ctx, reconciler, network) - assert.NotContains(t, deployment.Spec.Template.Annotations, faucetAuthTokenHashAnno) - current = requireNetwork(t, ctx, reconciler, network) - require.NotNil(t, current.Status.Endpoints) - assert.Nil(t, current.Status.Endpoints.Faucet) - assert.Nil(t, current.Status.Faucet) - assertCondition(t, ctx, reconciler, network, conditionTypeFaucetReady, metav1.ConditionFalse, conditionReasonFaucetDisabled) } func TestCardanoNetworkReconcilerReconcileIsIdempotent(t *testing.T) { ctx := context.Background() network := localCardanoNetwork("idempotent") - enableFaucet(network) reconciler := newTestReconciler(t, network) _, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) @@ -951,12 +778,12 @@ func TestCardanoNetworkReconcilerReconcileIsIdempotent(t *testing.T) { assert.Len(t, persistentVolumeClaims.Items, 1) var services corev1.ServiceList require.NoError(t, reconciler.List(ctx, &services)) - // node-to-node, ogmios, kupo, faucet, and the always-on artifacts Service. - assert.Len(t, services.Items, 5) + // node-to-node, ogmios, kupo, and the always-on artifacts Service. + assert.Len(t, services.Items, 4) var secrets corev1.SecretList require.NoError(t, reconciler.List(ctx, &secrets)) - // The faucet auth Secret and the genesis-funded faucet wallet Secret. - assert.Len(t, secrets.Items, 2) + // The genesis-funded faucet wallet Secret. + assert.Len(t, secrets.Items, 1) } func TestCardanoNetworkReconcilerReconcilePatchesMutableDeploymentTemplate(t *testing.T) { @@ -1200,191 +1027,13 @@ func TestCardanoNetworkReconcilerReconcileCorrectsKupoServiceAndPreservesMetadat assertCondition(t, ctx, reconciler, network, conditionTypeDegraded, metav1.ConditionFalse, conditionReasonReconcileSucceeded) } -func TestCardanoNetworkReconcilerReconcileCorrectsFaucetServiceAndPreservesMetadata(t *testing.T) { - const ( - clusterIP = "10.0.0.45" - foreignMetadataValue = "keep" - ) - - ctx := context.Background() - network := localCardanoNetwork("corrects-faucet-service") - enableFaucet(network) - reconciler := newTestReconciler(t, network) - - _, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) - require.NoError(t, err) - - service := requirePrimaryFaucetService(t, ctx, reconciler, network) - ipFamilyPolicy := corev1.IPFamilyPolicySingleStack - service.Labels["example.com/foreign-label"] = foreignMetadataValue - service.Labels[labelAppManagedBy] = wrongManagedByLabelValue - service.Annotations = map[string]string{"example.com/foreign-annotation": foreignMetadataValue} - service.Spec.Type = corev1.ServiceTypeNodePort - service.Spec.Selector = map[string]string{"unexpected": "true"} - service.Spec.Ports = []corev1.ServicePort{ - { - Name: "wrong", - Protocol: corev1.ProtocolTCP, - Port: 9996, - TargetPort: intstr.FromInt(9996), - NodePort: 32003, - }, - } - service.Spec.ClusterIP = clusterIP - service.Spec.ClusterIPs = []string{clusterIP} - service.Spec.IPFamilies = []corev1.IPFamily{corev1.IPv4Protocol} - service.Spec.IPFamilyPolicy = &ipFamilyPolicy - require.NoError(t, reconciler.Update(ctx, service)) - - _, err = reconciler.Reconcile(ctx, reconcileRequestFor(network)) - require.NoError(t, err) - - service = requirePrimaryFaucetService(t, ctx, reconciler, network) - assert.Equal(t, foreignMetadataValue, service.Labels["example.com/foreign-label"]) - assert.Equal(t, "yacd", service.Labels[labelAppManagedBy]) - assert.Equal(t, foreignMetadataValue, service.Annotations["example.com/foreign-annotation"]) - assert.Equal(t, corev1.ServiceTypeClusterIP, service.Spec.Type) - assert.Equal(t, primaryWorkloadSelectorLabels(network), service.Spec.Selector) - assert.Equal(t, []corev1.ServicePort{ - { - Name: faucetPortName, - Protocol: corev1.ProtocolTCP, - Port: defaultFaucetPort, - TargetPort: intstr.FromString(faucetPortName), - }, - }, service.Spec.Ports) - assert.Equal(t, clusterIP, service.Spec.ClusterIP) - assert.Equal(t, []string{clusterIP}, service.Spec.ClusterIPs) - assert.Equal(t, []corev1.IPFamily{corev1.IPv4Protocol}, service.Spec.IPFamilies) - require.NotNil(t, service.Spec.IPFamilyPolicy) - assert.Equal(t, corev1.IPFamilyPolicySingleStack, *service.Spec.IPFamilyPolicy) - assertCondition(t, ctx, reconciler, network, conditionTypeDegraded, metav1.ConditionFalse, conditionReasonReconcileSucceeded) -} - -func TestCardanoNetworkReconcilerReconcilePreservesValidFaucetAuthToken(t *testing.T) { - const ( - foreignMetadataValue = "keep" - validToken = "abcdefghijklmnopqrstuvwxyzABCDEF1234567890" - ) - - ctx := context.Background() - network := localCardanoNetwork("preserves-faucet-token") - enableFaucet(network) - reconciler := newTestReconciler(t, network) - - _, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) - require.NoError(t, err) - - secret := requirePrimaryFaucetAuthSecret(t, ctx, reconciler, network) - secret.Labels["example.com/foreign-label"] = foreignMetadataValue - secret.Labels[labelAppManagedBy] = wrongManagedByLabelValue - secret.Annotations = map[string]string{"example.com/foreign-annotation": foreignMetadataValue} - secret.Type = corev1.SecretTypeBasicAuth - secret.Data[faucetAuthTokenKey] = []byte(validToken) - require.NoError(t, reconciler.Update(ctx, secret)) - - _, err = reconciler.Reconcile(ctx, reconcileRequestFor(network)) - require.NoError(t, err) - - secret = requirePrimaryFaucetAuthSecret(t, ctx, reconciler, network) - deployment := requirePrimaryDeployment(t, ctx, reconciler, network) - assert.Equal(t, foreignMetadataValue, secret.Labels["example.com/foreign-label"]) - assert.Equal(t, "yacd", secret.Labels[labelAppManagedBy]) - assert.Equal(t, foreignMetadataValue, secret.Annotations["example.com/foreign-annotation"]) - assert.Equal(t, corev1.SecretTypeOpaque, secret.Type) - assert.Equal(t, validToken, string(secret.Data[faucetAuthTokenKey])) - assertDeploymentFaucetAuthTokenHash(t, deployment, secret) - assertCondition(t, ctx, reconciler, network, conditionTypeDegraded, metav1.ConditionFalse, conditionReasonReconcileSucceeded) -} - -func TestCardanoNetworkReconcilerReconcileRollsDeploymentForValidFaucetAuthTokenRotation(t *testing.T) { - const validToken = "abcdefghijklmnopqrstuvwxyzABCDEF1234567890" - - ctx := context.Background() - network := localCardanoNetwork("rotates-faucet-token") - enableFaucet(network) - reconciler := newTestReconciler(t, network) - - _, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) - require.NoError(t, err) - deployment := requirePrimaryDeployment(t, ctx, reconciler, network) - originalHash := deployment.Spec.Template.Annotations[faucetAuthTokenHashAnno] - - secret := requirePrimaryFaucetAuthSecret(t, ctx, reconciler, network) - secret.Data[faucetAuthTokenKey] = []byte(validToken) - require.NoError(t, reconciler.Update(ctx, secret)) - - _, err = reconciler.Reconcile(ctx, reconcileRequestFor(network)) - require.NoError(t, err) - - secret = requirePrimaryFaucetAuthSecret(t, ctx, reconciler, network) - deployment = requirePrimaryDeployment(t, ctx, reconciler, network) - assert.Equal(t, validToken, string(secret.Data[faucetAuthTokenKey])) - assert.NotEqual(t, originalHash, deployment.Spec.Template.Annotations[faucetAuthTokenHashAnno]) - assertDeploymentFaucetAuthTokenHash(t, deployment, secret) - assertCondition(t, ctx, reconciler, network, conditionTypeDegraded, metav1.ConditionFalse, conditionReasonReconcileSucceeded) -} - -func TestCardanoNetworkReconcilerReconcileRegeneratesInvalidFaucetAuthToken(t *testing.T) { - ctx := context.Background() - network := localCardanoNetwork("regenerates-faucet-token") - enableFaucet(network) - reconciler := newTestReconciler(t, network) - - _, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) - require.NoError(t, err) - - secret := requirePrimaryFaucetAuthSecret(t, ctx, reconciler, network) - secret.Data[faucetAuthTokenKey] = []byte("short") - require.NoError(t, reconciler.Update(ctx, secret)) - - _, err = reconciler.Reconcile(ctx, reconcileRequestFor(network)) - require.NoError(t, err) - - secret = requirePrimaryFaucetAuthSecret(t, ctx, reconciler, network) - deployment := requirePrimaryDeployment(t, ctx, reconciler, network) - token := string(secret.Data[faucetAuthTokenKey]) - assert.NotEqual(t, "short", token) - assert.True(t, validFaucetAuthToken(token)) - assertDeploymentFaucetAuthTokenHash(t, deployment, secret) - assertCondition(t, ctx, reconciler, network, conditionTypeDegraded, metav1.ConditionFalse, conditionReasonReconcileSucceeded) -} - -func TestCardanoNetworkReconcilerReconcileRepairsMissingFaucetAuthSecret(t *testing.T) { - ctx := context.Background() - network := localCardanoNetwork("repairs-faucet-token") - enableFaucet(network) - reconciler := newTestReconciler(t, network) - - _, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) - require.NoError(t, err) - secret := requirePrimaryFaucetAuthSecret(t, ctx, reconciler, network) - originalToken := string(secret.Data[faucetAuthTokenKey]) - deployment := requirePrimaryDeployment(t, ctx, reconciler, network) - originalHash := deployment.Spec.Template.Annotations[faucetAuthTokenHashAnno] - require.NoError(t, reconciler.Delete(ctx, secret)) - assertNoPrimaryFaucetAuthSecret(t, ctx, reconciler, network) - - _, err = reconciler.Reconcile(ctx, reconcileRequestFor(network)) - require.NoError(t, err) - - secret = requirePrimaryFaucetAuthSecret(t, ctx, reconciler, network) - deployment = requirePrimaryDeployment(t, ctx, reconciler, network) - token := string(secret.Data[faucetAuthTokenKey]) - assert.NotEqual(t, originalToken, token) - assert.True(t, validFaucetAuthToken(token)) - assert.NotEqual(t, originalHash, deployment.Spec.Template.Annotations[faucetAuthTokenHashAnno]) - assertDeploymentFaucetAuthTokenHash(t, deployment, secret) - assertCondition(t, ctx, reconciler, network, conditionTypeDegraded, metav1.ConditionFalse, conditionReasonReconcileSucceeded) -} - func TestCardanoNetworkReconcilerApplyPrimaryDeploymentIgnoresAPIDefaults(t *testing.T) { const foreignMetadataValue = "keep" ctx := context.Background() network := localCardanoNetwork("ignores-api-defaults") reconciler := newTestReconciler(t, network) - resources, err := (primaryWorkloadBuilder{scheme: reconciler.Scheme}).Build(network) + resources, err := newTestPrimaryWorkloadBuilder(t).Build(network) require.NoError(t, err) result, err := reconciler.applyPrimaryDeployment(ctx, resources.Deployment) @@ -1971,52 +1620,6 @@ func TestCardanoNetworkReconcilerReconcileRejectsChildResourceCollisions(t *test } }, }, - { - name: "foreign-owned-faucet-service", - child: func(network *yacdv1alpha1.CardanoNetwork) client.Object { - return &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: primaryFaucetServiceName(network), - Namespace: network.Namespace, - OwnerReferences: []metav1.OwnerReference{foreignControllerOwnerReference()}, - }, - } - }, - }, - { - name: "unowned-faucet-service", - child: func(network *yacdv1alpha1.CardanoNetwork) client.Object { - return &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: primaryFaucetServiceName(network), - Namespace: network.Namespace, - }, - } - }, - }, - { - name: "foreign-owned-faucet-secret", - child: func(network *yacdv1alpha1.CardanoNetwork) client.Object { - return &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: primaryFaucetAuthSecretName(network), - Namespace: network.Namespace, - OwnerReferences: []metav1.OwnerReference{foreignControllerOwnerReference()}, - }, - } - }, - }, - { - name: "unowned-faucet-secret", - child: func(network *yacdv1alpha1.CardanoNetwork) client.Object { - return &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: primaryFaucetAuthSecretName(network), - Namespace: network.Namespace, - }, - } - }, - }, } for _, tt := range tests { @@ -2036,7 +1639,6 @@ func TestCardanoNetworkReconcilerReconcileRejectsChildResourceCollisions(t *test assertCondition(t, ctx, reconciler, network, conditionTypeNodeReady, metav1.ConditionFalse, conditionReasonResourceConflict) assertCondition(t, ctx, reconciler, network, conditionTypeOgmiosReady, metav1.ConditionFalse, conditionReasonResourceConflict) assertCondition(t, ctx, reconciler, network, conditionTypeKupoReady, metav1.ConditionFalse, conditionReasonResourceConflict) - assertCondition(t, ctx, reconciler, network, conditionTypeFaucetReady, metav1.ConditionFalse, conditionReasonResourceConflict) assertCondition(t, ctx, reconciler, network, conditionTypeArtifactsReady, metav1.ConditionFalse, conditionReasonResourceConflict) }) } @@ -2044,7 +1646,10 @@ func TestCardanoNetworkReconcilerReconcileRejectsChildResourceCollisions(t *test func TestCardanoNetworkReconcilerReconcileReturnsInternalBuildErrors(t *testing.T) { ctx := context.Background() - network := localCardanoNetwork("internal-build-error") + // A public network is used so the build reaches its scheme guard without + // first tripping the local-only faucet wallet ensure step (which generates + // the wallet Secret before Build and needs a non-nil scheme of its own). + network := publicPreviewCardanoNetwork("internal-build-error") reconciler := newTestReconciler(t, network) reconciler.Scheme = nil @@ -2086,14 +1691,6 @@ func TestCardanoNetworkReconcilerReconcileMarksUnsupportedInput(t *testing.T) { return network }(), }, - { - name: "public faucet", - network: func() *yacdv1alpha1.CardanoNetwork { - network := publicPreviewCardanoNetwork("unsupported-public-faucet") - enableFaucet(network) - return network - }(), - }, { name: "public mainnet without mithril bootstrap", network: publicCardanoNetwork("unsupported-public-mainnet", yacdv1alpha1.PublicNetworkProfileMainnet), @@ -2129,52 +1726,17 @@ func TestCardanoNetworkReconcilerReconcileMarksUnsupportedInput(t *testing.T) { assertCondition(t, ctx, reconciler, network, conditionTypeNodeReady, metav1.ConditionFalse, conditionReasonUnsupportedSpec) assertCondition(t, ctx, reconciler, network, conditionTypeOgmiosReady, metav1.ConditionFalse, conditionReasonUnsupportedSpec) assertCondition(t, ctx, reconciler, network, conditionTypeKupoReady, metav1.ConditionFalse, conditionReasonUnsupportedSpec) - assertCondition(t, ctx, reconciler, network, conditionTypeFaucetReady, metav1.ConditionFalse, conditionReasonUnsupportedSpec) current := requireNetwork(t, ctx, reconciler, network) assert.Nil(t, current.Status.Endpoints) }) } } -func TestCardanoNetworkReconcilerReconcileRevokesFaucetOnUnsupportedSpec(t *testing.T) { - ctx := context.Background() - network := localCardanoNetwork("revokes-unsupported-faucet") - enableFaucet(network) - reconciler := newTestReconciler(t, network) - - _, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) - require.NoError(t, err) - requirePrimaryFaucetService(t, ctx, reconciler, network) - requirePrimaryFaucetAuthSecret(t, ctx, reconciler, network) - assertFaucetEndpoint(t, ctx, reconciler, network, primaryFaucetServiceName(network), defaultFaucetPort) - assertFaucetStatus(t, ctx, reconciler, network, primaryFaucetAuthSecretName(network)) - - current := requireNetwork(t, ctx, reconciler, network) - current.Spec.ChainAPI.Faucet.DefaultSource = "../utxo1" - require.NoError(t, reconciler.Update(ctx, current)) - - result, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) - require.NoError(t, err) - assert.Equal(t, ctrl.Result{}, result) - - assertNoPrimaryFaucetService(t, ctx, reconciler, network) - assertNoPrimaryFaucetAuthSecret(t, ctx, reconciler, network) - deployment := requirePrimaryDeployment(t, ctx, reconciler, network) - assertNoContainerNamed(t, deployment.Spec.Template.Spec.InitContainers, faucetSourceAddressInitContainerName) - assertNoContainerNamed(t, deployment.Spec.Template.Spec.Containers, faucetContainerName) - assertNoVolumeNamed(t, deployment.Spec.Template.Spec.Volumes, faucetAuthVolumeName) - assertCondition(t, ctx, reconciler, network, conditionTypeFaucetReady, metav1.ConditionFalse, conditionReasonUnsupportedSpec) - current = requireNetwork(t, ctx, reconciler, network) - require.NotNil(t, current.Status.Endpoints) - assert.Nil(t, current.Status.Endpoints.Faucet) - assert.Nil(t, current.Status.Faucet) -} - func TestCardanoNetworkReconcilerPrimaryNodeReadyConditionReportsMissingChildren(t *testing.T) { ctx := context.Background() network := localCardanoNetwork("missing-children") reconciler := newTestReconciler(t, network) - resources, err := (primaryWorkloadBuilder{scheme: reconciler.Scheme}).Build(network) + resources, err := newTestPrimaryWorkloadBuilder(t).Build(network) require.NoError(t, err) got, err := reconciler.primaryNodeReadyCondition(ctx, network) @@ -2275,19 +1837,6 @@ func publicCardanoNetwork(name string, profile yacdv1alpha1.PublicNetworkProfile } } -func enableFaucet(network *yacdv1alpha1.CardanoNetwork) { - if network.Spec.ChainAPI == nil { - network.Spec.ChainAPI = &yacdv1alpha1.ChainAPISpec{} - } - network.Spec.ChainAPI.Faucet = &yacdv1alpha1.FaucetSpec{ - Enabled: true, - Port: defaultFaucetPort, - DefaultSource: defaultFaucetSource, - MinTopUpLovelace: defaultFaucetMinLovelace, - MaxTopUpLovelace: defaultFaucetMaxLovelace, - } -} - func readyLocalCardanoNetwork() *yacdv1alpha1.CardanoNetwork { network := localCardanoNetwork("primary-dbsync") network.Status.ObservedGeneration = network.Generation @@ -2603,7 +2152,7 @@ func requirePrimaryKupoService( return service } -func requirePrimaryFaucetService( +func requirePrimaryArtifactsService( t *testing.T, ctx context.Context, reconciler *CardanoNetworkReconciler, @@ -2614,44 +2163,43 @@ func requirePrimaryFaucetService( service := &corev1.Service{} require.NoError(t, reconciler.Get(ctx, types.NamespacedName{ Namespace: network.Namespace, - Name: primaryFaucetServiceName(network), + Name: primaryArtifactsServiceName(network), }, service)) return service } -func requirePrimaryArtifactsService( - t *testing.T, - ctx context.Context, - reconciler *CardanoNetworkReconciler, - network *yacdv1alpha1.CardanoNetwork, -) *corev1.Service { - t.Helper() +// TestCardanoNetworkReconcilerReconcileGatesFaucetWalletOnLocalMode proves the +// P4 re-gate at the controller level: with the faucet service (and its +// spec.chainAPI.faucet toggle) gone, the genesis-funded faucet wallet Secret is +// created for a local network and is absent for a non-local one, gated on mode +// alone. This is the reconcile-driven companion to the unit-level +// TestFaucetWalletEnabledPredicate, and the load-bearing guard against a fresh +// devnet booting with no funding source. +func TestCardanoNetworkReconcilerReconcileGatesFaucetWalletOnLocalMode(t *testing.T) { + ctx := context.Background() - service := &corev1.Service{} - require.NoError(t, reconciler.Get(ctx, types.NamespacedName{ - Namespace: network.Namespace, - Name: primaryArtifactsServiceName(network), - }, service)) + t.Run("local mode creates the genesis-funded faucet wallet Secret", func(t *testing.T) { + network := localCardanoNetwork("faucet-wallet-local") + reconciler := newTestReconciler(t, network) - return service -} + _, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) + require.NoError(t, err) -func requirePrimaryFaucetAuthSecret( - t *testing.T, - ctx context.Context, - reconciler *CardanoNetworkReconciler, - network *yacdv1alpha1.CardanoNetwork, -) *corev1.Secret { - t.Helper() + secret := requirePrimaryFaucetWalletSecret(t, ctx, reconciler, network) + assert.Equal(t, faucetWalletName, secret.Labels[walletNameLabel]) + assert.NotEmpty(t, secret.Data[walletAddressKey]) + }) - secret := &corev1.Secret{} - require.NoError(t, reconciler.Get(ctx, types.NamespacedName{ - Namespace: network.Namespace, - Name: primaryFaucetAuthSecretName(network), - }, secret)) + t.Run("non-local mode leaves no faucet wallet Secret", func(t *testing.T) { + network := publicPreviewCardanoNetwork("faucet-wallet-public") + reconciler := newTestReconciler(t, network) - return secret + _, err := reconciler.Reconcile(ctx, reconcileRequestFor(network)) + require.NoError(t, err) + + assertNoPrimaryFaucetWalletSecret(t, ctx, reconciler, network) + }) } func requirePrimaryFaucetWalletSecret( @@ -2671,13 +2219,6 @@ func requirePrimaryFaucetWalletSecret( return secret } -func assertDeploymentFaucetAuthTokenHash(t *testing.T, deployment *appsv1.Deployment, secret *corev1.Secret) { - t.Helper() - - require.NotNil(t, deployment.Spec.Template.Annotations) - assert.Equal(t, faucetAuthTokenHash(secret), deployment.Spec.Template.Annotations[faucetAuthTokenHashAnno]) -} - // attachDBSyncSidecar reconciles the primary node twice so the db-sync primary // sidecar attachment settles. Since the network-artifacts ConfigMap removal (F0 // PR-B1) there is no producer ConfigMap to publish; the referenced CardanoDBSync @@ -2836,8 +2377,6 @@ func assertNoPrimaryChildren( assertNoPrimaryOgmiosService(t, ctx, reconciler, network) assertNoPrimaryKupoService(t, ctx, reconciler, network) - assertNoPrimaryFaucetService(t, ctx, reconciler, network) - assertNoPrimaryFaucetAuthSecret(t, ctx, reconciler, network) assertNoPrimaryArtifactsService(t, ctx, reconciler, network) } @@ -2871,21 +2410,6 @@ func assertNoPrimaryKupoService( assert.True(t, apierrors.IsNotFound(err), "expected Kupo Service to be absent, got %v", err) } -func assertNoPrimaryFaucetService( - t *testing.T, - ctx context.Context, - reconciler *CardanoNetworkReconciler, - network *yacdv1alpha1.CardanoNetwork, -) { - t.Helper() - - err := reconciler.Get(ctx, types.NamespacedName{ - Namespace: network.Namespace, - Name: primaryFaucetServiceName(network), - }, &corev1.Service{}) - assert.True(t, apierrors.IsNotFound(err), "expected faucet Service to be absent, got %v", err) -} - func assertNoPrimaryArtifactsService( t *testing.T, ctx context.Context, @@ -2901,21 +2425,6 @@ func assertNoPrimaryArtifactsService( assert.True(t, apierrors.IsNotFound(err), "expected artifacts Service to be absent, got %v", err) } -func assertNoPrimaryFaucetAuthSecret( - t *testing.T, - ctx context.Context, - reconciler *CardanoNetworkReconciler, - network *yacdv1alpha1.CardanoNetwork, -) { - t.Helper() - - err := reconciler.Get(ctx, types.NamespacedName{ - Namespace: network.Namespace, - Name: primaryFaucetAuthSecretName(network), - }, &corev1.Secret{}) - assert.True(t, apierrors.IsNotFound(err), "expected faucet auth Secret to be absent, got %v", err) -} - func assertNoPrimaryFaucetWalletSecret( t *testing.T, ctx context.Context, @@ -3054,27 +2563,6 @@ func assertKupoEndpoint( ) } -func assertFaucetEndpoint( - t *testing.T, - ctx context.Context, - reconciler *CardanoNetworkReconciler, - network *yacdv1alpha1.CardanoNetwork, - serviceName string, - port int32, -) { - t.Helper() - - current := requireNetwork(t, ctx, reconciler, network) - require.NotNil(t, current.Status.Endpoints) - require.NotNil(t, current.Status.Endpoints.Faucet) - assert.Equal(t, serviceName, current.Status.Endpoints.Faucet.ServiceName) - assert.Equal(t, port, current.Status.Endpoints.Faucet.Port) - assert.Equal(t, - fmt.Sprintf("http://%s.%s.svc.cluster.local:%d", serviceName, network.Namespace, port), - current.Status.Endpoints.Faucet.URL, - ) -} - func assertArtifactsEndpoint( t *testing.T, ctx context.Context, @@ -3096,20 +2584,6 @@ func assertArtifactsEndpoint( ) } -func assertFaucetStatus( - t *testing.T, - ctx context.Context, - reconciler *CardanoNetworkReconciler, - network *yacdv1alpha1.CardanoNetwork, - authSecretName string, -) { - t.Helper() - - current := requireNetwork(t, ctx, reconciler, network) - require.NotNil(t, current.Status.Faucet) - assert.Equal(t, authSecretName, current.Status.Faucet.AuthSecretName) -} - // reconcileRequestFor returns a reconcile request targeting object. func reconcileRequestFor(object *yacdv1alpha1.CardanoNetwork) ctrl.Request { return ctrl.Request{ diff --git a/internal/controller/cardanonetwork/defaults.go b/internal/controller/cardanonetwork/defaults.go index 76e95b29..e82a8f4c 100644 --- a/internal/controller/cardanonetwork/defaults.go +++ b/internal/controller/cardanonetwork/defaults.go @@ -29,8 +29,7 @@ const ( defaultMainnetNodeMemoryRequest = "24Gi" // localnetStateDir is the durable state mount root inside the primary - // workload Pod. cardano-testnet, cardano-node, ogmios, and the faucet share - // this prefix. + // workload Pod. cardano-testnet, cardano-node, and ogmios share this prefix. localnetStateDir = "/state" // localnetEnvDir is the cardano-testnet create-env output directory and the @@ -83,40 +82,13 @@ const ( // kupoServiceURLType is the scheme published on the kupo endpoint status. kupoServiceURLType = "http" - // defaultFaucetImage is the faucet sidecar image used when neither the - // CardanoNetwork spec nor the Reconciler-injected default specifies one. - // The Reconciler-injected DefaultFaucetImage is the legitimate primary - // injection point for the local dev stack's ko-built image; this constant - // is the last-resort fallback. - defaultFaucetImage = "ghcr.io/meigma/yacd/faucet:dev" - - // defaultFaucetPort is the faucet HTTP port used when the CardanoNetwork - // spec does not specify one. - defaultFaucetPort = primarypod.DefaultFaucetPort - - // defaultFaucetSource is the default UTXO source name for faucet top-ups. - defaultFaucetSource = "utxo1" - - // defaultFaucetMinLovelace is the minimum top-up amount in lovelace when - // the CardanoNetwork spec does not specify one. - defaultFaucetMinLovelace = 1_000_000 - - // defaultFaucetMaxLovelace is the maximum top-up amount in lovelace when - // the CardanoNetwork spec does not specify one. - defaultFaucetMaxLovelace = 10_000_000_000 - // defaultFaucetWalletFundingLovelace is the genesis allocation granted to // the well-known faucet wallet on local networks. 1,000,000 ADA sits well // under the local genesis supply headroom (~10M ADA) yet is plenty for a // devnet funding source that the CLI spends from. The allocation is added - // as a new initialFunds entry, so it never touches the faucet service's own - // utxo source. + // as a new initialFunds entry alongside the generated utxo sources. defaultFaucetWalletFundingLovelace int64 = 1_000_000_000_000 - // faucetServiceURLType is the scheme published on the faucet endpoint - // status. - faucetServiceURLType = "http" - // defaultServePort is the cardano-tools serve container port used for the // artifacts Service. defaultServePort = primarypod.DefaultServePort diff --git a/internal/controller/cardanonetwork/delete.go b/internal/controller/cardanonetwork/delete.go index 6e485957..489d02f2 100644 --- a/internal/controller/cardanonetwork/delete.go +++ b/internal/controller/cardanonetwork/delete.go @@ -2,17 +2,13 @@ package cardanonetwork import ( "context" - "errors" "fmt" yacdv1alpha1 "github.com/meigma/yacd/api/v1alpha1" ctrlmetadata "github.com/meigma/yacd/internal/ctrlkit/metadata" - appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/equality" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) @@ -39,15 +35,6 @@ func (r *CardanoNetworkReconciler) deletePrimaryKupoService( return r.deletePrimaryChainAPIService(ctx, network, primaryKupoServiceName(network), "Kupo") } -// deletePrimaryFaucetService deletes the optional faucet Service when the -// CardanoNetwork spec turns the faucet off after it had been enabled. -func (r *CardanoNetworkReconciler) deletePrimaryFaucetService( - ctx context.Context, - network *yacdv1alpha1.CardanoNetwork, -) (controllerutil.OperationResult, error) { - return r.deletePrimaryChainAPIService(ctx, network, primaryFaucetServiceName(network), "faucet") -} - // deletePrimaryArtifactsService deletes the optional artifacts Service when // the network no longer runs the serve sidecar (for example after a switch to // a custom public profile). @@ -58,46 +45,10 @@ func (r *CardanoNetworkReconciler) deletePrimaryArtifactsService( return r.deletePrimaryChainAPIService(ctx, network, primaryArtifactsServiceName(network), "artifacts") } -// deletePrimaryFaucetAuthSecret deletes the faucet auth Secret when the -// CardanoNetwork spec turns the faucet off after it had been enabled. -func (r *CardanoNetworkReconciler) deletePrimaryFaucetAuthSecret( - ctx context.Context, - network *yacdv1alpha1.CardanoNetwork, -) (controllerutil.OperationResult, error) { - desired := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: primaryFaucetAuthSecretName(network), - Namespace: network.Namespace, - }, - } - if err := controllerutil.SetControllerReference(network, desired, r.Scheme); err != nil { - return controllerutil.OperationResultNone, fmt.Errorf("set desired faucet auth Secret owner reference: %w", err) - } - - current := &corev1.Secret{} - // Secrets are not in the manager cache; live-read to avoid a cache miss - // looking like a non-existent object. - err := r.liveReader().Get(ctx, ctrlmetadata.ObjectKey(desired), current) - if apierrors.IsNotFound(err) { - return controllerutil.OperationResultNone, nil - } - if err != nil { - return controllerutil.OperationResultNone, err - } - if err := validateControllerOwner(current, desired); err != nil { - return controllerutil.OperationResultNone, err - } - if err := r.Delete(ctx, current); err != nil { - return controllerutil.OperationResultNone, err - } - - return operationResultDeleted, nil -} - // deletePrimaryFaucetWalletSecret deletes the well-known faucet wallet Secret -// when the CardanoNetwork no longer gates it on (faucet disabled or a switch -// away from local mode). An explicit disable is a deliberate request to discard -// the wallet, so the owned Secret is removed. +// when the CardanoNetwork no longer gates it on (a switch away from local +// mode). An explicit switch is a deliberate request to discard the wallet, so +// the owned Secret is removed. func (r *CardanoNetworkReconciler) deletePrimaryFaucetWalletSecret( ctx context.Context, network *yacdv1alpha1.CardanoNetwork, @@ -168,173 +119,3 @@ func (r *CardanoNetworkReconciler) deletePrimaryChainAPIService( return operationResultDeleted, nil } - -// revokePrimaryFaucetExposure tears down the faucet sidecar's externally -// visible surface (Service, auth Secret, Deployment containers/volumes) -// when the CardanoNetwork enters a Degraded state. Errors from the three -// teardown steps are joined so partial cleanup still tries every step -// before surfacing failure to the reconciler. -func (r *CardanoNetworkReconciler) revokePrimaryFaucetExposure( - ctx context.Context, - network *yacdv1alpha1.CardanoNetwork, -) error { - return errors.Join( - r.deletePrimaryFaucetServiceIfOwned(ctx, network), - r.deletePrimaryFaucetAuthSecretIfOwned(ctx, network), - r.removePrimaryFaucetFromDeploymentIfOwned(ctx, network), - ) -} - -// deletePrimaryFaucetServiceIfOwned deletes the faucet Service when present -// and owned by this controller. Used by revokePrimaryFaucetExposure for the -// best-effort cleanup path. -func (r *CardanoNetworkReconciler) deletePrimaryFaucetServiceIfOwned( - ctx context.Context, - network *yacdv1alpha1.CardanoNetwork, -) error { - desired := &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: primaryFaucetServiceName(network), - Namespace: network.Namespace, - }, - } - if err := controllerutil.SetControllerReference(network, desired, r.Scheme); err != nil { - return fmt.Errorf("set desired faucet Service owner reference: %w", err) - } - - return r.deleteObjectIfOwned(ctx, desired, &corev1.Service{}) -} - -// deletePrimaryFaucetAuthSecretIfOwned deletes the faucet auth Secret when -// present and owned by this controller. Reads through liveReader because -// Secrets are not in the manager cache. -func (r *CardanoNetworkReconciler) deletePrimaryFaucetAuthSecretIfOwned( - ctx context.Context, - network *yacdv1alpha1.CardanoNetwork, -) error { - desired := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: primaryFaucetAuthSecretName(network), - Namespace: network.Namespace, - }, - } - if err := controllerutil.SetControllerReference(network, desired, r.Scheme); err != nil { - return fmt.Errorf("set desired faucet auth Secret owner reference: %w", err) - } - - return r.deleteObjectIfOwnedWithReader(ctx, desired, &corev1.Secret{}, r.liveReader()) -} - -// deleteObjectIfOwned reads through the cached client, then deletes the -// object when present and owned by this controller. Best-effort: ownership -// mismatch is silently skipped rather than surfaced as an error. -func (r *CardanoNetworkReconciler) deleteObjectIfOwned( - ctx context.Context, - desired client.Object, - current client.Object, -) error { - return r.deleteObjectIfOwnedWithReader(ctx, desired, current, r.Client) -} - -// deleteObjectIfOwnedWithReader is the read-through variant used when the -// caller must bypass the manager cache (for example: Secrets, which are not -// cached). -func (r *CardanoNetworkReconciler) deleteObjectIfOwnedWithReader( - ctx context.Context, - desired client.Object, - current client.Object, - reader client.Reader, -) error { - err := reader.Get(ctx, ctrlmetadata.ObjectKey(desired), current) - if apierrors.IsNotFound(err) { - return nil - } - if err != nil { - return err - } - if validateControllerOwner(current, desired) != nil { - return nil - } - - return r.Delete(ctx, current) -} - -// removePrimaryFaucetFromDeploymentIfOwned strips the faucet container, -// faucet source-address init container, and the faucet auth volume from -// the live primary Deployment. Used during the Degraded faucet revocation -// path so the in-cluster Pod no longer references a Secret the controller -// is about to delete. -func (r *CardanoNetworkReconciler) removePrimaryFaucetFromDeploymentIfOwned( - ctx context.Context, - network *yacdv1alpha1.CardanoNetwork, -) error { - desired := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: primaryWorkloadName(network), - Namespace: network.Namespace, - }, - } - if err := controllerutil.SetControllerReference(network, desired, r.Scheme); err != nil { - return fmt.Errorf("set desired primary Deployment owner reference: %w", err) - } - - deployment := &appsv1.Deployment{} - err := r.Get(ctx, ctrlmetadata.ObjectKey(desired), deployment) - if apierrors.IsNotFound(err) { - return nil - } - if err != nil { - return err - } - if validateControllerOwner(deployment, desired) != nil { - return nil - } - - before := deployment.DeepCopy() - deployment.Spec.Template.Spec.InitContainers = removeContainersByName( - deployment.Spec.Template.Spec.InitContainers, - faucetSourceAddressInitContainerName, - ) - deployment.Spec.Template.Spec.Containers = removeContainersByName( - deployment.Spec.Template.Spec.Containers, - faucetContainerName, - ) - deployment.Spec.Template.Spec.Volumes = removeVolumesByName( - deployment.Spec.Template.Spec.Volumes, - faucetAuthVolumeName, - ) - if equality.Semantic.DeepEqual(before, deployment) { - return nil - } - - return r.Patch(ctx, deployment, client.MergeFrom(before)) -} - -// removeContainersByName returns a copy of containers with every entry whose -// Name matches removed. The implementation reuses the input slice's -// underlying array (filter-in-place pattern) because the caller (the -// faucet-revocation patch path) discards the input afterward. -func removeContainersByName(containers []corev1.Container, name string) []corev1.Container { - filtered := containers[:0] - for _, container := range containers { - if container.Name == name { - continue - } - filtered = append(filtered, container) - } - - return filtered -} - -// removeVolumesByName mirrors removeContainersByName for Volume slices. -func removeVolumesByName(volumes []corev1.Volume, name string) []corev1.Volume { - filtered := volumes[:0] - for _, volume := range volumes { - if volume.Name == name { - continue - } - filtered = append(filtered, volume) - } - - return filtered -} diff --git a/internal/controller/cardanonetwork/doc.go b/internal/controller/cardanonetwork/doc.go index eb60bbe0..0372ec5d 100644 --- a/internal/controller/cardanonetwork/doc.go +++ b/internal/controller/cardanonetwork/doc.go @@ -1,9 +1,10 @@ // Package cardanonetwork reconciles CardanoNetwork custom resources. The // controller renders a primary cardano-node workload, the optional ogmios / -// kupo / faucet chain API sidecars, an always-on cardano-tools serve sidecar +// kupo chain API sidecars, an always-on cardano-tools serve sidecar // (and its owned artifacts Service) that exposes the staged network artifacts -// over HTTP, and the selected CardanoDBSync primary-sidecar attachment; it then -// publishes endpoints and readiness state through CardanoNetwork status. +// over HTTP, the genesis-funded faucet wallet Secret on local networks, and the +// selected CardanoDBSync primary-sidecar attachment; it then publishes +// endpoints and readiness state through CardanoNetwork status. // // Network artifacts live on the node-state PVC at /state/artifacts: a local // network generates them with cardano-testnet create-env, a curated public @@ -19,16 +20,16 @@ // init_container.go: pure builders. Given a CardanoNetwork // spec they produce desired Kubernetes objects in memory and never // touch the API server, time, randomness, or the file system. -// - controller.go, apply.go, callbacks.go, delete.go, faucet_auth.go, +// - controller.go, apply.go, callbacks.go, delete.go, wallet.go, // status.go, readiness.go: side-effecting reconciler. Reads from and -// writes to the cluster, generates and hashes faucet auth tokens -// (faucet_auth.go is the only crypto/rand caller), and publishes status. +// writes to the cluster, generates the faucet wallet key material +// (wallet.go is the only crypto/rand caller), and publishes status. // // Owned-child apply is routed through ctrlkit/apply.ApplyOwnedObject with // per-resource Validate/Mutate callbacks (callbacks.go). // // Status conditions follow the standard Progressing / Degraded / Ready -// shape with per-component {Node,Ogmios,Kupo,Faucet,Artifacts}Ready +// shape with per-component {Node,Ogmios,Kupo,Artifacts}Ready // conditions. Condition type/reason/message strings are package-private // constants in conditions.go. package cardanonetwork diff --git a/internal/controller/cardanonetwork/faucet_auth.go b/internal/controller/cardanonetwork/faucet_auth.go deleted file mode 100644 index b4c9c82b..00000000 --- a/internal/controller/cardanonetwork/faucet_auth.go +++ /dev/null @@ -1,173 +0,0 @@ -// The faucet auth Secret apply is a deliberate exception to the otherwise -// uniform [github.com/meigma/yacd/internal/ctrlkit/apply.ApplyOwnedObject] -// pattern used everywhere else in this package. Two hard constraints make -// the shared helper a poor fit: -// -// 1. ApplyOwnedObject reads through [sigs.k8s.io/controller-runtime/pkg/client.Client] -// (the cached client). Faucet readiness and token-hash stamping must use -// live Secret data so stale cache reads cannot publish a misleading -// Deployment revision or Ready condition. -// 2. ApplyOwnedObject's Mutate callback only runs for existing objects, so -// create-once data (here: the random auth token) cannot flow through -// it without a second pass. -// -// The shape below is a small dispatcher that reads through liveReader, then -// routes to a create-with-token path or a reconcile-existing path. - -package cardanonetwork - -import ( - "context" - "crypto/rand" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "fmt" - "unicode" - - ctrlmetadata "github.com/meigma/yacd/internal/ctrlkit/metadata" - ctrlresources "github.com/meigma/yacd/internal/ctrlkit/resources" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/equality" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" -) - -// faucetAuthTokenByteLength is the random byte count fed into the faucet -// auth token before base64url encoding. 32 bytes (256 bits) is the lower -// bound validFaucetAuthToken enforces; matching the validator's length -// requirement keeps Create and validate-on-Update consistent. -const faucetAuthTokenByteLength = 32 - -// applyPrimaryFaucetAuthSecret reconciles the faucet auth Secret through a -// live read (Secrets are uncached) and then dispatches to the create or -// reconcile path. See the file-level comment for why this Secret does not -// flow through ApplyOwnedObject. -func (r *CardanoNetworkReconciler) applyPrimaryFaucetAuthSecret( - ctx context.Context, - desired *corev1.Secret, -) (controllerutil.OperationResult, *corev1.Secret, error) { - desired = desired.DeepCopy() - if err := r.defaultObject(desired); err != nil { - return controllerutil.OperationResultNone, nil, err - } - - current := &corev1.Secret{} - err := r.liveReader().Get(ctx, ctrlmetadata.ObjectKey(desired), current) - if apierrors.IsNotFound(err) { - return r.createFaucetAuthSecretWithToken(ctx, desired) - } - if err != nil { - return controllerutil.OperationResultNone, nil, err - } - - return r.reconcileFaucetAuthSecret(ctx, current, desired) -} - -// createFaucetAuthSecretWithToken handles the not-yet-created branch: -// generate a fresh token, populate Secret.Data, persist with Create. -func (r *CardanoNetworkReconciler) createFaucetAuthSecretWithToken( - ctx context.Context, - desired *corev1.Secret, -) (controllerutil.OperationResult, *corev1.Secret, error) { - token, err := generateFaucetAuthToken() - if err != nil { - return controllerutil.OperationResultNone, nil, err - } - desired.Data = map[string][]byte{ - faucetAuthTokenKey: []byte(token), - } - if err := r.Create(ctx, desired); err != nil { - return controllerutil.OperationResultNone, nil, err - } - - return controllerutil.OperationResultCreated, desired, nil -} - -// reconcileFaucetAuthSecret handles the live-Secret-exists branch: -// validate ownership, preserve an existing valid token, regenerate when the -// live data fails the validator, and persist with a diff-aware Patch. This -// is the create-once-then-preserve contract the faucet sidecar depends on -// across reconcile loops. -func (r *CardanoNetworkReconciler) reconcileFaucetAuthSecret( - ctx context.Context, - current *corev1.Secret, - desired *corev1.Secret, -) (controllerutil.OperationResult, *corev1.Secret, error) { - if err := validateControllerOwner(current, desired); err != nil { - return controllerutil.OperationResultNone, nil, err - } - - before := current.DeepCopy() - ctrlresources.MutateObjectMetadata(current, desired, nil) - current.Type = corev1.SecretTypeOpaque - if current.Data == nil { - current.Data = map[string][]byte{} - } - if !validFaucetAuthToken(string(current.Data[faucetAuthTokenKey])) { - token, err := generateFaucetAuthToken() - if err != nil { - return controllerutil.OperationResultNone, nil, err - } - current.Data[faucetAuthTokenKey] = []byte(token) - } - - if equality.Semantic.DeepEqual(before, current) { - return controllerutil.OperationResultNone, current, nil - } - if err := r.Patch(ctx, current, client.MergeFrom(before)); err != nil { - return controllerutil.OperationResultNone, nil, err - } - - return controllerutil.OperationResultUpdated, current, nil -} - -// generateFaucetAuthToken returns a base64url-encoded random token. The -// random source is crypto/rand; failure is surfaced as an error rather than -// panicking so the reconciler can requeue. -func generateFaucetAuthToken() (string, error) { - var tokenBytes [faucetAuthTokenByteLength]byte - if _, err := rand.Read(tokenBytes[:]); err != nil { - return "", fmt.Errorf("generate faucet auth token: %w", err) - } - - return base64.RawURLEncoding.EncodeToString(tokenBytes[:]), nil -} - -// setDeploymentFaucetAuthTokenHash stamps the live faucet auth token hash -// onto the Deployment pod template so token creation or rotation rolls the -// primary Pod through Kubernetes' normal ReplicaSet machinery. -func setDeploymentFaucetAuthTokenHash(deployment *appsv1.Deployment, secret *corev1.Secret) { - if deployment.Spec.Template.Annotations == nil { - deployment.Spec.Template.Annotations = map[string]string{} - } - deployment.Spec.Template.Annotations[faucetAuthTokenHashAnno] = faucetAuthTokenHash(secret) -} - -// faucetAuthTokenHash returns the stable Deployment revision value for the -// live token bytes. The caller validates and repairs the token before hashing. -func faucetAuthTokenHash(secret *corev1.Secret) string { - sum := sha256.Sum256(secret.Data[faucetAuthTokenKey]) - - return "sha256:" + hex.EncodeToString(sum[:]) -} - -// validFaucetAuthToken reports whether the given token meets the minimum -// length and character constraints (no whitespace, no control characters). -// Pure: no side effects, used both during apply (to decide whether to -// regenerate) and during readiness probing (to decide whether the live -// Secret is usable). -func validFaucetAuthToken(token string) bool { - if len(token) < faucetAuthTokenByteLength { - return false - } - for _, char := range token { - if unicode.IsSpace(char) || unicode.IsControl(char) { - return false - } - } - - return true -} diff --git a/internal/controller/cardanonetwork/faucet_auth_watch.go b/internal/controller/cardanonetwork/faucet_auth_watch.go deleted file mode 100644 index 6662f2e1..00000000 --- a/internal/controller/cardanonetwork/faucet_auth_watch.go +++ /dev/null @@ -1,51 +0,0 @@ -package cardanonetwork - -import ( - "strings" - - yacdv1alpha1 "github.com/meigma/yacd/api/v1alpha1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/predicate" -) - -// faucetAuthSecretEventPredicate keeps the owned Secret watch scoped to -// CardanoNetwork faucet auth Secrets. -func faucetAuthSecretEventPredicate() predicate.Predicate { - return predicate.Funcs{ - CreateFunc: func(e event.CreateEvent) bool { - return isPrimaryFaucetAuthSecretObject(e.Object) - }, - DeleteFunc: func(e event.DeleteEvent) bool { - return isPrimaryFaucetAuthSecretObject(e.Object) - }, - UpdateFunc: func(e event.UpdateEvent) bool { - return isPrimaryFaucetAuthSecretObject(e.ObjectOld) || - isPrimaryFaucetAuthSecretObject(e.ObjectNew) - }, - GenericFunc: func(e event.GenericEvent) bool { - return isPrimaryFaucetAuthSecretObject(e.Object) - }, - } -} - -func isPrimaryFaucetAuthSecretObject(object client.Object) bool { - if object == nil { - return false - } - if !strings.HasSuffix(object.GetName(), "-faucet-auth") { - return false - } - labels := object.GetLabels() - if labels[labelAppManagedBy] != "yacd" || - labels[labelAppName] != labelPrimaryNodeName || - labels[labelCardanoRole] != labelPrimaryRole { - return false - } - - controller := metav1.GetControllerOf(object) - return controller != nil && - controller.APIVersion == yacdv1alpha1.GroupVersion.String() && - controller.Kind == "CardanoNetwork" -} diff --git a/internal/controller/cardanonetwork/init_container.go b/internal/controller/cardanonetwork/init_container.go index 4214efec..43975a3c 100644 --- a/internal/controller/cardanonetwork/init_container.go +++ b/internal/controller/cardanonetwork/init_container.go @@ -20,7 +20,6 @@ const ( localnetCreateEnvInitContainerName = "cardano-testnet-create-env" mithrilBootstrapInitContainerName = "mithril-bootstrap" - faucetSourceAddressInitContainerName = "faucet-source-addresses" faucetWalletGenesisInitContainerName = "faucet-wallet-genesis-funding" servedArtifactsInitContainerName = "served-artifacts" localnetStateVolumeName = "localnet-state" @@ -28,9 +27,6 @@ const ( localnetCreateEnvCommand = "/opt/yacd/bin/yacd-cardano-testnet-init" cardanoToolsCommand = "/opt/yacd/bin/yacd-cardano-tools" mithrilBootstrapCommand = "/bin/sh" - faucetSourceAddressCommand = "/bin/sh" - faucetVerificationKeyFileName = "utxo.vkey" - faucetAddressFileName = "utxo.addr" localnetToolsRunAsID int64 = 10001 @@ -103,50 +99,6 @@ func (b primaryWorkloadBuilder) cardanoTestnetInitContainer(network *yacdv1alpha }, nil } -func (b primaryWorkloadBuilder) faucetSourceAddressInitContainer(plan localnet.Plan) corev1.Container { - toolVersion := strings.TrimSpace(plan.Spec.Tool.Version) - script := fmt.Sprintf(`for dir in %s/utxo[1-9]*; do - [ -d "$dir" ] || continue - [ -f "$dir/%s" ] || continue - cardano-cli address build --testnet-magic %d --payment-verification-key-file "$dir/%s" --out-file "$dir/%s" -done`, - faucetUTXOKeysDir, - faucetVerificationKeyFileName, - plan.Spec.NetworkMagic, - faucetVerificationKeyFileName, - faucetAddressFileName, - ) - - return corev1.Container{ - Name: faucetSourceAddressInitContainerName, - Image: b.cardanoTestnetImage(toolVersion), - ImagePullPolicy: corev1.PullIfNotPresent, - Command: []string{faucetSourceAddressCommand}, - Args: []string{"-eu", "-c", script}, - VolumeMounts: []corev1.VolumeMount{ - { - Name: localnetStateVolumeName, - MountPath: plan.Layout.StateDir, - }, - }, - SecurityContext: &corev1.SecurityContext{ - AllowPrivilegeEscalation: new(false), - Capabilities: &corev1.Capabilities{ - Drop: []corev1.Capability{"ALL"}, - }, - ReadOnlyRootFilesystem: new(true), - RunAsGroup: new(localnetToolsRunAsID), - RunAsNonRoot: new(true), - RunAsUser: new(localnetToolsRunAsID), - SeccompProfile: &corev1.SeccompProfile{ - Type: corev1.SeccompProfileTypeRuntimeDefault, - }, - }, - TerminationMessagePath: corev1.TerminationMessagePathDefault, - TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, - } -} - // faucetWalletGenesisFundingInitContainer builds the init container that adds a // genesis allocation for the well-known faucet wallet. It runs after create-env // (which writes shelley-genesis.json) and before the served-artifact stage init @@ -377,8 +329,8 @@ func isCuratedPublicProfile(plan primaryNetworkPlan) bool { } // cardanoTestnetImage returns the cardano-testnet container image reference -// used for the create-env init container, the faucet source-address init -// container, and the default cardano-node container. The +// used for the create-env init container and the default cardano-node +// container. The // Reconciler-injected defaultCardanoTestnetImage takes precedence so the // local dev stack can substitute a freshly built tools image when the // published cardano-testnet tag is behind the publisher code that depends diff --git a/internal/controller/cardanonetwork/init_container_test.go b/internal/controller/cardanonetwork/init_container_test.go index 05bc6568..249332bd 100644 --- a/internal/controller/cardanonetwork/init_container_test.go +++ b/internal/controller/cardanonetwork/init_container_test.go @@ -48,10 +48,9 @@ func TestLocalnetCreateEnvInitContainerBuildsFragment(t *testing.T) { // TestCardanoTestnetImageHonorsInjectedOverride verifies the Reconciler- // injected defaultCardanoTestnetImage replaces the legacy // ":-" reference on the create-env init -// container, the faucet source-address init container, and the default -// cardano-node container. This is the seam the local dev stack uses when -// the published cardano-testnet tag is behind publisher changes -// CardanoDBSync depends on. +// container and the default cardano-node container. This is the seam the +// local dev stack uses when the published cardano-testnet tag is behind +// publisher changes CardanoDBSync depends on. func TestCardanoTestnetImageHonorsInjectedOverride(t *testing.T) { const override = "ghcr.io/meigma/yacd/cardano-testnet:tilt" @@ -64,9 +63,6 @@ func TestCardanoTestnetImageHonorsInjectedOverride(t *testing.T) { require.NoError(t, err) assert.Equal(t, override, initContainer.Image) - addressInitContainer := builder.faucetSourceAddressInitContainer(plan) - assert.Equal(t, override, addressInitContainer.Image) - assert.Equal(t, override, builder.cardanoNodeImage(network)) } diff --git a/internal/controller/cardanonetwork/names.go b/internal/controller/cardanonetwork/names.go index c0a53956..25009083 100644 --- a/internal/controller/cardanonetwork/names.go +++ b/internal/controller/cardanonetwork/names.go @@ -17,7 +17,7 @@ func primaryWorkloadName(network *yacdv1alpha1.CardanoNetwork) string { // primaryNodeStatePVCName returns the DNS-label name of the PVC that backs the // primary node's durable state (cardano-node database, generated localnet -// environment, faucet UTXO keys). +// environment, staged network artifacts). func primaryNodeStatePVCName(network *yacdv1alpha1.CardanoNetwork) string { return ctrlnames.DNSLabelWithSuffix(network.Name, "node-state") } @@ -32,23 +32,12 @@ func primaryKupoServiceName(network *yacdv1alpha1.CardanoNetwork) string { return ctrlnames.DNSLabelWithSuffix(network.Name, "kupo") } -// primaryFaucetServiceName returns the DNS-label name of the faucet Service. -func primaryFaucetServiceName(network *yacdv1alpha1.CardanoNetwork) string { - return ctrlnames.DNSLabelWithSuffix(network.Name, "faucet") -} - // primaryArtifactsServiceName returns the DNS-label name of the artifacts // Service that exposes the cardano-tools serve sidecar. func primaryArtifactsServiceName(network *yacdv1alpha1.CardanoNetwork) string { return ctrlnames.DNSLabelWithSuffix(network.Name, "artifacts") } -// primaryFaucetAuthSecretName returns the DNS-label name of the faucet auth -// Secret that carries the API token consumed by the faucet sidecar. -func primaryFaucetAuthSecretName(network *yacdv1alpha1.CardanoNetwork) string { - return ctrlnames.DNSLabelWithSuffix(network.Name, "faucet-auth") -} - // primaryFaucetWalletSecretName returns the DNS-label name of the well-known // faucet wallet Secret. The faucet wallet is funded directly at genesis and // serves as the local funding source the CLI later spends from. diff --git a/internal/controller/cardanonetwork/readiness.go b/internal/controller/cardanonetwork/readiness.go index adb93164..e04fbfcc 100644 --- a/internal/controller/cardanonetwork/readiness.go +++ b/internal/controller/cardanonetwork/readiness.go @@ -81,17 +81,13 @@ type sidecarReadinessConfig struct { missingServiceMessage string unavailableMessage string containerNotReadyMessage string - // preReadinessCheck runs after the Service get and before the container - // readiness probe. Non-nil only for the faucet, which must also verify - // its uncached auth Secret carries a usable token. - preReadinessCheck func(ctx context.Context, network *yacdv1alpha1.CardanoNetwork) (notReady *metav1.Condition, err error) } -// primarySidecarReadyCondition is the shared body used by the three optional -// sidecars (ogmios, kupo, faucet). Each one customizes the variation through +// primarySidecarReadyCondition is the shared body used by the optional sidecars +// (ogmios, kupo). Each one customizes the variation through // sidecarReadinessConfig; the orchestration (disabled short circuit, Service -// get, optional pre-readiness check, container readiness probe, blocked -// mapping, success condition) lives here once. +// get, container readiness probe, blocked mapping, success condition) lives +// here once. func (r *CardanoNetworkReconciler) primarySidecarReadyCondition( ctx context.Context, network *yacdv1alpha1.CardanoNetwork, @@ -110,16 +106,6 @@ func (r *CardanoNetworkReconciler) primarySidecarReadyCondition( return metav1.Condition{}, err } - if cfg.preReadinessCheck != nil { - notReady, err := cfg.preReadinessCheck(ctx, network) - if err != nil { - return metav1.Condition{}, err - } - if notReady != nil { - return *notReady, nil - } - } - readiness, err := r.primaryDeploymentContainerReadiness(ctx, network, cfg.containerName) if err != nil { return metav1.Condition{}, err @@ -173,29 +159,6 @@ func (r *CardanoNetworkReconciler) primaryKupoReadyCondition( }) } -// primaryFaucetReadyCondition computes the FaucetReady condition. The -// faucet's preReadinessCheck verifies the uncached auth Secret carries a -// usable token before reporting ready. -func (r *CardanoNetworkReconciler) primaryFaucetReadyCondition( - ctx context.Context, - network *yacdv1alpha1.CardanoNetwork, - enabled bool, -) (metav1.Condition, error) { - return r.primarySidecarReadyCondition(ctx, network, enabled, sidecarReadinessConfig{ - serviceName: primaryFaucetServiceName, - containerName: faucetContainerName, - condition: faucetReadyCondition, - disabledReason: conditionReasonFaucetDisabled, - disabledMessage: conditionMessageFaucetDisabled, - readyReason: conditionReasonFaucetReady, - readyMessage: conditionMessageFaucetReady, - missingServiceMessage: "Faucet Service is missing", - unavailableMessage: "Faucet sidecar is not available", - containerNotReadyMessage: "Faucet sidecar is not ready", - preReadinessCheck: r.faucetAuthSecretReady, - }) -} - // primaryArtifactsReadyCondition computes the ArtifactsReady condition. It // mirrors the optional sidecars: the artifacts Service must exist and the // always-on serve container must be ready in the primary Deployment. The @@ -286,40 +249,6 @@ func (r *CardanoNetworkReconciler) primaryDBSyncAttachmentReadyCondition( ), nil } -// faucetAuthSecretReady is the faucet's preReadinessCheck. It reads the -// uncached auth Secret and returns a non-ready FaucetReady condition when -// the Secret is missing or carries an invalid token; returns (nil, nil) when -// the Secret is healthy and the caller should proceed to the container -// readiness probe. -func (r *CardanoNetworkReconciler) faucetAuthSecretReady( - ctx context.Context, - network *yacdv1alpha1.CardanoNetwork, -) (*metav1.Condition, error) { - // Secrets are not cached; liveReader bypasses the manager cache. - secret := &corev1.Secret{} - if err := r.liveReader().Get(ctx, client.ObjectKey{Namespace: network.Namespace, Name: primaryFaucetAuthSecretName(network)}, secret); err != nil { - if apierrors.IsNotFound(err) { - blocked := faucetReadyCondition( - metav1.ConditionFalse, - conditionReasonPrimaryWorkloadMissing, - "Faucet auth Secret is missing", - ) - return &blocked, nil - } - return nil, err - } - if !validFaucetAuthToken(string(secret.Data[faucetAuthTokenKey])) { - blocked := faucetReadyCondition( - metav1.ConditionFalse, - conditionReasonDeploymentProgressing, - "Faucet auth Secret token is not ready", - ) - return &blocked, nil - } - - return nil, nil -} - // primaryDeploymentContainerReadiness returns the readiness state for a // named container on the primary Deployment. It reads the live Deployment // through the controller cache and Pod list through liveReader to avoid @@ -397,7 +326,7 @@ func primaryDeploymentContainerBlockedCondition( // liveReader is the uncached reader for status checks that must observe the // freshest cluster state. When the Reconciler was constructed with a Reader // (typical for envtest) we use it; otherwise we fall back to the cached -// Client. Status readers and faucet-auth reads must always go through this +// Client. Status readers and uncached Secret reads must always go through this // path so stale cache reads cannot stamp out a misleading status. func (r *CardanoNetworkReconciler) liveReader() client.Reader { if r.Reader != nil { diff --git a/internal/controller/cardanonetwork/resources.go b/internal/controller/cardanonetwork/resources.go index cebab4ed..869392f5 100644 --- a/internal/controller/cardanonetwork/resources.go +++ b/internal/controller/cardanonetwork/resources.go @@ -14,8 +14,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ) -// Resource-construction internals shared by the Deployment and the faucet -// auth Secret. +// Resource-construction internals shared by the Deployment and the owned +// Secrets. const ( // nodeIPCVolumeName is the EmptyDir volume cardano-node and ogmios share // for IPC socket communication. @@ -28,15 +28,6 @@ const ( // so kupo can run with a read-only root filesystem. kupoTmpVolumeName = "kupo-tmp" - // faucetAuthVolumeName is the Secret-backed volume that mounts the - // faucet's auth token into its container. - faucetAuthVolumeName = "faucet-auth" - - // faucetAuthTokenKey is the data key inside the faucet auth Secret that - // carries the token. The Secret data map is shaped {faucetAuthTokenKey: - // []byte(token)}. - faucetAuthTokenKey = "token" - // walletNameLabel marks an owned wallet Secret with its well-known name so // consumers (the CLI, dashboards) can select a specific wallet without // parsing the Secret name. The genesis-funded faucet wallet and CLI-managed @@ -58,11 +49,11 @@ const ( ) // deployment builds the primary workload Deployment. It composes the -// cardano-node container with the enabled optional sidecars (ogmios, kupo, -// faucet) and wires the init container that prepares the localnet environment. +// cardano-node container with the enabled optional sidecars (ogmios, kupo) and +// wires the init container that prepares the localnet environment. // The RecreateDeploymentStrategyType prevents two cardano-node instances // from running at once (they cannot share the underlying state PVC). -func (b primaryWorkloadBuilder) deployment(network *yacdv1alpha1.CardanoNetwork, plan primaryNetworkPlan, initContainer *corev1.Container, ogmios ogmiosSettings, kupo kupoSettings, faucet faucetSettings, faucetWallet faucetWalletSettings) (*appsv1.Deployment, error) { +func (b primaryWorkloadBuilder) deployment(network *yacdv1alpha1.CardanoNetwork, plan primaryNetworkPlan, initContainer *corev1.Container, ogmios ogmiosSettings, kupo kupoSettings, faucetWallet faucetWalletSettings) (*appsv1.Deployment, error) { selectorLabels := primaryWorkloadSelectorLabels(network) labels := primaryWorkloadLabels(network) deploymentName := primaryWorkloadName(network) @@ -73,9 +64,6 @@ func (b primaryWorkloadBuilder) deployment(network *yacdv1alpha1.CardanoNetwork, if kupo.enabled { containers = append(containers, b.kupoContainer(kupo, ogmios)) } - if faucet.enabled { - containers = append(containers, b.faucetContainer(faucet, ogmios, kupo)) - } if b.dbSyncAttachment != nil { containers = append(containers, b.dbSyncAttachment.Container) } @@ -117,9 +105,6 @@ func (b primaryWorkloadBuilder) deployment(network *yacdv1alpha1.CardanoNetwork, if b.dbSyncAttachment != nil { initContainers = append(initContainers, b.dbSyncAttachment.InitContainer) } - if faucet.enabled { - initContainers = append(initContainers, b.faucetSourceAddressInitContainer(*plan.Localnet)) - } volumes := []corev1.Volume{ { Name: localnetStateVolumeName, @@ -156,27 +141,6 @@ func (b primaryWorkloadBuilder) deployment(network *yacdv1alpha1.CardanoNetwork, }, ) } - if faucet.enabled { - // The faucet auth token Secret is always present at apply time - // because the apply orchestrator creates it before the Deployment - // rolls; Optional=false fails the Pod fast if the token disappears. - optional := false - volumes = append(volumes, corev1.Volume{ - Name: faucetAuthVolumeName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: faucet.authSecretName, - Items: []corev1.KeyToPath{ - { - Key: faucet.authSecretKey, - Path: faucet.authSecretKey, - }, - }, - Optional: &optional, - }, - }, - }) - } if b.dbSyncAttachment != nil { volumes = append(volumes, b.dbSyncAttachment.Volumes...) } @@ -356,35 +320,6 @@ func (b primaryWorkloadBuilder) kupoService(network *yacdv1alpha1.CardanoNetwork return service, nil } -// faucetService builds the optional faucet ClusterIP Service. -func (b primaryWorkloadBuilder) faucetService(network *yacdv1alpha1.CardanoNetwork, settings faucetSettings) (*corev1.Service, error) { - service := &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: primaryFaucetServiceName(network), - Namespace: network.Namespace, - Labels: primaryWorkloadLabels(network), - }, - Spec: corev1.ServiceSpec{ - Type: corev1.ServiceTypeClusterIP, - Selector: primaryWorkloadSelectorLabels(network), - Ports: []corev1.ServicePort{ - { - Name: faucetPortName, - Protocol: corev1.ProtocolTCP, - Port: settings.port, - TargetPort: intstr.FromString(faucetPortName), - }, - }, - }, - } - - if err := controllerutil.SetControllerReference(network, service, b.scheme); err != nil { - return nil, fmt.Errorf("set faucet Service owner reference: %w", err) - } - - return service, nil -} - // artifactsService builds the artifacts ClusterIP Service that exposes the // always-on cardano-tools serve sidecar. It mirrors the chain API Services: // the selector targets the primary node Pod labels and the single port maps to @@ -417,26 +352,6 @@ func (b primaryWorkloadBuilder) artifactsService(network *yacdv1alpha1.CardanoNe return service, nil } -// faucetAuthSecret builds the opaque Secret that carries the faucet's auth -// token. The data map is populated by the apply phase (the builder cannot -// generate random material since it must stay pure). -func (b primaryWorkloadBuilder) faucetAuthSecret(network *yacdv1alpha1.CardanoNetwork, settings faucetSettings) (*corev1.Secret, error) { - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: settings.authSecretName, - Namespace: network.Namespace, - Labels: primaryWorkloadLabels(network), - }, - Type: corev1.SecretTypeOpaque, - } - - if err := controllerutil.SetControllerReference(network, secret, b.scheme); err != nil { - return nil, fmt.Errorf("set faucet auth Secret owner reference: %w", err) - } - - return secret, nil -} - // faucetWalletSecret builds the opaque Secret that carries the well-known // faucet wallet's payment key envelopes and address. It shares the developer // wallet's data shape and create-once contract; the marker labels identify it diff --git a/internal/controller/cardanonetwork/settings.go b/internal/controller/cardanonetwork/settings.go index c542e2f2..bcf26b33 100644 --- a/internal/controller/cardanonetwork/settings.go +++ b/internal/controller/cardanonetwork/settings.go @@ -34,35 +34,6 @@ type kupoSettings struct { resources *corev1.ResourceRequirements } -// faucetSettings is the effective faucet sidecar configuration after applying -// CardanoNetwork spec overrides on top of the package defaults. -type faucetSettings struct { - // enabled is whether the faucet sidecar should run. Requires both ogmios - // and kupo to be enabled. - enabled bool - // image is the resolved container image reference. The repository must - // match the resolved default faucet image repository. - image string - // port is the resolved container/Service port. - port int32 - // defaultSource is the default UTXO source name used for top-ups - // (must match the utxoN format). - defaultSource string - // minTopUpLovelace bounds the per-request top-up minimum. - minTopUpLovelace int64 - // maxTopUpLovelace bounds the per-request top-up maximum. - maxTopUpLovelace int64 - // resources is an optional resource requirements override. - resources *corev1.ResourceRequirements - // authSecretName is the per-CardanoNetwork faucet auth Secret name. - authSecretName string - // authSecretKey is the data key inside the auth Secret carrying the token. - authSecretKey string - // authTokenFilePath is the in-container mount path the faucet reads its - // token from. - authTokenFilePath string -} - // resolveOgmiosSettings applies the CardanoNetwork spec on top of the package // defaults and returns the effective ogmios configuration. func resolveOgmiosSettings(network *yacdv1alpha1.CardanoNetwork) (ogmiosSettings, error) { @@ -155,91 +126,3 @@ func applyDependentDefaults(ogmios ogmiosSettings, kupo kupoSettings, kupoMentio return kupo } - -// resolveFaucetSettings applies the CardanoNetwork spec on top of the package -// defaults and returns the effective faucet configuration. The faucet -// requires both ogmios and kupo to be enabled. -func (b primaryWorkloadBuilder) resolveFaucetSettings(network *yacdv1alpha1.CardanoNetwork, ogmios ogmiosSettings, kupo kupoSettings) (faucetSettings, error) { - settings := faucetSettings{ - enabled: false, - image: b.resolvedDefaultFaucetImage(), - port: defaultFaucetPort, - defaultSource: defaultFaucetSource, - minTopUpLovelace: defaultFaucetMinLovelace, - maxTopUpLovelace: defaultFaucetMaxLovelace, - authSecretName: primaryFaucetAuthSecretName(network), - authSecretKey: faucetAuthTokenKey, - authTokenFilePath: faucetAuthTokenPath, - } - if network.Spec.ChainAPI == nil || network.Spec.ChainAPI.Faucet == nil { - return settings, nil - } - - spec := network.Spec.ChainAPI.Faucet - if !spec.Enabled { - settings.enabled = false - return settings, nil - } - settings.enabled = true - if !ogmios.enabled { - return faucetSettings{}, unsupportedSpec("faucet requires ogmios to be enabled") - } - if !kupo.enabled { - return faucetSettings{}, unsupportedSpec("faucet requires kupo to be enabled") - } - - if spec.Image != nil { - settings.image = strings.TrimSpace(*spec.Image) - } - if strings.TrimSpace(settings.image) == "" { - return faucetSettings{}, unsupportedSpec("faucet image is required") - } - // Repository pinning: even when an override is provided, it must live - // under the same OCI repository as the default. This prevents accidental - // adoption of a third-party image carrying faucet-shaped APIs. - defaultImageRepo := imageRepository(b.resolvedDefaultFaucetImage()) - if imageRepository(settings.image) != defaultImageRepo { - return faucetSettings{}, unsupportedSpec("faucet image repository must match the configured default faucet image repository %q", defaultImageRepo) - } - if spec.Port < 1 || spec.Port > 65535 { - return faucetSettings{}, unsupportedSpec("faucet port must be between 1 and 65535") - } - settings.port = spec.Port - settings.defaultSource = strings.TrimSpace(spec.DefaultSource) - if err := validateFaucetSourceName(settings.defaultSource); err != nil { - return faucetSettings{}, err - } - if spec.MinTopUpLovelace < 1 { - return faucetSettings{}, unsupportedSpec("faucet minTopUpLovelace must be greater than 0") - } - if spec.MaxTopUpLovelace < 1 { - return faucetSettings{}, unsupportedSpec("faucet maxTopUpLovelace must be greater than 0") - } - if spec.MinTopUpLovelace > spec.MaxTopUpLovelace { - return faucetSettings{}, unsupportedSpec("faucet minTopUpLovelace must not exceed maxTopUpLovelace") - } - settings.minTopUpLovelace = spec.MinTopUpLovelace - settings.maxTopUpLovelace = spec.MaxTopUpLovelace - if spec.Resources != nil { - settings.resources = spec.Resources.DeepCopy() - } - - return settings, nil -} - -// resolvedDefaultFaucetImage returns the effective default faucet image. The -// Reconciler-injected DefaultFaucetImage is the legitimate primary injection -// point for the local dev stack's ko-built image; the package constant -// defaultFaucetImage is the final fallback used only when no injected value -// is configured. -// -// This controller-side fallback is a deliberate exception to the -// "defaults live in the planner" rule because the planner does not know -// about ko-injected images at all. -func (b primaryWorkloadBuilder) resolvedDefaultFaucetImage() string { - if injected := strings.TrimSpace(b.defaultFaucetImage); injected != "" { - return injected - } - - return defaultFaucetImage -} diff --git a/internal/controller/cardanonetwork/status.go b/internal/controller/cardanonetwork/status.go index 174e010c..c9a519d6 100644 --- a/internal/controller/cardanonetwork/status.go +++ b/internal/controller/cardanonetwork/status.go @@ -10,18 +10,18 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// patchStatusConditionsClearingFaucet writes a status patch that clears the -// faucet endpoints while applying the caller-supplied conditions. Used on the -// Degraded paths (unsupported spec, apply error) where the faucet must be torn -// down and the conditions must reflect the failure reason. -func (r *CardanoNetworkReconciler) patchStatusConditionsClearingFaucet( +// patchStatusConditionsClearingRuntime writes a status patch that clears the +// runtime (sync) status while applying the caller-supplied conditions. Used on +// the Degraded paths (unsupported spec, apply error) where the runtime status +// must not lag and the conditions must reflect the failure reason. +func (r *CardanoNetworkReconciler) patchStatusConditionsClearingRuntime( ctx context.Context, network *yacdv1alpha1.CardanoNetwork, networkPlan primaryNetworkPlan, acceptedIdentity acceptedNetworkIdentity, conditions ...metav1.Condition, ) error { - return r.patchPrimaryWorkloadStatus(ctx, network, networkPlan, acceptedIdentity, nil, nil, nil, nil, nil, nil, nil, true, conditions...) + return r.patchPrimaryWorkloadStatus(ctx, network, networkPlan, acceptedIdentity, nil, nil, nil, nil, nil, true, conditions...) } // patchPrimaryWorkloadAppliedStatus computes per-component readiness for @@ -36,9 +36,7 @@ func (r *CardanoNetworkReconciler) patchPrimaryWorkloadAppliedStatus( nodeService *corev1.Service, ogmiosService *corev1.Service, kupoService *corev1.Service, - faucetService *corev1.Service, artifactsService *corev1.Service, - faucetAuthSecret *corev1.Secret, dbSyncAttached bool, dbSyncAttachmentCondition metav1.Condition, ) (metav1.Condition, error) { @@ -58,10 +56,6 @@ func (r *CardanoNetworkReconciler) patchPrimaryWorkloadAppliedStatus( if err != nil { return metav1.Condition{}, err } - faucetReady, err := r.primaryFaucetReadyCondition(ctx, network, faucetService != nil) - if err != nil { - return metav1.Condition{}, err - } // Derive ArtifactsReady from served-artifacts availability (the artifacts // Service and the always-on serve sidecar container's readiness) rather than @@ -72,11 +66,11 @@ func (r *CardanoNetworkReconciler) patchPrimaryWorkloadAppliedStatus( return metav1.Condition{}, err } syncStatus, nodeSynchronized, nodeProgressing := r.primaryNodeSyncStatusConditions(ctx, network, ogmiosService, artifactsReady.Status == metav1.ConditionTrue, artifactsReady.Message) - ready := readyCondition(dbSyncAttachmentReady, nodeReady, ogmiosReady, kupoReady, faucetReady, artifactsReady, dbSyncAttached, kupoService != nil, faucetService != nil) + ready := readyCondition(dbSyncAttachmentReady, nodeReady, ogmiosReady, kupoReady, artifactsReady, dbSyncAttached, kupoService != nil) degraded := degradedCondition(metav1.ConditionFalse, conditionReasonReconcileSucceeded, conditionMessagePrimaryWorkloadApplied) - if err := r.patchPrimaryWorkloadStatus(ctx, network, networkPlan, acceptedIdentity, nodeService, ogmiosService, kupoService, faucetService, artifactsService, faucetAuthSecret, syncStatus, false, + if err := r.patchPrimaryWorkloadStatus(ctx, network, networkPlan, acceptedIdentity, nodeService, ogmiosService, kupoService, artifactsService, syncStatus, false, degraded, progressingForReadyCondition(ready), ready, @@ -86,7 +80,6 @@ func (r *CardanoNetworkReconciler) patchPrimaryWorkloadAppliedStatus( nodeProgressing, ogmiosReady, kupoReady, - faucetReady, artifactsReady, ); err != nil { return metav1.Condition{}, err @@ -106,11 +99,9 @@ func (r *CardanoNetworkReconciler) patchPrimaryWorkloadStatus( nodeService *corev1.Service, ogmiosService *corev1.Service, kupoService *corev1.Service, - faucetService *corev1.Service, artifactsService *corev1.Service, - faucetAuthSecret *corev1.Secret, syncStatus *yacdv1alpha1.CardanoNetworkSyncStatus, - clearFaucet bool, + clearRuntime bool, conditions ...metav1.Condition, ) error { original := network.DeepCopy() @@ -119,11 +110,9 @@ func (r *CardanoNetworkReconciler) patchPrimaryWorkloadStatus( setNetworkIdentityStatus(network, networkPlan, acceptedIdentity) } if nodeService != nil { - setEndpointStatus(network, nodeService, ogmiosService, kupoService, faucetService, artifactsService) - setFaucetStatus(network, faucetAuthSecret) + setEndpointStatus(network, nodeService, ogmiosService, kupoService, artifactsService) setSyncStatus(network, syncStatus) - } else if clearFaucet { - clearFaucetStatus(network) + } else if clearRuntime { clearSyncStatus(network) } ctrlstatus.SetObserved(&network.Status.Conditions, network.Generation, conditions...) @@ -143,16 +132,6 @@ func setSyncStatus(network *yacdv1alpha1.CardanoNetwork, syncStatus *yacdv1alpha network.Status.Sync = syncStatus.DeepCopy() } -// clearFaucetStatus removes the faucet endpoint and auth secret name from -// CardanoNetwork status. Used on the Degraded path to ensure the faucet -// status does not lag the live faucet revocation. -func clearFaucetStatus(network *yacdv1alpha1.CardanoNetwork) { - if network.Status.Endpoints != nil { - network.Status.Endpoints.Faucet = nil - } - network.Status.Faucet = nil -} - // clearSyncStatus removes the sync payload from CardanoNetwork status. Used on // failure paths where retaining the previous probe result would be stale. func clearSyncStatus(network *yacdv1alpha1.CardanoNetwork) { @@ -195,7 +174,7 @@ func setNetworkIdentityStatus(network *yacdv1alpha1.CardanoNetwork, plan primary // setEndpointStatus publishes the in-cluster endpoint URLs for the primary // node-to-node Service and any enabled chain API sidecars. -func setEndpointStatus(network *yacdv1alpha1.CardanoNetwork, nodeService *corev1.Service, ogmiosService *corev1.Service, kupoService *corev1.Service, faucetService *corev1.Service, artifactsService *corev1.Service) { +func setEndpointStatus(network *yacdv1alpha1.CardanoNetwork, nodeService *corev1.Service, ogmiosService *corev1.Service, kupoService *corev1.Service, artifactsService *corev1.Service) { if network.Status.Endpoints == nil { network.Status.Endpoints = &yacdv1alpha1.CardanoNetworkEndpointsStatus{} } @@ -224,16 +203,6 @@ func setEndpointStatus(network *yacdv1alpha1.CardanoNetwork, nodeService *corev1 } } - if faucetService == nil { - network.Status.Endpoints.Faucet = nil - } else { - network.Status.Endpoints.Faucet = &yacdv1alpha1.ServiceEndpointStatus{ - ServiceName: faucetService.Name, - Port: faucetService.Spec.Ports[0].Port, - URL: fmt.Sprintf("%s://%s.%s.svc.cluster.local:%d", faucetServiceURLType, faucetService.Name, faucetService.Namespace, faucetService.Spec.Ports[0].Port), - } - } - if artifactsService == nil { network.Status.Endpoints.Artifacts = nil return @@ -245,16 +214,3 @@ func setEndpointStatus(network *yacdv1alpha1.CardanoNetwork, nodeService *corev1 URL: fmt.Sprintf("%s://%s.%s.svc.cluster.local:%d", serveServiceURLType, artifactsService.Name, artifactsService.Namespace, artifactsService.Spec.Ports[0].Port), } } - -// setFaucetStatus publishes the faucet auth Secret reference into -// CardanoNetwork status. The CLI consumes this to locate the token. -func setFaucetStatus(network *yacdv1alpha1.CardanoNetwork, faucetAuthSecret *corev1.Secret) { - if faucetAuthSecret == nil { - network.Status.Faucet = nil - return - } - - network.Status.Faucet = &yacdv1alpha1.FaucetStatus{ - AuthSecretName: faucetAuthSecret.Name, - } -} diff --git a/internal/controller/cardanonetwork/validate.go b/internal/controller/cardanonetwork/validate.go index b733525e..1b272717 100644 --- a/internal/controller/cardanonetwork/validate.go +++ b/internal/controller/cardanonetwork/validate.go @@ -25,7 +25,7 @@ func validateKupoImage(settings kupoSettings) error { // node, the optional chain API sidecars, and the always-on cardano-tools // serve sidecar. Each must claim a distinct port. serveEnabled reserves the // fixed serve port only for the networks that run the serve sidecar. -func validatePrimaryWorkloadPorts(nodePort int32, ogmios ogmiosSettings, kupo kupoSettings, faucet faucetSettings, serveEnabled bool) error { +func validatePrimaryWorkloadPorts(nodePort int32, ogmios ogmiosSettings, kupo kupoSettings, serveEnabled bool) error { seen := map[int32]string{ nodePort: cardanoNodePortName, } @@ -50,32 +50,6 @@ func validatePrimaryWorkloadPorts(nodePort int32, ogmios ogmiosSettings, kupo ku } seen[kupo.port] = kupoPortName } - if faucet.enabled { - if owner, ok := seen[faucet.port]; ok { - return unsupportedSpec("faucet port %d conflicts with %s port", faucet.port, owner) - } - } - - return nil -} - -// validateFaucetSourceName rejects faucet defaultSource values that do not -// match the utxoN format (where N is a positive integer with no leading -// zero). cardano-testnet generates the matching key directory using this -// naming, so any drift would produce a non-functional faucet. -func validateFaucetSourceName(sourceName string) error { - if !strings.HasPrefix(sourceName, "utxo") || len(sourceName) < len("utxo1") { - return unsupportedSpec("faucet defaultSource must use the utxoN source name format") - } - digits := sourceName[len("utxo"):] - if digits[0] == '0' { - return unsupportedSpec("faucet defaultSource must use the utxoN source name format") - } - for _, char := range digits { - if char < '0' || char > '9' { - return unsupportedSpec("faucet defaultSource must use the utxoN source name format") - } - } return nil } @@ -151,28 +125,6 @@ func ogmiosCompatibilityKey(image string) (string, error) { return "v" + parts[0] + "." + parts[1], nil } -// imageRepository extracts the repository portion of an OCI image reference, -// stripping any tag and digest suffix. -func imageRepository(image string) string { - image = strings.TrimSpace(image) - if image == "" { - return "" - } - if repo, _, ok := strings.Cut(image, "@"); ok { - return repo - } - // Distinguish "host:port/repo" (last colon before last slash, no tag) - // from "repo:tag" (last colon after last slash). Only the latter has a - // tag to strip. - lastSlash := strings.LastIndex(image, "/") - lastColon := strings.LastIndex(image, ":") - if lastColon > lastSlash { - return image[:lastColon] - } - - return image -} - // containerImageTag extracts the tag from an OCI image reference. It returns // false when the reference has no tag (digest-only references included). func containerImageTag(image string) (string, bool) { diff --git a/internal/controller/cardanonetwork/wallet.go b/internal/controller/cardanonetwork/wallet.go index 41793a8c..585940a8 100644 --- a/internal/controller/cardanonetwork/wallet.go +++ b/internal/controller/cardanonetwork/wallet.go @@ -72,8 +72,8 @@ type faucetWalletApplyResult struct { // known at build time because the genesis-funding init container injects it as // an env literal: editing the genesis after the node already booted from the // unfunded one would rewrite the chain under a running node. When the faucet -// wallet is not gated on (non-local or faucet disabled) it returns a disabled -// result and leaves any stale Secret for the apply phase to delete. +// wallet is not gated on (non-local networks) it returns a disabled result and +// leaves any stale Secret for the apply phase to delete. func (r *CardanoNetworkReconciler) ensurePrimaryFaucetWalletSecret( ctx context.Context, network *yacdv1alpha1.CardanoNetwork, @@ -104,11 +104,11 @@ func (r *CardanoNetworkReconciler) ensurePrimaryFaucetWalletSecret( }, nil } -// applyWalletSecret is the shared apply core for the developer and faucet -// wallets: live-read the Secret (Secrets are uncached), create it with freshly -// generated key material when absent, and preserve existing key material -// verbatim otherwise. Both wallets are funded against their derived address, so -// neither may ever regenerate the key. +// applyWalletSecret is the apply core for the faucet wallet Secret: live-read +// the Secret (Secrets are uncached), create it with freshly generated key +// material when absent, and preserve existing key material verbatim otherwise. +// The wallet is funded against its derived address, so the key may never be +// regenerated. func (r *CardanoNetworkReconciler) applyWalletSecret( ctx context.Context, desired *corev1.Secret, @@ -152,10 +152,10 @@ func (r *CardanoNetworkReconciler) createWalletSecretWithKeys( return controllerutil.OperationResultCreated, desired, nil } -// reconcileWalletSecret handles the live-Secret-exists branch. Unlike the -// faucet auth Secret, the wallet's key material is never regenerated: the wallet -// holds the funds at its derived address, so an existing wallet's Data is -// preserved verbatim and only metadata is reconciled. +// reconcileWalletSecret handles the live-Secret-exists branch. The wallet's key +// material is never regenerated: the wallet holds the funds at its derived +// address, so an existing wallet's Data is preserved verbatim and only metadata +// is reconciled. func (r *CardanoNetworkReconciler) reconcileWalletSecret( ctx context.Context, current *corev1.Secret, diff --git a/internal/controller/cardanonetwork/wallet_test.go b/internal/controller/cardanonetwork/wallet_test.go index 498d1a81..d864f3ea 100644 --- a/internal/controller/cardanonetwork/wallet_test.go +++ b/internal/controller/cardanonetwork/wallet_test.go @@ -16,23 +16,16 @@ import ( func TestResolveFaucetWalletSettings(t *testing.T) { localPlan := primaryNetworkPlan{Mode: yacdv1alpha1.CardanoNetworkModeLocal} publicPlan := primaryNetworkPlan{Mode: yacdv1alpha1.CardanoNetworkModePublic} - enabledFaucet := faucetSettings{enabled: true} - - t.Run("disabled when faucet is off", func(t *testing.T) { - network := localCardanoNetwork("fw") - settings := resolveFaucetWalletSettings(network, localPlan, faucetSettings{enabled: false}) - assert.False(t, settings.enabled) - }) t.Run("disabled on non-local networks", func(t *testing.T) { network := localCardanoNetwork("fw") - settings := resolveFaucetWalletSettings(network, publicPlan, enabledFaucet) + settings := resolveFaucetWalletSettings(network, publicPlan) assert.False(t, settings.enabled) }) - t.Run("enabled with the faucet on a local network", func(t *testing.T) { + t.Run("enabled on a local network", func(t *testing.T) { network := localCardanoNetwork("fw") - settings := resolveFaucetWalletSettings(network, localPlan, enabledFaucet) + settings := resolveFaucetWalletSettings(network, localPlan) assert.True(t, settings.enabled) assert.Equal(t, defaultFaucetWalletFundingLovelace, settings.fundingLovelace) assert.Equal(t, primaryFaucetWalletSecretName(network), settings.secretName) @@ -40,27 +33,18 @@ func TestResolveFaucetWalletSettings(t *testing.T) { } func TestFaucetWalletEnabledPredicate(t *testing.T) { - t.Run("local with faucet", func(t *testing.T) { - network := localCardanoNetwork("fw") - enableFaucet(network) - assert.True(t, faucetWalletEnabled(network)) - }) - - t.Run("local without faucet", func(t *testing.T) { - assert.False(t, faucetWalletEnabled(localCardanoNetwork("fw"))) + t.Run("local network", func(t *testing.T) { + assert.True(t, faucetWalletEnabled(localCardanoNetwork("fw"))) }) - t.Run("public with faucet flag", func(t *testing.T) { - network := publicPreviewCardanoNetwork("fw") - enableFaucet(network) - assert.False(t, faucetWalletEnabled(network)) + t.Run("public network", func(t *testing.T) { + assert.False(t, faucetWalletEnabled(publicPreviewCardanoNetwork("fw"))) }) } func TestApplyPrimaryFaucetWalletSecretCreatesOnceAndPreservesKeys(t *testing.T) { ctx := context.Background() network := localCardanoNetwork("faucet-wallet-create") - enableFaucet(network) reconciler := newTestReconciler(t, network) desired, err := (primaryWorkloadBuilder{scheme: reconciler.Scheme}).faucetWalletSecret(network, faucetWalletSettings{secretName: primaryFaucetWalletSecretName(network)}) @@ -98,7 +82,9 @@ func TestEnsurePrimaryFaucetWalletSecret(t *testing.T) { ctx := context.Background() t.Run("disabled returns no address", func(t *testing.T) { - network := localCardanoNetwork("fw-off") + // The faucet wallet is gated on local mode; a public network never + // bootstraps one. + network := publicPreviewCardanoNetwork("fw-off") reconciler := newTestReconciler(t, network) result, err := reconciler.ensurePrimaryFaucetWalletSecret(ctx, network) @@ -110,7 +96,6 @@ func TestEnsurePrimaryFaucetWalletSecret(t *testing.T) { t.Run("creates the Secret once and returns a stable address", func(t *testing.T) { network := localCardanoNetwork("fw-on") - enableFaucet(network) reconciler := newTestReconciler(t, network) first, err := reconciler.ensurePrimaryFaucetWalletSecret(ctx, network) diff --git a/moon.yml b/moon.yml index ece34c7d..bd46d0df 100644 --- a/moon.yml +++ b/moon.yml @@ -41,7 +41,6 @@ fileGroups: - 'Tiltfile' - '.dev/ctlptl.yaml' - '.dev/ko-build.sh' - - '.dev/ko-build-faucet.sh' - '.dev/scripts/dev-up.sh' - '.dev/scripts/dev-down.sh' toolchainConfig: @@ -160,7 +159,6 @@ tasks: - '@group(toolchainConfig)' - 'Dockerfile' - '.dockerignore' - - 'services/faucet/Dockerfile' - '.dev/scripts/deploy.sh' - '.dev/scripts/undeploy.sh' - '.dev/scripts/test-e2e.sh' diff --git a/services/faucet/Dockerfile b/services/faucet/Dockerfile deleted file mode 100644 index 4cf66128..00000000 --- a/services/faucet/Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -# Build the faucet binary. -FROM golang:1.26.3 AS builder -ARG TARGETOS -ARG TARGETARCH -ARG VERSION=dev -ARG COMMIT=none -ARG DATE=unknown - -WORKDIR /workspace -COPY go.mod go.mod -COPY go.sum go.sum -RUN go mod download - -COPY . . -RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -trimpath \ - -ldflags="-s -w -buildid= -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.date=${DATE}" \ - -o yacd-faucet ./services/faucet/cmd/yacd-faucet - -FROM gcr.io/distroless/static:nonroot -WORKDIR / -COPY --from=builder /workspace/yacd-faucet /yacd-faucet -USER 65532:65532 - -ENTRYPOINT ["/yacd-faucet"] diff --git a/services/faucet/cmd/yacd-faucet/main.go b/services/faucet/cmd/yacd-faucet/main.go deleted file mode 100644 index 40526268..00000000 --- a/services/faucet/cmd/yacd-faucet/main.go +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "os/signal" - "syscall" - - "github.com/meigma/yacd/services/faucet/internal/cli" -) - -//nolint:gochecknoglobals // GoReleaser injects these values with ldflags during releases. -var ( - version = "dev" - commit = "none" - date = "unknown" -) - -func main() { - os.Exit(run()) -} - -func run() int { - ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) - defer stop() - - root := cli.NewRootCommand(cli.Options{ - In: os.Stdin, - Build: cli.BuildInfo{ - Version: version, - Commit: commit, - Date: date, - }, - Out: os.Stdout, - Err: os.Stderr, - }) - if err := root.ExecuteContext(ctx); err != nil { - if _, writeErr := fmt.Fprintln(os.Stderr, err); writeErr != nil { - return 1 - } - - return 1 - } - - return 0 -} diff --git a/services/faucet/internal/cli/root.go b/services/faucet/internal/cli/root.go deleted file mode 100644 index 95b13b61..00000000 --- a/services/faucet/internal/cli/root.go +++ /dev/null @@ -1,413 +0,0 @@ -package cli - -import ( - "fmt" - "io" - "log/slog" - "net" - "os" - "strings" - "time" - "unicode" - - "github.com/meigma/yacd/internal/cardano/tx" - "github.com/meigma/yacd/services/faucet/internal/server" - "github.com/meigma/yacd/services/faucet/internal/sources" - "github.com/meigma/yacd/services/faucet/internal/topup" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/spf13/viper" -) - -const ( - defaultListenAddress = "127.0.0.1:8080" - defaultUTXOKeysDir = "/state/env/utxo-keys" - defaultSource = "utxo1" - defaultOgmiosURL = "ws://127.0.0.1:1337" - defaultKupoURL = "http://127.0.0.1:1442" - defaultAuthTokenFile = "/var/run/yacd-faucet/token" // #nosec G101 -- this is a token file path, not token material. - defaultChainRequestTimeout = 15 * time.Second - defaultTxTTLSlots = 300 - maxAuthTokenBytes = 8 * 1024 - minAuthTokenLength = 32 -) - -// BuildInfo describes linker-injected build metadata printed by --version. -type BuildInfo struct { - Version string - Commit string - Date string -} - -// Options customizes root command construction. -type Options struct { - In io.Reader - Out io.Writer - Err io.Writer - Build BuildInfo - Viper *viper.Viper - - ServerRunner func(*server.Config) error -} - -type commandContext struct { - err io.Writer - viper *viper.Viper - serverRunner func(*server.Config) error - logger *slog.Logger -} - -// RuntimeConfig is the faucet process runtime configuration. -type RuntimeConfig struct { - ListenAddress string - UTXOKeysDir string - DefaultSource string - AllowRemoteListen bool - OgmiosURL string - KupoURL string - AuthTokenFile string - MinTopUpLovelace int64 - MaxTopUpLovelace int64 - ChainRequestTimeout time.Duration - TxTTLSlots int64 - LogLevel string - LogFormat string -} - -// NewRootCommand creates the YACD faucet service command. -func NewRootCommand(options Options) *cobra.Command { - if options.In == nil { - options.In = strings.NewReader("") - } - if options.Out == nil { - options.Out = io.Discard - } - if options.Err == nil { - options.Err = io.Discard - } - if options.Viper == nil { - options.Viper = viper.New() - } - if options.ServerRunner == nil { - options.ServerRunner = func(config *server.Config) error { - return server.Run(config) - } - } - options.Build = options.Build.withDefaults() - - commandContext := &commandContext{ - err: options.Err, - viper: options.Viper, - serverRunner: options.ServerRunner, - logger: slog.New( - slog.NewTextHandler(options.Err, &slog.HandlerOptions{Level: slog.LevelInfo}), - ), - } - - root := &cobra.Command{ - Use: "yacd-faucet", - Short: "YACD faucet service", - Version: options.Build.Version, - Args: cobra.NoArgs, - SilenceUsage: true, - SilenceErrors: true, - PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { - if err := initializeConfig(cmd, commandContext.viper); err != nil { - return err - } - runtimeConfig, err := loadRuntimeConfig(commandContext.viper) - if err != nil { - return err - } - commandContext.logger = newLogger(runtimeConfig, commandContext.err) - return nil - }, - RunE: func(cmd *cobra.Command, _ []string) error { - runtimeConfig, err := loadRuntimeConfig(commandContext.viper) - if err != nil { - return err - } - authToken, err := loadAuthTokenFile(runtimeConfig.AuthTokenFile) - if err != nil { - return err - } - - sourceStore := sources.NewStore( - runtimeConfig.UTXOKeysDir, - runtimeConfig.DefaultSource, - ) - transactionSubmitter := tx.Apollo{ - OgmiosURL: runtimeConfig.OgmiosURL, - KupoURL: runtimeConfig.KupoURL, - RequestTimeout: runtimeConfig.ChainRequestTimeout, - TTLSlots: runtimeConfig.TxTTLSlots, - } - - return commandContext.serverRunner(&server.Config{ - Context: cmd.Context(), - ListenAddress: runtimeConfig.ListenAddress, - Sources: sourceStore, - TopUps: topup.NewService( - sourceStore, - transactionSubmitter, - topup.Config{ - MinLovelace: runtimeConfig.MinTopUpLovelace, - MaxLovelace: runtimeConfig.MaxTopUpLovelace, - }, - ), - AuthToken: authToken, - AuthTokenFile: runtimeConfig.AuthTokenFile, - AuthTokenLoader: loadAuthTokenFile, - Logger: commandContext.logger, - }) - }, - } - root.SetVersionTemplate( - fmt.Sprintf( - "yacd-faucet %s (%s) built %s\n", - options.Build.Version, - options.Build.Commit, - options.Build.Date, - ), - ) - root.SetIn(options.In) - root.SetOut(options.Out) - root.SetErr(options.Err) - - root.Flags().String( - "listen-address", - defaultListenAddress, - "Address for the HTTP server to listen on", - ) - root.Flags().Bool("allow-remote-listen", false, "Allow non-loopback listen addresses for intentional network exposure") - root.Flags().String("utxo-keys-dir", defaultUTXOKeysDir, "Path to the cardano-testnet utxo-keys directory") - root.Flags().String("default-source", defaultSource, "Default faucet source name") - root.Flags().String("ogmios-url", defaultOgmiosURL, "Ogmios websocket URL for transaction submission") - root.Flags().String("kupo-url", defaultKupoURL, "Kupo HTTP URL for transaction building") - root.Flags().String("auth-token-file", defaultAuthTokenFile, "Path to a file containing the bearer token required for top-up requests") - root.Flags().Int64("min-topup-lovelace", topup.DefaultMinLovelace, "Minimum lovelace accepted for one top-up") - root.Flags().Int64("max-topup-lovelace", topup.DefaultMaxLovelace, "Maximum lovelace accepted for one top-up") - root.Flags().Duration("chain-request-timeout", defaultChainRequestTimeout, "Timeout for individual chain requests") - root.Flags().Int64("tx-ttl-slots", defaultTxTTLSlots, "Transaction TTL measured in slots after the latest block") - root.Flags().String("log-level", "info", "Log level: debug, info, warn, error") - root.Flags().String("log-format", "text", "Log format: text, json") - - return root -} - -func (b BuildInfo) withDefaults() BuildInfo { - if strings.TrimSpace(b.Version) == "" { - b.Version = "dev" - } - if strings.TrimSpace(b.Commit) == "" { - b.Commit = "none" - } - if strings.TrimSpace(b.Date) == "" { - b.Date = "unknown" - } - return b -} - -func initializeConfig(cmd *cobra.Command, vp *viper.Viper) error { - vp.SetDefault("listen-address", defaultListenAddress) - vp.SetDefault("allow-remote-listen", false) - vp.SetDefault("utxo-keys-dir", defaultUTXOKeysDir) - vp.SetDefault("default-source", defaultSource) - vp.SetDefault("ogmios-url", defaultOgmiosURL) - vp.SetDefault("kupo-url", defaultKupoURL) - vp.SetDefault("auth-token-file", defaultAuthTokenFile) - vp.SetDefault("min-topup-lovelace", topup.DefaultMinLovelace) - vp.SetDefault("max-topup-lovelace", topup.DefaultMaxLovelace) - vp.SetDefault("chain-request-timeout", defaultChainRequestTimeout) - vp.SetDefault("tx-ttl-slots", defaultTxTTLSlots) - vp.SetDefault("log-level", "info") - vp.SetDefault("log-format", "text") - vp.SetEnvPrefix("YACD_FAUCET") - vp.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) - vp.AutomaticEnv() - - rootFlags := cmd.Root().Flags() - for _, flag := range []struct { - key string - name string - }{ - {key: "listen-address", name: "listen-address"}, - {key: "allow-remote-listen", name: "allow-remote-listen"}, - {key: "utxo-keys-dir", name: "utxo-keys-dir"}, - {key: "default-source", name: "default-source"}, - {key: "ogmios-url", name: "ogmios-url"}, - {key: "kupo-url", name: "kupo-url"}, - {key: "auth-token-file", name: "auth-token-file"}, - {key: "min-topup-lovelace", name: "min-topup-lovelace"}, - {key: "max-topup-lovelace", name: "max-topup-lovelace"}, - {key: "chain-request-timeout", name: "chain-request-timeout"}, - {key: "tx-ttl-slots", name: "tx-ttl-slots"}, - {key: "log-level", name: "log-level"}, - {key: "log-format", name: "log-format"}, - } { - if err := bindFlag(vp, flag.key, rootFlags.Lookup(flag.name)); err != nil { - return err - } - } - - return nil -} - -func bindFlag(vp *viper.Viper, key string, flag *pflag.Flag) error { - if flag == nil { - return fmt.Errorf("bind flag %q: flag is missing", key) - } - if err := vp.BindPFlag(key, flag); err != nil { - return fmt.Errorf("bind flag %q: %w", key, err) - } - - return nil -} - -func loadRuntimeConfig(vp *viper.Viper) (RuntimeConfig, error) { - config := RuntimeConfig{ - ListenAddress: strings.TrimSpace(vp.GetString("listen-address")), - AllowRemoteListen: vp.GetBool("allow-remote-listen"), - UTXOKeysDir: strings.TrimSpace(vp.GetString("utxo-keys-dir")), - DefaultSource: strings.TrimSpace(vp.GetString("default-source")), - OgmiosURL: strings.TrimSpace(vp.GetString("ogmios-url")), - KupoURL: strings.TrimSpace(vp.GetString("kupo-url")), - AuthTokenFile: strings.TrimSpace(vp.GetString("auth-token-file")), - MinTopUpLovelace: vp.GetInt64("min-topup-lovelace"), - MaxTopUpLovelace: vp.GetInt64("max-topup-lovelace"), - ChainRequestTimeout: vp.GetDuration("chain-request-timeout"), - TxTTLSlots: vp.GetInt64("tx-ttl-slots"), - LogLevel: strings.TrimSpace(vp.GetString("log-level")), - LogFormat: strings.TrimSpace(vp.GetString("log-format")), - } - if config.ListenAddress == "" { - return RuntimeConfig{}, fmt.Errorf("--listen-address is required") - } - if err := validateListenAddress(config.ListenAddress, config.AllowRemoteListen); err != nil { - return RuntimeConfig{}, err - } - if config.UTXOKeysDir == "" { - return RuntimeConfig{}, fmt.Errorf("--utxo-keys-dir is required") - } - if err := sources.ValidateName(config.DefaultSource); err != nil { - return RuntimeConfig{}, fmt.Errorf("invalid --default-source: %w", err) - } - if config.OgmiosURL == "" { - return RuntimeConfig{}, fmt.Errorf("--ogmios-url is required") - } - if config.KupoURL == "" { - return RuntimeConfig{}, fmt.Errorf("--kupo-url is required") - } - if config.AuthTokenFile == "" { - return RuntimeConfig{}, fmt.Errorf("--auth-token-file is required") - } - if config.MinTopUpLovelace <= 0 { - return RuntimeConfig{}, fmt.Errorf("--min-topup-lovelace must be positive") - } - if config.MaxTopUpLovelace <= 0 { - return RuntimeConfig{}, fmt.Errorf("--max-topup-lovelace must be positive") - } - if config.MinTopUpLovelace > config.MaxTopUpLovelace { - return RuntimeConfig{}, fmt.Errorf("--min-topup-lovelace must be less than or equal to --max-topup-lovelace") - } - if config.ChainRequestTimeout <= 0 { - return RuntimeConfig{}, fmt.Errorf("--chain-request-timeout must be positive") - } - if config.TxTTLSlots <= 0 { - return RuntimeConfig{}, fmt.Errorf("--tx-ttl-slots must be positive") - } - if config.LogLevel == "" { - config.LogLevel = "info" - } - if config.LogFormat == "" { - config.LogFormat = "text" - } - - switch config.LogLevel { - case "debug", "info", "warn", "error": - default: - return RuntimeConfig{}, fmt.Errorf("unsupported log level %q", config.LogLevel) - } - switch config.LogFormat { - case "text", "json": - default: - return RuntimeConfig{}, fmt.Errorf("unsupported log format %q", config.LogFormat) - } - - return config, nil -} - -func loadAuthTokenFile(path string) (string, error) { - if strings.TrimSpace(path) == "" { - return "", fmt.Errorf("--auth-token-file is required") - } - - // #nosec G304 -- the token file is an explicit operator-controlled configuration path. - file, err := os.Open(path) - if err != nil { - return "", fmt.Errorf("read --auth-token-file: %w", err) - } - defer func() { - _ = file.Close() - }() - - contents, err := io.ReadAll(io.LimitReader(file, maxAuthTokenBytes+1)) - if err != nil { - return "", fmt.Errorf("read --auth-token-file: %w", err) - } - if len(contents) > maxAuthTokenBytes { - return "", fmt.Errorf("--auth-token-file is larger than %d bytes", maxAuthTokenBytes) - } - - token := strings.TrimSpace(string(contents)) - if len(token) < minAuthTokenLength { - return "", fmt.Errorf("--auth-token-file token must be at least %d characters", minAuthTokenLength) - } - for _, character := range token { - if unicode.IsSpace(character) || unicode.IsControl(character) { - return "", fmt.Errorf("--auth-token-file token must not contain whitespace or control characters") - } - } - - return token, nil -} - -func validateListenAddress(address string, allowRemote bool) error { - host, _, err := net.SplitHostPort(address) - if err != nil { - return fmt.Errorf("invalid --listen-address: %w", err) - } - if allowRemote { - return nil - } - if host == "localhost" { - return nil - } - ip := net.ParseIP(host) - if ip != nil && ip.IsLoopback() { - return nil - } - - return fmt.Errorf("--listen-address %q is not loopback; set --allow-remote-listen for intentional network exposure", address) -} - -func newLogger(config RuntimeConfig, out io.Writer) *slog.Logger { - var level slog.Level - switch config.LogLevel { - case "debug": - level = slog.LevelDebug - case "warn": - level = slog.LevelWarn - case "error": - level = slog.LevelError - default: - level = slog.LevelInfo - } - - handlerOptions := &slog.HandlerOptions{Level: level} - if config.LogFormat == "json" { - return slog.New(slog.NewJSONHandler(out, handlerOptions)) - } - - return slog.New(slog.NewTextHandler(out, handlerOptions)) -} diff --git a/services/faucet/internal/cli/root_test.go b/services/faucet/internal/cli/root_test.go deleted file mode 100644 index ea93e7cb..00000000 --- a/services/faucet/internal/cli/root_test.go +++ /dev/null @@ -1,410 +0,0 @@ -package cli - -import ( - "bytes" - "context" - "errors" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/meigma/yacd/services/faucet/internal/server" - "github.com/meigma/yacd/services/faucet/internal/topup" - "github.com/spf13/viper" -) - -const ( - testAddress = "addr_test1vqy2n0vz5rlpykf6dcqn55xdcpey7mejyexlgj6370leayst4k6ta" - testAuthToken = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" -) - -func TestVersionFlagPrintsBuildMetadata(t *testing.T) { - t.Parallel() - - var stdout bytes.Buffer - var stderr bytes.Buffer - root := NewRootCommand(Options{ - Out: &stdout, - Err: &stderr, - Build: BuildInfo{ - Version: "0.1.0", - Commit: "abc1234", - Date: "2026-05-22T10:00:00Z", - }, - ServerRunner: func(*server.Config) error { - return errors.New("server should not run for --version") - }, - }) - root.SetArgs([]string{"--version"}) - - if err := root.ExecuteContext(context.Background()); err != nil { - t.Fatalf("ExecuteContext returned an error: %v", err) - } - if got, want := stdout.String(), "yacd-faucet 0.1.0 (abc1234) built 2026-05-22T10:00:00Z\n"; got != want { - t.Fatalf("stdout = %q, want %q", got, want) - } - if got := stderr.String(); got != "" { - t.Fatalf("stderr = %q, want empty", got) - } -} - -func TestRootCommandUsesDefaults(t *testing.T) { - t.Parallel() - - var captured *server.Config - tokenFile := writeTokenFile(t, testAuthToken) - root := NewRootCommand(Options{ - Viper: viper.New(), - ServerRunner: func(config *server.Config) error { - captured = config - return nil - }, - }) - root.SetArgs([]string{"--auth-token-file", tokenFile}) - - if err := root.ExecuteContext(context.Background()); err != nil { - t.Fatalf("ExecuteContext returned an error: %v", err) - } - if captured == nil { - t.Fatal("server did not run") - } - if got, want := captured.ListenAddress, "127.0.0.1:8080"; got != want { - t.Fatalf("listen address = %q, want %q", got, want) - } - if got, want := captured.Sources.RootDir(), "/state/env/utxo-keys"; got != want { - t.Fatalf("utxo keys dir = %q, want %q", got, want) - } - if got, want := captured.Sources.DefaultName(), "utxo1"; got != want { - t.Fatalf("default source = %q, want %q", got, want) - } - if got, want := captured.AuthToken, testAuthToken; got != want { - t.Fatalf("auth token = %q, want %q", got, want) - } - _, err := captured.TopUps.Submit(context.Background(), topup.Request{ - DestinationAddress: testAddress, - Lovelace: topup.DefaultMaxLovelace + 1, - }) - if err == nil { - t.Fatal("top-up over default max succeeded, want error") - } - assertInvalidTopUpCode(t, err) - _, err = captured.TopUps.Submit(context.Background(), topup.Request{ - DestinationAddress: testAddress, - Lovelace: topup.DefaultMinLovelace - 1, - }) - if err == nil { - t.Fatal("top-up below default min succeeded, want error") - } - assertInvalidTopUpCode(t, err) -} - -func TestRootCommandReadsEnvironment(t *testing.T) { - tokenFile := writeTokenFile(t, "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") - t.Setenv("YACD_FAUCET_LISTEN_ADDRESS", "127.0.0.1:9090") - t.Setenv("YACD_FAUCET_UTXO_KEYS_DIR", "/custom/utxo-keys") - t.Setenv("YACD_FAUCET_DEFAULT_SOURCE", "utxo2") - t.Setenv("YACD_FAUCET_OGMIOS_URL", "ws://127.0.0.1:9999") - t.Setenv("YACD_FAUCET_KUPO_URL", "http://127.0.0.1:9998") - t.Setenv("YACD_FAUCET_AUTH_TOKEN_FILE", tokenFile) - t.Setenv("YACD_FAUCET_MIN_TOPUP_LOVELACE", "50") - t.Setenv("YACD_FAUCET_MAX_TOPUP_LOVELACE", "100") - t.Setenv("YACD_FAUCET_CHAIN_REQUEST_TIMEOUT", "2s") - t.Setenv("YACD_FAUCET_TX_TTL_SLOTS", "42") - t.Setenv("YACD_FAUCET_LOG_LEVEL", "debug") - t.Setenv("YACD_FAUCET_LOG_FORMAT", "json") - - var captured *server.Config - root := NewRootCommand(Options{ - Viper: viper.New(), - ServerRunner: func(config *server.Config) error { - captured = config - return nil - }, - }) - root.SetArgs([]string{}) - - if err := root.ExecuteContext(context.Background()); err != nil { - t.Fatalf("ExecuteContext returned an error: %v", err) - } - if got, want := captured.ListenAddress, "127.0.0.1:9090"; got != want { - t.Fatalf("listen address = %q, want %q", got, want) - } - if got, want := captured.Sources.RootDir(), "/custom/utxo-keys"; got != want { - t.Fatalf("utxo keys dir = %q, want %q", got, want) - } - if got, want := captured.Sources.DefaultName(), "utxo2"; got != want { - t.Fatalf("default source = %q, want %q", got, want) - } - if got, want := captured.AuthToken, "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; got != want { - t.Fatalf("auth token = %q, want %q", got, want) - } - _, err := captured.TopUps.Submit(context.Background(), topup.Request{ - DestinationAddress: testAddress, - Lovelace: 101, - }) - if err == nil { - t.Fatal("top-up over env max succeeded, want error") - } - assertInvalidTopUpCode(t, err) - _, err = captured.TopUps.Submit(context.Background(), topup.Request{ - DestinationAddress: testAddress, - Lovelace: 49, - }) - if err == nil { - t.Fatal("top-up below env min succeeded, want error") - } - assertInvalidTopUpCode(t, err) -} - -func TestRootCommandAllowsRemoteListenWhenExplicit(t *testing.T) { - t.Parallel() - - var captured *server.Config - tokenFile := writeTokenFile(t, testAuthToken) - root := NewRootCommand(Options{ - Viper: viper.New(), - ServerRunner: func(config *server.Config) error { - captured = config - return nil - }, - }) - root.SetArgs([]string{"--listen-address", "0.0.0.0:8080", "--allow-remote-listen", "--auth-token-file", tokenFile}) - - if err := root.ExecuteContext(context.Background()); err != nil { - t.Fatalf("ExecuteContext returned an error: %v", err) - } - if captured == nil { - t.Fatal("server did not run") - } - if got, want := captured.ListenAddress, "0.0.0.0:8080"; got != want { - t.Fatalf("listen address = %q, want %q", got, want) - } -} - -func TestRootCommandRejectsInvalidTopUpConfig(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - args []string - want string - }{ - { - name: "remote listen without opt-in", - args: []string{"--listen-address", "0.0.0.0:8080"}, - want: "not loopback", - }, - { - name: "missing Ogmios URL", - args: []string{"--ogmios-url", ""}, - want: "--ogmios-url is required", - }, - { - name: "missing Kupo URL", - args: []string{"--kupo-url", ""}, - want: "--kupo-url is required", - }, - { - name: "invalid max top-up", - args: []string{"--max-topup-lovelace", "0"}, - want: "--max-topup-lovelace must be positive", - }, - { - name: "invalid min top-up", - args: []string{"--min-topup-lovelace", "0"}, - want: "--min-topup-lovelace must be positive", - }, - { - name: "min exceeds max", - args: []string{"--min-topup-lovelace", "101", "--max-topup-lovelace", "100"}, - want: "--min-topup-lovelace must be less than or equal to --max-topup-lovelace", - }, - { - name: "invalid chain timeout", - args: []string{"--chain-request-timeout", "0s"}, - want: "--chain-request-timeout must be positive", - }, - { - name: "invalid tx ttl", - args: []string{"--tx-ttl-slots", "0"}, - want: "--tx-ttl-slots must be positive", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - root := NewRootCommand(Options{ - Viper: viper.New(), - ServerRunner: func(*server.Config) error { - return errors.New("server should not run with invalid top-up config") - }, - }) - root.SetArgs(tt.args) - - err := root.ExecuteContext(context.Background()) - if err == nil { - t.Fatal("ExecuteContext succeeded, want config error") - } - if got := err.Error(); !strings.Contains(got, tt.want) { - t.Fatalf("error = %q, want %q", got, tt.want) - } - }) - } -} - -func TestRootCommandRejectsInvalidAuthTokenFile(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - path func(t *testing.T) string - want string - }{ - { - name: "missing", - path: func(t *testing.T) string { - t.Helper() - return filepath.Join(t.TempDir(), "missing-token") - }, - want: "read --auth-token-file", - }, - { - name: "directory", - path: func(t *testing.T) string { - t.Helper() - return t.TempDir() - }, - want: "read --auth-token-file", - }, - { - name: "empty", - path: func(t *testing.T) string { - t.Helper() - return writeTokenFile(t, "") - }, - want: "at least 32 characters", - }, - { - name: "short", - path: func(t *testing.T) string { - t.Helper() - return writeTokenFile(t, "short") - }, - want: "at least 32 characters", - }, - { - name: "embedded whitespace", - path: func(t *testing.T) string { - t.Helper() - return writeTokenFile(t, "aaaaaaaaaaaaaaaa aaaaaaaaaaaaaaa") - }, - want: "must not contain whitespace", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - root := NewRootCommand(Options{ - Viper: viper.New(), - ServerRunner: func(*server.Config) error { - return errors.New("server should not run with invalid auth token") - }, - }) - root.SetArgs([]string{"--auth-token-file", tt.path(t)}) - - err := root.ExecuteContext(context.Background()) - if err == nil { - t.Fatal("ExecuteContext succeeded, want token error") - } - if got := err.Error(); !strings.Contains(got, tt.want) { - t.Fatalf("error = %q, want %q", got, tt.want) - } - }) - } -} - -func TestRootCommandRejectsInvalidLogLevel(t *testing.T) { - t.Parallel() - - root := NewRootCommand(Options{ - Viper: viper.New(), - ServerRunner: func(*server.Config) error { - return errors.New("server should not run with invalid log level") - }, - }) - root.SetArgs([]string{"--log-level", "trace"}) - - err := root.ExecuteContext(context.Background()) - if err == nil { - t.Fatal("ExecuteContext succeeded, want log level error") - } - if got := err.Error(); !strings.Contains(got, `unsupported log level "trace"`) { - t.Fatalf("error = %q, want unsupported log level", got) - } -} - -func TestRootCommandRejectsInvalidLogFormat(t *testing.T) { - t.Parallel() - - root := NewRootCommand(Options{ - Viper: viper.New(), - ServerRunner: func(*server.Config) error { - return errors.New("server should not run with invalid log format") - }, - }) - root.SetArgs([]string{"--log-format", "pretty"}) - - err := root.ExecuteContext(context.Background()) - if err == nil { - t.Fatal("ExecuteContext succeeded, want log format error") - } - if got := err.Error(); !strings.Contains(got, `unsupported log format "pretty"`) { - t.Fatalf("error = %q, want unsupported log format", got) - } -} - -func TestRootCommandRejectsUnexpectedArgs(t *testing.T) { - t.Parallel() - - root := NewRootCommand(Options{ - Viper: viper.New(), - ServerRunner: func(*server.Config) error { - return errors.New("server should not run with unexpected args") - }, - }) - root.SetArgs([]string{"unexpected"}) - - err := root.ExecuteContext(context.Background()) - if err == nil { - t.Fatal("ExecuteContext succeeded, want argument error") - } - if got := err.Error(); !strings.Contains(got, `unknown command "unexpected"`) { - t.Fatalf("error = %q, want unexpected arg message", got) - } -} - -func writeTokenFile(t *testing.T, token string) string { - t.Helper() - - path := filepath.Join(t.TempDir(), "token") - if err := os.WriteFile(path, []byte(token), 0o600); err != nil { - t.Fatalf("write token file: %v", err) - } - - return path -} - -func assertInvalidTopUpCode(t *testing.T, err error) { - t.Helper() - - var topupErr *topup.Error - if !errors.As(err, &topupErr) { - t.Fatalf("error = %v, want top-up error", err) - } - if topupErr.Code != topup.CodeInvalidRequest { - t.Fatalf("top-up error code = %q, want %q", topupErr.Code, topup.CodeInvalidRequest) - } -} diff --git a/services/faucet/internal/server/server.go b/services/faucet/internal/server/server.go deleted file mode 100644 index 5ee0ad5f..00000000 --- a/services/faucet/internal/server/server.go +++ /dev/null @@ -1,439 +0,0 @@ -package server - -import ( - "context" - "crypto/sha256" - "crypto/subtle" - "encoding/json" - "errors" - "fmt" - "io" - "log/slog" - "mime" - "net/http" - "net/url" - "strings" - "time" - - "github.com/meigma/yacd/services/faucet/internal/sources" - "github.com/meigma/yacd/services/faucet/internal/topup" -) - -const ( - codeMethodNotAllowed = "method_not_allowed" - codeNotFound = "not_found" - codeNotReady = "not_ready" - codeInternalError = "internal_error" - codeUnauthorized = "unauthorized" - codeUnsupportedMedia = "unsupported_media_type" - maxTopUpBodyBytes = 4 * 1024 - requiredTopUpMediaType = "application/json" - - faucetReadHeaderTimeout = 5 * time.Second - faucetReadTimeout = 10 * time.Second - faucetWriteTimeout = 30 * time.Second - faucetIdleTimeout = 60 * time.Second -) - -// Config describes the faucet HTTP server runtime configuration. -type Config struct { - Context context.Context - ListenAddress string - Sources sources.Store - TopUps topup.Service - AuthToken string - AuthTokenFile string - AuthTokenLoader func(string) (string, error) - Logger *slog.Logger -} - -type handler struct { - sources sources.Store - topups topup.Service - authTokenSource func() (string, error) - logger *slog.Logger -} - -type statusResponse struct { - Status string `json:"status"` -} - -type errorResponse struct { - Error responseError `json:"error"` -} - -type responseError struct { - Code string `json:"code"` - Message string `json:"message"` -} - -type topUpRequest struct { - Address string `json:"address"` - Lovelace *int64 `json:"lovelace"` - Source string `json:"source,omitempty"` -} - -// NewHandler builds the faucet HTTP handler. -func NewHandler(store sources.Store, topups topup.Service, authToken string, logger *slog.Logger) http.Handler { - if logger == nil { - logger = slog.Default() - } - - return &handler{ - sources: store, - topups: topups, - authTokenSource: func() (string, error) { - return authToken, nil - }, - logger: logger, - } -} - -// NewHandlerWithAuthTokenFile builds a handler that reloads the auth token file -// for each mutating request. -func NewHandlerWithAuthTokenFile( - store sources.Store, - topups topup.Service, - authTokenFile string, - loadAuthToken func(string) (string, error), - logger *slog.Logger, -) http.Handler { - if logger == nil { - logger = slog.Default() - } - if loadAuthToken == nil { - loadAuthToken = func(string) (string, error) { - return "", fmt.Errorf("auth token loader is not configured") - } - } - - return &handler{ - sources: store, - topups: topups, - authTokenSource: func() (string, error) { - return loadAuthToken(authTokenFile) - }, - logger: logger, - } -} - -// Run starts the faucet HTTP server and gracefully shuts it down when the -// configured context is canceled. -func Run(config *Config) error { - if config.Context == nil { - config.Context = context.Background() - } - if config.Logger == nil { - config.Logger = slog.Default() - } - - httpServer := newHTTPServer(config) - - errCh := make(chan error, 1) - go func() { - err := httpServer.ListenAndServe() - if errors.Is(err, http.ErrServerClosed) { - errCh <- nil - return - } - errCh <- err - }() - - select { - case <-config.Context.Done(): - shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := httpServer.Shutdown(shutdownCtx); err != nil { - return fmt.Errorf("shutdown faucet server: %w", err) - } - if err := <-errCh; err != nil { - return fmt.Errorf("serve faucet: %w", err) - } - - return nil - case err := <-errCh: - if err != nil { - return fmt.Errorf("serve faucet: %w", err) - } - - return nil - } -} - -func newHTTPServer(config *Config) *http.Server { - return &http.Server{ - Addr: config.ListenAddress, - Handler: newConfiguredHandler(config), - ReadHeaderTimeout: faucetReadHeaderTimeout, - ReadTimeout: faucetReadTimeout, - WriteTimeout: faucetWriteTimeout, - IdleTimeout: faucetIdleTimeout, - } -} - -func newConfiguredHandler(config *Config) http.Handler { - if strings.TrimSpace(config.AuthTokenFile) != "" { - return NewHandlerWithAuthTokenFile( - config.Sources, - config.TopUps, - config.AuthTokenFile, - config.AuthTokenLoader, - config.Logger, - ) - } - - return NewHandler(config.Sources, config.TopUps, config.AuthToken, config.Logger) -} - -func (h *handler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - switch { - case request.URL.Path == "/healthz": - h.handleHealth(writer, request) - case request.URL.Path == "/readyz": - h.handleReady(writer, request) - case request.URL.Path == "/v1/sources": - h.handleSourceList(writer, request) - case request.URL.Path == "/v1/topups": - h.handleTopUp(writer, request) - case strings.HasPrefix(request.URL.Path, "/v1/sources/"): - h.handleSource(writer, request) - default: - writeError(writer, http.StatusNotFound, codeNotFound, "route was not found") - } -} - -func (h *handler) handleHealth(writer http.ResponseWriter, request *http.Request) { - if !requireGet(writer, request) { - return - } - - writeJSON(writer, http.StatusOK, statusResponse{Status: "ok"}) -} - -func (h *handler) handleReady(writer http.ResponseWriter, request *http.Request) { - if !requireGet(writer, request) { - return - } - - if err := h.sources.Ready(); err != nil { - writeError(writer, http.StatusServiceUnavailable, codeNotReady, err.Error()) - return - } - - writeJSON(writer, http.StatusOK, statusResponse{Status: "ok"}) -} - -func (h *handler) handleSourceList(writer http.ResponseWriter, request *http.Request) { - if !requireGet(writer, request) { - return - } - - list, err := h.sources.List() - if err != nil { - h.writeSourceError(writer, err) - return - } - - writeJSON(writer, http.StatusOK, list) -} - -func (h *handler) handleSource(writer http.ResponseWriter, request *http.Request) { - if !requireGet(writer, request) { - return - } - - name, err := url.PathUnescape(strings.TrimPrefix(request.URL.Path, "/v1/sources/")) - if err != nil { - writeError(writer, http.StatusBadRequest, sources.CodeInvalidSourceName, "source name is invalid") - return - } - - source, err := h.sources.Get(name) - if err != nil { - h.writeSourceError(writer, err) - return - } - - writeJSON(writer, http.StatusOK, source) -} - -func (h *handler) handleTopUp(writer http.ResponseWriter, request *http.Request) { - if !requireMethod(writer, request, http.MethodPost) { - return - } - if !h.requireAuth(writer, request) { - return - } - if !requireJSONContentType(writer, request) { - return - } - - var body topUpRequest - if err := decodeRequestBody(writer, request, &body); err != nil { - writeError(writer, http.StatusBadRequest, topup.CodeInvalidRequest, err.Error()) - return - } - if body.Lovelace == nil { - writeError(writer, http.StatusBadRequest, topup.CodeInvalidRequest, "lovelace is required") - return - } - - result, err := h.topups.Submit(request.Context(), topup.Request{ - Source: body.Source, - DestinationAddress: body.Address, - Lovelace: *body.Lovelace, - }) - if err != nil { - h.writeTopUpError(writer, err) - return - } - - writeJSON(writer, http.StatusOK, result) -} - -func (h *handler) requireAuth(writer http.ResponseWriter, request *http.Request) bool { - authToken, err := h.authTokenSource() - if err != nil { - h.logger.Error("Top-up auth token could not be loaded", "error", err) - writeError(writer, http.StatusInternalServerError, codeInternalError, "top-up auth is not configured") - return false - } - if strings.TrimSpace(authToken) == "" { - h.logger.Error("Top-up auth token is not configured") - writeError(writer, http.StatusInternalServerError, codeInternalError, "top-up auth is not configured") - return false - } - - authorization := request.Header.Get("Authorization") - fields := strings.Fields(authorization) - if len(fields) != 2 || !strings.EqualFold(fields[0], "Bearer") { - writeUnauthorized(writer) - return false - } - - expected := sha256.Sum256([]byte(authToken)) - got := sha256.Sum256([]byte(fields[1])) - if subtle.ConstantTimeCompare(expected[:], got[:]) != 1 { - writeUnauthorized(writer) - return false - } - - return true -} - -func (h *handler) writeSourceError(writer http.ResponseWriter, err error) { - var sourceErr *sources.Error - if !errors.As(err, &sourceErr) { - h.logger.Error("Unhandled source error", "error", err) - writeError(writer, http.StatusInternalServerError, codeInternalError, "source error") - return - } - - switch sourceErr.Code { - case sources.CodeInvalidSourceName: - writeError(writer, http.StatusBadRequest, sourceErr.Code, sourceErr.Message) - case sources.CodeSourceNotFound: - writeError(writer, http.StatusNotFound, sourceErr.Code, sourceErr.Message) - case sources.CodeSourceIncomplete: - writeError(writer, http.StatusNotFound, sources.CodeSourceNotFound, sourceErr.Message) - default: - writeError(writer, http.StatusInternalServerError, sourceErr.Code, sourceErr.Message) - } -} - -func (h *handler) writeTopUpError(writer http.ResponseWriter, err error) { - var topUpErr *topup.Error - if !errors.As(err, &topUpErr) { - h.logger.Error("Unhandled top-up error", "error", err) - writeError(writer, http.StatusInternalServerError, codeInternalError, "top-up error") - return - } - - switch topUpErr.Code { - case topup.CodeInvalidRequest: - writeError(writer, http.StatusBadRequest, topUpErr.Code, topUpErr.Message) - case topup.CodeSourceNotFound: - writeError(writer, http.StatusNotFound, topUpErr.Code, topUpErr.Message) - case topup.CodeSourceUnavailable, topup.CodeChainUnavailable: - writeError(writer, http.StatusServiceUnavailable, topUpErr.Code, topUpErr.Message) - default: - h.logger.Error("Unhandled structured top-up error", "code", topUpErr.Code, "error", err) - writeError(writer, http.StatusInternalServerError, codeInternalError, topUpErr.Message) - } -} - -func requireGet(writer http.ResponseWriter, request *http.Request) bool { - return requireMethod(writer, request, http.MethodGet) -} - -func requireMethod(writer http.ResponseWriter, request *http.Request, method string) bool { - if request.Method == method { - return true - } - - writer.Header().Set("Allow", method) - writeError( - writer, - http.StatusMethodNotAllowed, - codeMethodNotAllowed, - fmt.Sprintf("method %s is not allowed", request.Method), - ) - - return false -} - -func requireJSONContentType(writer http.ResponseWriter, request *http.Request) bool { - contentType := request.Header.Get("Content-Type") - if contentType == "" { - writeError(writer, http.StatusUnsupportedMediaType, codeUnsupportedMedia, "Content-Type must be application/json") - return false - } - - mediaType, _, err := mime.ParseMediaType(contentType) - if err != nil { - writeError(writer, http.StatusUnsupportedMediaType, codeUnsupportedMedia, "Content-Type must be application/json") - return false - } - if mediaType != requiredTopUpMediaType { - writeError(writer, http.StatusUnsupportedMediaType, codeUnsupportedMedia, "Content-Type must be application/json") - return false - } - - return true -} - -func decodeRequestBody(writer http.ResponseWriter, request *http.Request, target any) error { - reader := http.MaxBytesReader(writer, request.Body, maxTopUpBodyBytes) - decoder := json.NewDecoder(reader) - decoder.DisallowUnknownFields() - if err := decoder.Decode(target); err != nil { - return fmt.Errorf("decode JSON request body: %w", err) - } - if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) { - return errors.New("decode JSON request body: multiple JSON values are not allowed") - } - - return nil -} - -func writeJSON(writer http.ResponseWriter, statusCode int, body any) { - writer.Header().Set("Content-Type", "application/json") - writer.WriteHeader(statusCode) - if err := json.NewEncoder(writer).Encode(body); err != nil { - slog.Default().Error("Failed to write JSON response", "error", err) - } -} - -func writeError(writer http.ResponseWriter, statusCode int, code string, message string) { - writeJSON(writer, statusCode, errorResponse{ - Error: responseError{ - Code: code, - Message: message, - }, - }) -} - -func writeUnauthorized(writer http.ResponseWriter) { - writer.Header().Set("WWW-Authenticate", "Bearer") - writeError(writer, http.StatusUnauthorized, codeUnauthorized, "top-up request is unauthorized") -} diff --git a/services/faucet/internal/server/server_test.go b/services/faucet/internal/server/server_test.go deleted file mode 100644 index 3abc327b..00000000 --- a/services/faucet/internal/server/server_test.go +++ /dev/null @@ -1,616 +0,0 @@ -package server - -import ( - "bytes" - "context" - "crypto/ed25519" - "encoding/hex" - "encoding/json" - "errors" - "log/slog" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/meigma/yacd/internal/cardano/tx" - "github.com/meigma/yacd/services/faucet/internal/sources" - "github.com/meigma/yacd/services/faucet/internal/topup" - "github.com/stretchr/testify/assert" -) - -const ( - testDefaultSource = "utxo1" - testAuthToken = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - testInputKey = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:0" -) - -var ( - testSigningRawKeyHex = strings.Repeat("01", 32) - testVerificationRawKeyHex = deriveTestVerificationKeyHex(testSigningRawKeyHex) - testVerificationKeyCBORHex = "5820" + testVerificationRawKeyHex - testSigningKeyCBORHex = "5820" + testSigningRawKeyHex - testSourceAddress = mustDeriveTestnetPaymentAddress(testVerificationRawKeyHex) - testDestinationAddress = mustDeriveTestnetPaymentAddress(deriveTestVerificationKeyHex(strings.Repeat("02", 32))) -) - -func TestNewHTTPServerSetsBoundedTimeouts(t *testing.T) { - t.Parallel() - - server := newHTTPServer(&Config{ListenAddress: "127.0.0.1:0"}) - - assert.Equal(t, "127.0.0.1:0", server.Addr) - assert.Equal(t, faucetReadHeaderTimeout, server.ReadHeaderTimeout) - assert.Equal(t, faucetReadTimeout, server.ReadTimeout) - assert.Equal(t, faucetWriteTimeout, server.WriteTimeout) - assert.Equal(t, faucetIdleTimeout, server.IdleTimeout) - assert.NotNil(t, server.Handler) -} - -func TestHandlerHealth(t *testing.T) { - t.Parallel() - - response := performRequest(t, testHandler(t, testDefaultSource), http.MethodGet, "/healthz") - if response.Code != http.StatusOK { - t.Fatalf("status = %d, want %d", response.Code, http.StatusOK) - } - - var body statusResponse - decodeResponse(t, response, &body) - if body.Status != "ok" { - t.Fatalf("status body = %q, want ok", body.Status) - } -} - -func TestHandlerReady(t *testing.T) { - t.Parallel() - - response := performRequest(t, testHandler(t, testDefaultSource), http.MethodGet, "/readyz") - if response.Code != http.StatusOK { - t.Fatalf("status = %d, want %d", response.Code, http.StatusOK) - } -} - -func TestHandlerReadyReportsMissingDefault(t *testing.T) { - t.Parallel() - - response := performRequest(t, testHandler(t, "utxo9"), http.MethodGet, "/readyz") - if response.Code != http.StatusServiceUnavailable { - t.Fatalf("status = %d, want %d", response.Code, http.StatusServiceUnavailable) - } - - var body errorResponse - decodeResponse(t, response, &body) - if body.Error.Code != codeNotReady { - t.Fatalf("error code = %q, want %q", body.Error.Code, codeNotReady) - } -} - -func TestHandlerListsSources(t *testing.T) { - t.Parallel() - - response := performRequest(t, testHandler(t, testDefaultSource), http.MethodGet, "/v1/sources") - if response.Code != http.StatusOK { - t.Fatalf("status = %d, want %d", response.Code, http.StatusOK) - } - - var body sources.List - decodeResponse(t, response, &body) - if body.DefaultSource != testDefaultSource { - t.Fatalf("defaultSource = %q, want utxo1", body.DefaultSource) - } - if len(body.Sources) != 2 { - t.Fatalf("sources length = %d, want 2", len(body.Sources)) - } - if body.Sources[0].Name != testDefaultSource || !body.Sources[0].Default { - t.Fatalf("first source = %#v, want default utxo1", body.Sources[0]) - } -} - -func TestHandlerReturnsOneSource(t *testing.T) { - t.Parallel() - - response := performRequest(t, testHandler(t, testDefaultSource), http.MethodGet, "/v1/sources/utxo2") - if response.Code != http.StatusOK { - t.Fatalf("status = %d, want %d", response.Code, http.StatusOK) - } - - var body sources.Source - decodeResponse(t, response, &body) - if body.Name != "utxo2" { - t.Fatalf("source name = %q, want utxo2", body.Name) - } -} - -func TestHandlerReturnsSourceNotFound(t *testing.T) { - t.Parallel() - - response := performRequest(t, testHandler(t, testDefaultSource), http.MethodGet, "/v1/sources/utxo9") - if response.Code != http.StatusNotFound { - t.Fatalf("status = %d, want %d", response.Code, http.StatusNotFound) - } - - var body errorResponse - decodeResponse(t, response, &body) - if body.Error.Code != sources.CodeSourceNotFound { - t.Fatalf("error code = %q, want %q", body.Error.Code, sources.CodeSourceNotFound) - } -} - -func TestHandlerSubmitsTopUp(t *testing.T) { - t.Parallel() - - submitter := &fakeSubmitter{result: tx.Result{TxID: "abc123", SpentInputKeys: []string{testInputKey}}} - response := performTopUpRequest( - t, - testHandlerWithSubmitter(t, testDefaultSource, submitter), - `{"address":"`+testDestinationAddress+`","lovelace":1000000}`, - ) - if response.Code != http.StatusOK { - t.Fatalf("status = %d, want %d: %s", response.Code, http.StatusOK, response.Body.String()) - } - - var body topup.Result - decodeResponse(t, response, &body) - if body.TxID != "abc123" { - t.Fatalf("txId = %q, want abc123", body.TxID) - } - if body.Source != testDefaultSource { - t.Fatalf("source = %q, want utxo1", body.Source) - } - if body.DestinationAddress != testDestinationAddress { - t.Fatalf("destinationAddress = %q, want %q", body.DestinationAddress, testDestinationAddress) - } - if len(submitter.requests) != 1 { - t.Fatalf("submitter requests = %d, want 1", len(submitter.requests)) - } - if submitter.requests[0].Lovelace != 1_000_000 { - t.Fatalf("submitted lovelace = %d, want 1000000", submitter.requests[0].Lovelace) - } -} - -func TestHandlerRejectsMalformedTopUpJSON(t *testing.T) { - t.Parallel() - - response := performTopUpRequest( - t, - testHandler(t, testDefaultSource), - `{"address":`, - ) - if response.Code != http.StatusBadRequest { - t.Fatalf("status = %d, want %d", response.Code, http.StatusBadRequest) - } - - var body errorResponse - decodeResponse(t, response, &body) - if body.Error.Code != topup.CodeInvalidRequest { - t.Fatalf("error code = %q, want %q", body.Error.Code, topup.CodeInvalidRequest) - } -} - -func TestHandlerRejectsUnknownTopUpFields(t *testing.T) { - t.Parallel() - - response := performTopUpRequest( - t, - testHandler(t, testDefaultSource), - `{"address":"`+testDestinationAddress+`","lovelace":1,"extra":true}`, - ) - if response.Code != http.StatusBadRequest { - t.Fatalf("status = %d, want %d", response.Code, http.StatusBadRequest) - } -} - -func TestHandlerRejectsTopUpUnsupportedMethod(t *testing.T) { - t.Parallel() - - response := performRequest(t, testHandler(t, testDefaultSource), http.MethodGet, "/v1/topups") - if response.Code != http.StatusMethodNotAllowed { - t.Fatalf("status = %d, want %d", response.Code, http.StatusMethodNotAllowed) - } - if got, want := response.Header().Get("Allow"), http.MethodPost; got != want { - t.Fatalf("Allow = %q, want %q", got, want) - } -} - -func TestHandlerTopUpRequiresBearerAuth(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - authorization string - }{ - {name: "missing"}, - {name: "wrong scheme", authorization: "Basic " + testAuthToken}, - {name: "wrong token", authorization: "Bearer wrong"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - response := performRawRequestBody( - t, - testHandler(t, testDefaultSource), - http.MethodPost, - "/v1/topups", - `{"address":"`+testDestinationAddress+`","lovelace":1000000}`, - map[string]string{ - "Authorization": tt.authorization, - "Content-Type": "application/json", - }, - ) - - if response.Code != http.StatusUnauthorized { - t.Fatalf("status = %d, want %d", response.Code, http.StatusUnauthorized) - } - if got, want := response.Header().Get("WWW-Authenticate"), "Bearer"; got != want { - t.Fatalf("WWW-Authenticate = %q, want %q", got, want) - } - var body errorResponse - decodeResponse(t, response, &body) - if body.Error.Code != codeUnauthorized { - t.Fatalf("error code = %q, want %q", body.Error.Code, codeUnauthorized) - } - }) - } -} - -func TestHandlerTopUpRequiresJSONContentType(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - contentType string - }{ - {name: "missing"}, - {name: "text plain", contentType: "text/plain"}, - {name: "malformed", contentType: "application/json; charset"}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - response := performRawRequestBody( - t, - testHandler(t, testDefaultSource), - http.MethodPost, - "/v1/topups", - `{"address":"`+testDestinationAddress+`","lovelace":1000000}`, - map[string]string{ - "Authorization": "Bearer " + testAuthToken, - "Content-Type": tt.contentType, - }, - ) - - if response.Code != http.StatusUnsupportedMediaType { - t.Fatalf("status = %d, want %d", response.Code, http.StatusUnsupportedMediaType) - } - var body errorResponse - decodeResponse(t, response, &body) - if body.Error.Code != codeUnsupportedMedia { - t.Fatalf("error code = %q, want %q", body.Error.Code, codeUnsupportedMedia) - } - }) - } -} - -func TestHandlerTopUpAllowsJSONContentTypeParameters(t *testing.T) { - t.Parallel() - - response := performRawRequestBody( - t, - testHandler(t, testDefaultSource), - http.MethodPost, - "/v1/topups", - `{"address":"`+testDestinationAddress+`","lovelace":1000000}`, - map[string]string{ - "Authorization": "Bearer " + testAuthToken, - "Content-Type": "application/json; charset=utf-8", - }, - ) - - if response.Code != http.StatusOK { - t.Fatalf("status = %d, want %d: %s", response.Code, http.StatusOK, response.Body.String()) - } -} - -func TestHandlerTopUpReportsSourceNotFound(t *testing.T) { - t.Parallel() - - response := performTopUpRequest( - t, - testHandler(t, testDefaultSource), - `{"address":"`+testDestinationAddress+`","lovelace":1000000,"source":"utxo9"}`, - ) - if response.Code != http.StatusNotFound { - t.Fatalf("status = %d, want %d", response.Code, http.StatusNotFound) - } - - var body errorResponse - decodeResponse(t, response, &body) - if body.Error.Code != topup.CodeSourceNotFound { - t.Fatalf("error code = %q, want %q", body.Error.Code, topup.CodeSourceNotFound) - } -} - -func TestHandlerTopUpReportsSourceUnavailable(t *testing.T) { - t.Parallel() - - rootDir := t.TempDir() - sourceDir := filepath.Join(rootDir, testDefaultSource) - requireNoError(t, os.MkdirAll(sourceDir, 0o700)) - writeSourceFile(t, filepath.Join(sourceDir, "utxo.addr"), testSourceAddress) - writeSourceFile(t, filepath.Join(sourceDir, "utxo.vkey"), `{"type":"bad","cborHex":"`+testVerificationKeyCBORHex+`"}`) - writeSourceFile(t, filepath.Join(sourceDir, "utxo.skey"), `{"type":"GenesisUTxOSigningKey_ed25519","cborHex":"`+testSigningKeyCBORHex+`"}`) - store := sources.NewStore(rootDir, testDefaultSource) - handler := NewHandler( - store, - topup.NewService(store, &fakeSubmitter{}, topup.Config{MaxLovelace: 10_000_000}), - testAuthToken, - slog.New(slog.NewTextHandler(os.Stderr, nil)), - ) - - response := performTopUpRequest( - t, - handler, - `{"address":"`+testDestinationAddress+`","lovelace":1000000}`, - ) - if response.Code != http.StatusServiceUnavailable { - t.Fatalf("status = %d, want %d", response.Code, http.StatusServiceUnavailable) - } - - var body errorResponse - decodeResponse(t, response, &body) - if body.Error.Code != topup.CodeSourceUnavailable { - t.Fatalf("error code = %q, want %q", body.Error.Code, topup.CodeSourceUnavailable) - } -} - -func TestHandlerTopUpReportsChainFailure(t *testing.T) { - t.Parallel() - - response := performTopUpRequest( - t, - testHandlerWithSubmitter(t, testDefaultSource, &fakeSubmitter{err: errors.New("chain failed")}), - `{"address":"`+testDestinationAddress+`","lovelace":1000000}`, - ) - if response.Code != http.StatusServiceUnavailable { - t.Fatalf("status = %d, want %d", response.Code, http.StatusServiceUnavailable) - } - - var body errorResponse - decodeResponse(t, response, &body) - if body.Error.Code != topup.CodeChainUnavailable { - t.Fatalf("error code = %q, want %q", body.Error.Code, topup.CodeChainUnavailable) - } -} - -func TestHandlerTopUpReloadsAuthTokenFile(t *testing.T) { - t.Parallel() - - rootDir := t.TempDir() - writeSource(t, rootDir, testDefaultSource) - store := sources.NewStore(rootDir, testDefaultSource) - currentToken := testAuthToken - handler := NewHandlerWithAuthTokenFile( - store, - topup.NewService(store, &fakeSubmitter{ - result: tx.Result{TxID: "abc123", SpentInputKeys: []string{testInputKey}}, - }, topup.Config{MaxLovelace: 10_000_000}), - "/token", - func(string) (string, error) { - return currentToken, nil - }, - slog.New(slog.NewTextHandler(os.Stderr, nil)), - ) - - response := performTopUpRequest( - t, - handler, - `{"address":"`+testDestinationAddress+`","lovelace":1000000}`, - ) - if response.Code != http.StatusOK { - t.Fatalf("status = %d, want %d", response.Code, http.StatusOK) - } - - currentToken = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" - response = performTopUpRequest( - t, - handler, - `{"address":"`+testDestinationAddress+`","lovelace":1000000}`, - ) - if response.Code != http.StatusUnauthorized { - t.Fatalf("status with old token = %d, want %d", response.Code, http.StatusUnauthorized) - } - - response = performRawRequestBody( - t, - handler, - http.MethodPost, - "/v1/topups", - `{"address":"`+testDestinationAddress+`","lovelace":1000000}`, - map[string]string{ - "Authorization": "Bearer " + currentToken, - "Content-Type": "application/json", - }, - ) - if response.Code != http.StatusOK { - t.Fatalf("status with new token = %d, want %d", response.Code, http.StatusOK) - } -} - -func TestHandlerRejectsUnsupportedMethod(t *testing.T) { - t.Parallel() - - response := performRequest(t, testHandler(t, testDefaultSource), http.MethodPost, "/v1/sources") - if response.Code != http.StatusMethodNotAllowed { - t.Fatalf("status = %d, want %d", response.Code, http.StatusMethodNotAllowed) - } - if got, want := response.Header().Get("Allow"), http.MethodGet; got != want { - t.Fatalf("Allow = %q, want %q", got, want) - } - - var body errorResponse - decodeResponse(t, response, &body) - if body.Error.Code != codeMethodNotAllowed { - t.Fatalf("error code = %q, want %q", body.Error.Code, codeMethodNotAllowed) - } -} - -func testHandler(t *testing.T, defaultSource string) http.Handler { - t.Helper() - - return testHandlerWithSubmitter(t, defaultSource, &fakeSubmitter{ - result: tx.Result{TxID: "abc123", SpentInputKeys: []string{testInputKey}}, - }) -} - -func testHandlerWithSubmitter(t *testing.T, defaultSource string, submitter tx.Submitter) http.Handler { - t.Helper() - - rootDir := t.TempDir() - writeSource(t, rootDir, "utxo2") - writeSource(t, rootDir, testDefaultSource) - store := sources.NewStore(rootDir, defaultSource) - - return NewHandler( - store, - topup.NewService(store, submitter, topup.Config{MaxLovelace: 10_000_000}), - testAuthToken, - slog.New(slog.NewTextHandler(os.Stderr, nil)), - ) -} - -func performRequest(t *testing.T, handler http.Handler, method string, path string) *httptest.ResponseRecorder { - t.Helper() - - return performRequestBody(t, handler, method, path, "") -} - -func performRequestBody(t *testing.T, handler http.Handler, method string, path string, body string) *httptest.ResponseRecorder { - t.Helper() - - return performRawRequestBody(t, handler, method, path, body, nil) -} - -func performTopUpRequest(t *testing.T, handler http.Handler, body string) *httptest.ResponseRecorder { - t.Helper() - - return performRawRequestBody( - t, - handler, - http.MethodPost, - "/v1/topups", - body, - map[string]string{ - "Authorization": "Bearer " + testAuthToken, - "Content-Type": "application/json", - }, - ) -} - -func performRawRequestBody( - t *testing.T, - handler http.Handler, - method string, - path string, - body string, - headers map[string]string, -) *httptest.ResponseRecorder { - t.Helper() - - request := httptest.NewRequest(method, path, bytes.NewBufferString(body)) - for key, value := range headers { - if value == "" { - continue - } - request.Header.Set(key, value) - } - response := httptest.NewRecorder() - handler.ServeHTTP(response, request) - - return response -} - -func decodeResponse(t *testing.T, response *httptest.ResponseRecorder, target any) { - t.Helper() - - if got, want := response.Header().Get("Content-Type"), "application/json"; got != want { - t.Fatalf("Content-Type = %q, want %q", got, want) - } - if err := json.Unmarshal(response.Body.Bytes(), target); err != nil { - t.Fatalf("decode response: %v\n%s", err, response.Body.String()) - } -} - -func writeSource(t *testing.T, rootDir string, name string) { - t.Helper() - - sourceDir := filepath.Join(rootDir, name) - requireNoError(t, os.MkdirAll(sourceDir, 0o700)) - writeSourceFile(t, filepath.Join(sourceDir, "utxo.addr"), testSourceAddress) - writeSourceFile(t, filepath.Join(sourceDir, "utxo.vkey"), `{ - "type": "GenesisUTxOVerificationKey_ed25519", - "description": "Genesis Initial UTxO Verification Key", - "cborHex": "`+testVerificationKeyCBORHex+`" -}`) - writeSourceFile(t, filepath.Join(sourceDir, "utxo.skey"), `{ - "type": "GenesisUTxOSigningKey_ed25519", - "description": "Genesis Initial UTxO Signing Key", - "cborHex": "`+testSigningKeyCBORHex+`" -}`) -} - -func writeSourceFile(t *testing.T, path string, contents string) { - t.Helper() - - requireNoError(t, os.WriteFile(path, []byte(contents), 0o600)) -} - -func requireNoError(t *testing.T, err error) { - t.Helper() - - if err != nil { - t.Fatalf("unexpected error: %v", err) - } -} - -func deriveTestVerificationKeyHex(signingKeyHex string) string { - signingKey, err := hex.DecodeString(signingKeyHex) - if err != nil { - panic(err) - } - privateKey := ed25519.NewKeyFromSeed(signingKey) - publicKey := privateKey.Public().(ed25519.PublicKey) - - return hex.EncodeToString(publicKey) -} - -func mustDeriveTestnetPaymentAddress(verificationKeyHex string) string { - address, err := sources.DeriveTestnetPaymentAddress(verificationKeyHex) - if err != nil { - panic(err) - } - - return address -} - -type fakeSubmitter struct { - result tx.Result - err error - requests []tx.Request -} - -func (f *fakeSubmitter) Submit(_ context.Context, request tx.Request) (tx.Result, error) { - f.requests = append(f.requests, request) - if f.err != nil { - return tx.Result{}, f.err - } - if len(f.result.SpentInputKeys) == 0 { - f.result.SpentInputKeys = []string{testInputKey} - } - - return f.result, nil -} diff --git a/services/faucet/internal/sources/sources.go b/services/faucet/internal/sources/sources.go deleted file mode 100644 index f90aeeab..00000000 --- a/services/faucet/internal/sources/sources.go +++ /dev/null @@ -1,749 +0,0 @@ -package sources - -import ( - "bytes" - "context" - "crypto/ed25519" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "sort" - "strings" - "unicode" - - apolloBech32 "github.com/Salvionied/apollo/crypto/bech32" - apolloAddress "github.com/Salvionied/apollo/serialization/Address" - "github.com/fxamacker/cbor/v2" - - "github.com/meigma/yacd/internal/cardano/wallet" -) - -const ( - verificationKeyFile = "utxo.vkey" - signingKeyFile = "utxo.skey" - addressFile = "utxo.addr" - verificationKeyType = "GenesisUTxOVerificationKey_ed25519" - signingKeyType = "GenesisUTxOSigningKey_ed25519" - sourceDirectoryPathName = "." - sourceNamePrefix = "utxo" - addressHRP = "addr_test" - addressPrefix = addressHRP + "1" - maxSourceEntries = 64 - maxKeyFileSize = 4 * 1024 - maxAddressFileSize = 512 - keyCBORBytesLength = 32 - keyHashBytesLength = 28 - baseAddressBytesLength = 1 + keyHashBytesLength + keyHashBytesLength - shortAddressBytesLength = 1 + keyHashBytesLength - - // CodeInvalidSourceName identifies a source name validation error. - CodeInvalidSourceName = "invalid_source_name" - // CodeSourceNotFound identifies a missing or unusable source. - CodeSourceNotFound = "source_not_found" - // CodeSourceIncomplete identifies a source missing required source files. - CodeSourceIncomplete = "source_incomplete" - // CodeSourceInvalidKey identifies malformed source file metadata. - CodeSourceInvalidKey = "source_invalid_key" - // CodeSourceReadFailed identifies a filesystem read failure. - CodeSourceReadFailed = "source_read_failed" -) - -// Store discovers faucet sources from a cardano-testnet utxo-keys directory. -type Store struct { - rootDir string - defaultName string -} - -// List describes the source listing API response. -type List struct { - DefaultSource string `json:"defaultSource"` - Sources []Source `json:"sources"` -} - -// Source describes one usable faucet source without exposing signing material. -type Source struct { - Name string `json:"name"` - Default bool `json:"default"` - Address string `json:"address"` - VerificationKeyType string `json:"verificationKeyType"` - SigningKeyType string `json:"signingKeyType"` - VerificationKeyDescription string `json:"verificationKeyDescription,omitempty"` - SigningKeyDescription string `json:"signingKeyDescription,omitempty"` -} - -// FundingSource contains the private key material needed to submit faucet -// transactions. It must never be returned directly from HTTP handlers. -type FundingSource struct { - // Name is the source directory name such as utxo1. - Name string - // Address is the source payment address. - Address string - // VerificationKeyHex is the lowercase raw verification key bytes encoded as hex. - VerificationKeyHex string - // SigningKeyHex is the lowercase raw signing key bytes encoded as hex. - SigningKeyHex string -} - -// Error is a structured source discovery error. -type Error struct { - Code string - Message string -} - -type keyMetadata struct { - Type string `json:"type"` - Description string `json:"description"` - CBORHex string `json:"cborHex"` - rawHex string - rawBytes []byte -} - -// NewStore returns a source store rooted at rootDir. -func NewStore(rootDir string, defaultName string) Store { - return Store{ - rootDir: rootDir, - defaultName: defaultName, - } -} - -// RootDir returns the configured utxo-keys directory. -func (s Store) RootDir() string { - return s.rootDir -} - -// DefaultName returns the configured default source name. -func (s Store) DefaultName() string { - return s.defaultName -} - -func (e *Error) Error() string { - return e.Message -} - -// IsCode reports whether err is a source Error with code. -func IsCode(err error, code string) bool { - var sourceErr *Error - ok := errors.As(err, &sourceErr) - - return ok && sourceErr.Code == code -} - -// DeriveTestnetPaymentAddress returns the enterprise testnet address for a raw -// 32-byte payment verification key encoded as lowercase or uppercase hex. -func DeriveTestnetPaymentAddress(verificationKeyHex string) (string, error) { - verificationKeyBytes, err := decodeRawKeyHex(verificationKeyHex, "verification key") - if err != nil { - return "", err - } - - return deriveTestnetPaymentAddress(verificationKeyBytes) -} - -// ValidateFundingSource validates private source material before transaction -// submission. -func ValidateFundingSource(source FundingSource) error { - if err := ValidateName(source.Name); err != nil { - return err - } - if err := ValidateTestnetAddress(source.Address); err != nil { - return sourceError(CodeSourceInvalidKey, "address for source %q is invalid: %v", source.Name, err) - } - - verificationKeyBytes, err := decodeRawKeyHex(source.VerificationKeyHex, "verification key") - if err != nil { - return sourceError(CodeSourceInvalidKey, "verification key for source %q is invalid: %v", source.Name, err) - } - signingKeyBytes, err := decodeRawKeyHex(source.SigningKeyHex, "signing key") - if err != nil { - return sourceError(CodeSourceInvalidKey, "signing key for source %q is invalid: %v", source.Name, err) - } - - return validateFundingSource(source.Name, source.Address, verificationKeyBytes, signingKeyBytes) -} - -// ValidateName validates a request/config source name. -func ValidateName(name string) error { - if name == "" { - return sourceError(CodeInvalidSourceName, "source name is required") - } - if strings.TrimSpace(name) != name { - return sourceError(CodeInvalidSourceName, "source name must not contain leading or trailing whitespace") - } - if name == "." || name == ".." || strings.Contains(name, "..") { - return sourceError(CodeInvalidSourceName, "source name must not contain traversal") - } - if strings.ContainsAny(name, `/\`) { - return sourceError(CodeInvalidSourceName, "source name must not contain path separators") - } - if filepath.Base(name) != name { - return sourceError(CodeInvalidSourceName, "source name must be a single path segment") - } - if strings.ContainsFunc(name, unicode.IsControl) { - return sourceError(CodeInvalidSourceName, "source name must not contain control characters") - } - if !strings.HasPrefix(name, sourceNamePrefix) || len(name) == len(sourceNamePrefix) { - return sourceError(CodeInvalidSourceName, "source name must match utxo[1-9][0-9]*") - } - digits := name[len(sourceNamePrefix):] - if digits[0] == '0' { - return sourceError(CodeInvalidSourceName, "source name must match utxo[1-9][0-9]*") - } - for _, char := range digits { - if char < '0' || char > '9' { - return sourceError(CodeInvalidSourceName, "source name must match utxo[1-9][0-9]*") - } - } - - return nil -} - -// List returns valid faucet sources sorted by name. -func (s Store) List() (List, error) { - if err := ValidateName(s.defaultName); err != nil { - return List{}, err - } - - root, err := s.openRoot() - if err != nil { - return List{}, err - } - defer func() { - _ = root.Close() - }() - - entries, err := readDir(root, sourceDirectoryPathName, maxSourceEntries) - if err != nil { - return List{}, sourceError(CodeSourceReadFailed, "read source directory %q: %v", s.rootDir, err) - } - - result := List{ - DefaultSource: s.defaultName, - Sources: make([]Source, 0, len(entries)), - } - for _, entry := range entries { - if !entry.IsDir() { - continue - } - if err := ValidateName(entry.Name()); err != nil { - continue - } - - source, err := s.readSource(root, entry.Name()) - if err != nil { - if IsCode(err, CodeSourceNotFound) || IsCode(err, CodeSourceIncomplete) { - continue - } - - return List{}, err - } - result.Sources = append(result.Sources, source) - } - - sort.Slice(result.Sources, func(i int, j int) bool { - return result.Sources[i].Name < result.Sources[j].Name - }) - - return result, nil -} - -// Get returns one valid faucet source by name. -func (s Store) Get(name string) (Source, error) { - if err := ValidateName(name); err != nil { - return Source{}, err - } - - root, err := s.openRoot() - if err != nil { - return Source{}, err - } - defer func() { - _ = root.Close() - }() - - source, err := s.readSource(root, name) - if err != nil { - if IsCode(err, CodeSourceIncomplete) { - return Source{}, sourceError(CodeSourceNotFound, "faucet source %q was not found", name) - } - - return Source{}, err - } - - return source, nil -} - -// ReadFundingSource returns private source data for transaction submission. -func (s Store) ReadFundingSource(ctx context.Context, name string) (FundingSource, error) { - if ctx == nil { - ctx = context.Background() - } - if err := ctx.Err(); err != nil { - return FundingSource{}, sourceError(CodeSourceReadFailed, "read faucet source %q: %v", name, err) - } - if err := ValidateName(name); err != nil { - return FundingSource{}, err - } - - root, err := s.openRoot() - if err != nil { - return FundingSource{}, err - } - defer func() { - _ = root.Close() - }() - - source, err := s.readFundingSource(root, name) - if err != nil { - if IsCode(err, CodeSourceIncomplete) { - return FundingSource{}, sourceError(CodeSourceNotFound, "faucet source %q was not found", name) - } - - return FundingSource{}, err - } - - return source, nil -} - -// Ready returns nil when the source directory and default source are usable. -func (s Store) Ready() error { - root, err := s.openRoot() - if err != nil { - return err - } - defer func() { - _ = root.Close() - }() - - if _, err := readDir(root, sourceDirectoryPathName, maxSourceEntries); err != nil { - return sourceError(CodeSourceReadFailed, "read source directory %q: %v", s.rootDir, err) - } - if _, err := s.readSource(root, s.defaultName); err != nil { - return err - } - - return nil -} - -func (s Store) openRoot() (*os.Root, error) { - root, err := os.OpenRoot(s.rootDir) - if err != nil { - return nil, sourceError(CodeSourceReadFailed, "open source directory %q: %v", s.rootDir, err) - } - - return root, nil -} - -func readDir(root *os.Root, name string, maxEntries int) ([]os.DirEntry, error) { - dir, err := root.Open(name) - if err != nil { - return nil, err - } - defer func() { - _ = dir.Close() - }() - - entries, err := dir.ReadDir(maxEntries + 1) - if err != nil && !errors.Is(err, io.EOF) { - return nil, err - } - if len(entries) > maxEntries { - return nil, fmt.Errorf("source directory has more than %d entries", maxEntries) - } - - return entries, nil -} - -func (s Store) readSource(root *os.Root, name string) (Source, error) { - address, verificationKey, signingKey, err := readSourceFiles(root, name) - if err != nil { - return Source{}, err - } - - return Source{ - Name: name, - Default: name == s.defaultName, - Address: address, - VerificationKeyType: verificationKey.Type, - SigningKeyType: signingKey.Type, - VerificationKeyDescription: verificationKey.Description, - SigningKeyDescription: signingKey.Description, - }, nil -} - -func (s Store) readFundingSource(root *os.Root, name string) (FundingSource, error) { - address, verificationKey, signingKey, err := readSourceFiles(root, name) - if err != nil { - return FundingSource{}, err - } - - return FundingSource{ - Name: name, - Address: address, - VerificationKeyHex: verificationKey.rawHex, - SigningKeyHex: signingKey.rawHex, - }, nil -} - -func readSourceFiles(root *os.Root, name string) (string, keyMetadata, keyMetadata, error) { - info, err := root.Lstat(name) - if err != nil { - if os.IsNotExist(err) { - return "", keyMetadata{}, keyMetadata{}, sourceError(CodeSourceNotFound, "faucet source %q was not found", name) - } - - return "", keyMetadata{}, keyMetadata{}, sourceError(CodeSourceReadFailed, "stat source %q: %v", name, err) - } - if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() { - return "", keyMetadata{}, keyMetadata{}, sourceError(CodeSourceNotFound, "faucet source %q was not found", name) - } - - address, err := readAddress(root, filepath.Join(name, addressFile), name) - if err != nil { - return "", keyMetadata{}, keyMetadata{}, err - } - verificationKey, err := readKeyMetadata( - root, - filepath.Join(name, verificationKeyFile), - name, - "verification", - verificationKeyType, - ) - if err != nil { - return "", keyMetadata{}, keyMetadata{}, err - } - signingKey, err := readKeyMetadata( - root, - filepath.Join(name, signingKeyFile), - name, - "signing", - signingKeyType, - ) - if err != nil { - return "", keyMetadata{}, keyMetadata{}, err - } - if err := validateFundingSource(name, address, verificationKey.rawBytes, signingKey.rawBytes); err != nil { - return "", keyMetadata{}, keyMetadata{}, err - } - - return address, verificationKey, signingKey, nil -} - -func readKeyMetadata( - root *os.Root, - path string, - sourceName string, - keyKind string, - expectedType string, -) (keyMetadata, error) { - contents, err := readRegularFile(root, path, maxKeyFileSize, sourceName, keyKind+" key") - if err != nil { - return keyMetadata{}, err - } - - var metadata keyMetadata - if err := json.Unmarshal(contents, &metadata); err != nil { - return keyMetadata{}, sourceError( - CodeSourceInvalidKey, - "parse %s key for source %q: %v", - keyKind, - sourceName, - err, - ) - } - if strings.TrimSpace(metadata.Type) == "" { - return keyMetadata{}, sourceError( - CodeSourceInvalidKey, - "%s key for source %q is missing type", - keyKind, - sourceName, - ) - } - if metadata.Type != expectedType { - return keyMetadata{}, sourceError( - CodeSourceInvalidKey, - "%s key for source %q has type %q, want %q", - keyKind, - sourceName, - metadata.Type, - expectedType, - ) - } - rawHex, err := decodeKeyCBORHex(metadata.CBORHex, sourceName, keyKind) - if err != nil { - return keyMetadata{}, err - } - metadata.rawHex = rawHex - rawBytes, err := hex.DecodeString(rawHex) - if err != nil { - return keyMetadata{}, sourceError( - CodeSourceInvalidKey, - "decode %s key raw hex for source %q: %v", - keyKind, - sourceName, - err, - ) - } - metadata.rawBytes = rawBytes - - return metadata, nil -} - -func readAddress(root *os.Root, path string, sourceName string) (string, error) { - contents, err := readRegularFile(root, path, maxAddressFileSize, sourceName, "address") - if err != nil { - return "", err - } - - address := strings.TrimSpace(string(contents)) - if err := ValidateTestnetAddress(address); err != nil { - return "", sourceError(CodeSourceInvalidKey, "address for source %q is invalid: %v", sourceName, err) - } - - return address, nil -} - -// ValidateTestnetAddress validates a bech32 Cardano testnet payment address. -func ValidateTestnetAddress(address string) error { - if strings.TrimSpace(address) != address { - return errors.New("address must not contain leading or trailing whitespace") - } - if address == "" { - return errors.New("address is required") - } - if strings.ContainsFunc(address, func(char rune) bool { - return unicode.IsControl(char) || unicode.IsSpace(char) - }) { - return errors.New("address must not contain whitespace or control characters") - } - if !strings.HasPrefix(address, addressPrefix) { - return fmt.Errorf("address must start with %q", addressPrefix) - } - hrp, data, err := apolloBech32.Decode(address) - if err != nil { - return fmt.Errorf("address is not valid bech32: %w", err) - } - if hrp != addressHRP { - return fmt.Errorf("address human-readable prefix must be %q", addressHRP) - } - payload, err := apolloBech32.ConvertBits(data, 5, 8, false) - if err != nil { - return fmt.Errorf("address payload is invalid: %w", err) - } - if len(payload) == 0 { - return errors.New("address payload is empty") - } - - header := payload[0] - network := header & 0x0f - addressType := (header & 0xf0) >> 4 - if network != apolloAddress.TESTNET { - return errors.New("address network must be testnet") - } - switch addressType { - case apolloAddress.KEY_KEY, apolloAddress.SCRIPT_KEY, apolloAddress.KEY_SCRIPT, apolloAddress.SCRIPT_SCRIPT: - if len(payload) != baseAddressBytesLength { - return fmt.Errorf("address payload length is %d bytes, want %d", len(payload), baseAddressBytesLength) - } - case apolloAddress.KEY_NONE, apolloAddress.SCRIPT_NONE: - if len(payload) != shortAddressBytesLength { - return fmt.Errorf("address payload length is %d bytes, want %d", len(payload), shortAddressBytesLength) - } - default: - return errors.New("address must be a payment address") - } - - return nil -} - -func readRegularFile( - root *os.Root, - path string, - maxSize int64, - sourceName string, - fieldName string, -) ([]byte, error) { - info, err := root.Lstat(path) - if err != nil { - if os.IsNotExist(err) { - return nil, sourceError( - CodeSourceIncomplete, - "faucet source %q is missing %s", - sourceName, - fieldName, - ) - } - - return nil, sourceError( - CodeSourceReadFailed, - "read %s for source %q: %v", - fieldName, - sourceName, - err, - ) - } - if info.Mode()&os.ModeSymlink != 0 { - return nil, sourceError( - CodeSourceInvalidKey, - "%s for source %q must not be a symlink", - fieldName, - sourceName, - ) - } - if !info.Mode().IsRegular() { - return nil, sourceError( - CodeSourceInvalidKey, - "%s for source %q must be a regular file", - fieldName, - sourceName, - ) - } - if info.Size() > maxSize { - return nil, sourceError( - CodeSourceInvalidKey, - "%s for source %q is larger than %d bytes", - fieldName, - sourceName, - maxSize, - ) - } - - file, err := root.Open(path) - if err != nil { - return nil, sourceError( - CodeSourceReadFailed, - "read %s for source %q: %v", - fieldName, - sourceName, - err, - ) - } - defer func() { - _ = file.Close() - }() - - contents, err := io.ReadAll(io.LimitReader(file, maxSize+1)) - if err != nil { - return nil, sourceError( - CodeSourceReadFailed, - "read %s for source %q: %v", - fieldName, - sourceName, - err, - ) - } - if int64(len(contents)) > maxSize { - return nil, sourceError( - CodeSourceInvalidKey, - "%s for source %q is larger than %d bytes", - fieldName, - sourceName, - maxSize, - ) - } - - return contents, nil -} - -func decodeKeyCBORHex(cborHex string, sourceName string, keyKind string) (string, error) { - cborHex = strings.TrimSpace(cborHex) - if cborHex == "" { - return "", sourceError(CodeSourceInvalidKey, "%s key for source %q is missing cborHex", keyKind, sourceName) - } - - rawCBOR, err := hex.DecodeString(cborHex) - if err != nil { - return "", sourceError(CodeSourceInvalidKey, "decode %s key cborHex for source %q: %v", keyKind, sourceName, err) - } - - var keyBytes []byte - if err := cbor.Unmarshal(rawCBOR, &keyBytes); err != nil { - return "", sourceError(CodeSourceInvalidKey, "parse %s key cborHex for source %q: %v", keyKind, sourceName, err) - } - if len(keyBytes) != keyCBORBytesLength { - return "", sourceError( - CodeSourceInvalidKey, - "%s key cborHex for source %q decodes to %d bytes, want %d", - keyKind, - sourceName, - len(keyBytes), - keyCBORBytesLength, - ) - } - - return hex.EncodeToString(keyBytes), nil -} - -func sourceError(code string, format string, args ...any) *Error { - return &Error{ - Code: code, - Message: fmt.Sprintf(format, args...), - } -} - -func decodeRawKeyHex(value string, fieldName string) ([]byte, error) { - trimmed := strings.TrimSpace(value) - decoded, err := hex.DecodeString(trimmed) - if err != nil { - return nil, fmt.Errorf("decode %s hex: %w", fieldName, err) - } - if len(decoded) != keyCBORBytesLength { - return nil, fmt.Errorf("%s must be %d bytes, got %d", fieldName, keyCBORBytesLength, len(decoded)) - } - - return decoded, nil -} - -func validateFundingSource(sourceName string, address string, verificationKeyBytes []byte, signingKeyBytes []byte) error { - if len(verificationKeyBytes) != keyCBORBytesLength { - return sourceError( - CodeSourceInvalidKey, - "verification key for source %q must be %d bytes, got %d", - sourceName, - keyCBORBytesLength, - len(verificationKeyBytes), - ) - } - if len(signingKeyBytes) != keyCBORBytesLength { - return sourceError( - CodeSourceInvalidKey, - "signing key for source %q must be %d bytes, got %d", - sourceName, - keyCBORBytesLength, - len(signingKeyBytes), - ) - } - - signingKey := ed25519.NewKeyFromSeed(signingKeyBytes) - signingPublicKey, ok := signingKey.Public().(ed25519.PublicKey) - if !ok { - return sourceError(CodeSourceInvalidKey, "signing key for source %q cannot derive a public key", sourceName) - } - if !bytes.Equal(verificationKeyBytes, signingPublicKey) { - return sourceError(CodeSourceInvalidKey, "signing key for source %q does not match verification key", sourceName) - } - - derivedAddress, err := deriveTestnetPaymentAddress(verificationKeyBytes) - if err != nil { - return sourceError(CodeSourceInvalidKey, "derive address for source %q: %v", sourceName, err) - } - if address != derivedAddress { - return sourceError( - CodeSourceInvalidKey, - "address for source %q does not match verification key", - sourceName, - ) - } - - return nil -} - -func deriveTestnetPaymentAddress(verificationKeyBytes []byte) (string, error) { - // Address derivation is shared with the operator's developer-wallet - // bootstrap so the faucet and the controller can never compute an address - // two different ways. - return wallet.DeriveTestnetAddress(verificationKeyBytes) -} diff --git a/services/faucet/internal/sources/sources_test.go b/services/faucet/internal/sources/sources_test.go deleted file mode 100644 index 5fa6a4a5..00000000 --- a/services/faucet/internal/sources/sources_test.go +++ /dev/null @@ -1,501 +0,0 @@ -package sources - -import ( - "crypto/ed25519" - "encoding/hex" - "encoding/json" - "fmt" - "os" - "path/filepath" - "strings" - "testing" -) - -const ( - testDefaultSource = "utxo1" - otherTestAddress = "addr_test1vqy2n0vz5rlpykf6dcqn55xdcpey7mejyexlgj6370leayst4k6ta" - testSecretMaterial = "secret-cbor-material" - oversizedFileContents = 5 * 1024 -) - -var ( - testSigningRawKeyHex = strings.Repeat("01", keyCBORBytesLength) - testVerificationRawKeyHex = deriveTestVerificationKeyHex(testSigningRawKeyHex) - testVerificationKeyCBORHex = cborHexForRawKey(testVerificationRawKeyHex) - testSigningKeyCBORHex = cborHexForRawKey(testSigningRawKeyHex) - testAddress = mustDeriveTestnetPaymentAddress(testVerificationRawKeyHex) -) - -func TestStoreListDiscoversValidSources(t *testing.T) { - t.Parallel() - - rootDir := t.TempDir() - writeSource(t, rootDir, "utxo3") - writeSource(t, rootDir, testDefaultSource) - writeSource(t, rootDir, "utxo2") - writeSourceFile(t, filepath.Join(rootDir, "README.md"), "ignored") - writeSourceFile(t, filepath.Join(rootDir, "loose-file"), "ignored") - requireNoError(t, os.Mkdir(filepath.Join(rootDir, "incomplete"), 0o700)) - - list, err := NewStore(rootDir, testDefaultSource).List() - requireNoError(t, err) - - if got, want := list.DefaultSource, testDefaultSource; got != want { - t.Fatalf("DefaultSource = %q, want %q", got, want) - } - if got, want := sourceNames(list.Sources), []string{testDefaultSource, "utxo2", "utxo3"}; !equalStrings(got, want) { - t.Fatalf("sources = %#v, want %#v", got, want) - } - if !list.Sources[0].Default { - t.Fatal("utxo1 was not marked as default") - } - if list.Sources[0].VerificationKeyType != "GenesisUTxOVerificationKey_ed25519" { - t.Fatalf("verification key type = %q", list.Sources[0].VerificationKeyType) - } - if list.Sources[0].SigningKeyType != "GenesisUTxOSigningKey_ed25519" { - t.Fatalf("signing key type = %q", list.Sources[0].SigningKeyType) - } -} - -func TestStoreReadyRequiresDefaultSource(t *testing.T) { - t.Parallel() - - rootDir := t.TempDir() - writeSource(t, rootDir, "utxo2") - - err := NewStore(rootDir, testDefaultSource).Ready() - if err == nil { - t.Fatal("Ready succeeded, want missing default source error") - } - if !IsCode(err, CodeSourceNotFound) { - t.Fatalf("error = %v, want %s", err, CodeSourceNotFound) - } -} - -func TestStoreRejectsTraversalNames(t *testing.T) { - t.Parallel() - - store := NewStore(t.TempDir(), testDefaultSource) - for _, name := range []string{"../utxo1", "utxo/1", `utxo\1`, "..", "utxo..1", "wallet1", "utxo0", "utxo01", "utxo1\x00"} { - t.Run(name, func(t *testing.T) { - t.Parallel() - - _, err := store.Get(name) - if err == nil { - t.Fatal("Get succeeded, want invalid source name") - } - if !IsCode(err, CodeInvalidSourceName) { - t.Fatalf("error = %v, want %s", err, CodeInvalidSourceName) - } - }) - } -} - -func TestSourceJSONDoesNotExposeCBORHex(t *testing.T) { - t.Parallel() - - rootDir := t.TempDir() - writeSource(t, rootDir, testDefaultSource) - source, err := NewStore(rootDir, testDefaultSource).Get(testDefaultSource) - requireNoError(t, err) - - encoded, err := json.Marshal(source) - requireNoError(t, err) - if strings.Contains(string(encoded), "cborHex") || - strings.Contains(string(encoded), testSecretMaterial) || - strings.Contains(string(encoded), testVerificationRawKeyHex) || - strings.Contains(string(encoded), testSigningRawKeyHex) { - t.Fatalf("source JSON exposed key material: %s", encoded) - } - for _, secretPathDetail := range []string{"verificationKeyPath", "signingKeyPath", rootDir, "utxo.skey"} { - if strings.Contains(string(encoded), secretPathDetail) { - t.Fatalf("source JSON exposed path detail %q: %s", secretPathDetail, encoded) - } - } -} - -func TestStoreGetReturnsValidSource(t *testing.T) { - t.Parallel() - - rootDir := t.TempDir() - writeSource(t, rootDir, testDefaultSource) - - source, err := NewStore(rootDir, testDefaultSource).Get(testDefaultSource) - requireNoError(t, err) - - if got, want := source.Name, testDefaultSource; got != want { - t.Fatalf("Name = %q, want %q", got, want) - } - if got, want := source.SigningKeyType, signingKeyType; got != want { - t.Fatalf("SigningKeyType = %q, want %q", got, want) - } - if got, want := source.Address, testAddress; got != want { - t.Fatalf("Address = %q, want %q", got, want) - } -} - -func TestStoreReadFundingSourceReturnsRawKeyHex(t *testing.T) { - t.Parallel() - - rootDir := t.TempDir() - writeSource(t, rootDir, testDefaultSource) - - source, err := NewStore(rootDir, testDefaultSource).ReadFundingSource(t.Context(), testDefaultSource) - requireNoError(t, err) - - if got, want := source.Name, testDefaultSource; got != want { - t.Fatalf("Name = %q, want %q", got, want) - } - if got, want := source.Address, testAddress; got != want { - t.Fatalf("Address = %q, want %q", got, want) - } - if got, want := source.VerificationKeyHex, testVerificationRawKeyHex; got != want { - t.Fatalf("VerificationKeyHex = %q, want %q", got, want) - } - if got, want := source.SigningKeyHex, testSigningRawKeyHex; got != want { - t.Fatalf("SigningKeyHex = %q, want %q", got, want) - } -} - -func TestStoreRejectsSymlinkedSource(t *testing.T) { - t.Parallel() - - rootDir := t.TempDir() - outsideDir := t.TempDir() - writeSource(t, outsideDir, "external") - requireNoError(t, os.Symlink(filepath.Join(outsideDir, "external"), filepath.Join(rootDir, testDefaultSource))) - - err := NewStore(rootDir, testDefaultSource).Ready() - if err == nil { - t.Fatal("Ready succeeded, want symlinked source rejection") - } - if !IsCode(err, CodeSourceNotFound) { - t.Fatalf("error = %v, want %s", err, CodeSourceNotFound) - } -} - -func TestStoreRejectsSymlinkedKeyFiles(t *testing.T) { - t.Parallel() - - rootDir := t.TempDir() - sourceDir := filepath.Join(rootDir, testDefaultSource) - requireNoError(t, os.MkdirAll(sourceDir, 0o700)) - outsideKey := filepath.Join(t.TempDir(), "utxo.vkey") - writeKey(t, outsideKey, verificationKeyType) - requireNoError(t, os.Symlink(outsideKey, filepath.Join(sourceDir, "utxo.vkey"))) - writeAddress(t, filepath.Join(sourceDir, "utxo.addr"), testAddress) - writeKey(t, filepath.Join(sourceDir, "utxo.skey"), signingKeyType) - - _, err := NewStore(rootDir, testDefaultSource).Get(testDefaultSource) - if err == nil { - t.Fatal("Get succeeded, want symlinked key rejection") - } - if !IsCode(err, CodeSourceInvalidKey) { - t.Fatalf("error = %v, want %s", err, CodeSourceInvalidKey) - } -} - -func TestStoreRejectsUnexpectedKeyType(t *testing.T) { - t.Parallel() - - rootDir := t.TempDir() - sourceDir := filepath.Join(rootDir, testDefaultSource) - requireNoError(t, os.MkdirAll(sourceDir, 0o700)) - writeAddress(t, filepath.Join(sourceDir, "utxo.addr"), testAddress) - writeKey(t, filepath.Join(sourceDir, "utxo.vkey"), "PaymentVerificationKeyShelley_ed25519") - writeKey(t, filepath.Join(sourceDir, "utxo.skey"), signingKeyType) - - err := NewStore(rootDir, testDefaultSource).Ready() - if err == nil { - t.Fatal("Ready succeeded, want unexpected key type error") - } - if !IsCode(err, CodeSourceInvalidKey) { - t.Fatalf("error = %v, want %s", err, CodeSourceInvalidKey) - } -} - -func TestStoreRequiresUsableAddress(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - address string - expectCode string - omitAddress bool - }{ - { - name: "missing address", - expectCode: CodeSourceIncomplete, - omitAddress: true, - }, - { - name: "wrong address prefix", - address: "addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3l62x5n0x", - expectCode: CodeSourceInvalidKey, - }, - { - name: "bad address checksum", - address: "addr_test1vqy2n0vz5rlpykf6dcqn55xdcpey7mejyexlgj6370leayst4k6tx", - expectCode: CodeSourceInvalidKey, - }, - { - name: "address contains whitespace", - address: testAddress + "\nextra", - expectCode: CodeSourceInvalidKey, - }, - { - name: "address file is oversized", - address: "addr_test1" + strings.Repeat("a", maxAddressFileSize), - expectCode: CodeSourceInvalidKey, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - rootDir := t.TempDir() - sourceDir := filepath.Join(rootDir, testDefaultSource) - requireNoError(t, os.MkdirAll(sourceDir, 0o700)) - if !tt.omitAddress { - writeAddress(t, filepath.Join(sourceDir, "utxo.addr"), tt.address) - } - writeKey(t, filepath.Join(sourceDir, "utxo.vkey"), verificationKeyType) - writeKey(t, filepath.Join(sourceDir, "utxo.skey"), signingKeyType) - - err := NewStore(rootDir, testDefaultSource).Ready() - if err == nil { - t.Fatal("Ready succeeded, want unusable address error") - } - if !IsCode(err, tt.expectCode) { - t.Fatalf("error = %v, want %s", err, tt.expectCode) - } - }) - } -} - -func TestStoreRejectsAddressThatDoesNotMatchKeys(t *testing.T) { - t.Parallel() - - rootDir := t.TempDir() - sourceDir := filepath.Join(rootDir, testDefaultSource) - requireNoError(t, os.MkdirAll(sourceDir, 0o700)) - writeAddress(t, filepath.Join(sourceDir, "utxo.addr"), otherTestAddress) - writeKey(t, filepath.Join(sourceDir, "utxo.vkey"), verificationKeyType) - writeKey(t, filepath.Join(sourceDir, "utxo.skey"), signingKeyType) - - err := NewStore(rootDir, testDefaultSource).Ready() - if err == nil { - t.Fatal("Ready succeeded, want mismatched address error") - } - if !IsCode(err, CodeSourceInvalidKey) { - t.Fatalf("error = %v, want %s", err, CodeSourceInvalidKey) - } -} - -func TestStoreRejectsSigningKeyThatDoesNotMatchVerificationKey(t *testing.T) { - t.Parallel() - - rootDir := t.TempDir() - sourceDir := filepath.Join(rootDir, testDefaultSource) - requireNoError(t, os.MkdirAll(sourceDir, 0o700)) - writeAddress(t, filepath.Join(sourceDir, "utxo.addr"), testAddress) - writeKey(t, filepath.Join(sourceDir, "utxo.vkey"), verificationKeyType) - writeKeyWithCBORHex( - t, - filepath.Join(sourceDir, "utxo.skey"), - signingKeyType, - cborHexForRawKey(strings.Repeat("02", keyCBORBytesLength)), - ) - - err := NewStore(rootDir, testDefaultSource).Ready() - if err == nil { - t.Fatal("Ready succeeded, want mismatched keypair error") - } - if !IsCode(err, CodeSourceInvalidKey) { - t.Fatalf("error = %v, want %s", err, CodeSourceInvalidKey) - } -} - -func TestStoreRejectsInvalidKeyCBORHex(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - cborHex string - }{ - { - name: "missing cborHex", - }, - { - name: "not hex", - cborHex: "not-hex", - }, - { - name: "valid cbor wrong length", - cborHex: "41ff", - }, - { - name: "valid cbor wrong envelope", - cborHex: "01", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - rootDir := t.TempDir() - sourceDir := filepath.Join(rootDir, testDefaultSource) - requireNoError(t, os.MkdirAll(sourceDir, 0o700)) - writeAddress(t, filepath.Join(sourceDir, "utxo.addr"), testAddress) - writeKeyWithCBORHex(t, filepath.Join(sourceDir, "utxo.vkey"), verificationKeyType, tt.cborHex) - writeKey(t, filepath.Join(sourceDir, "utxo.skey"), signingKeyType) - - err := NewStore(rootDir, testDefaultSource).Ready() - if err == nil { - t.Fatal("Ready succeeded, want invalid cborHex error") - } - if !IsCode(err, CodeSourceInvalidKey) { - t.Fatalf("error = %v, want %s", err, CodeSourceInvalidKey) - } - }) - } -} - -func TestStoreRejectsOversizedKeyFile(t *testing.T) { - t.Parallel() - - rootDir := t.TempDir() - sourceDir := filepath.Join(rootDir, testDefaultSource) - requireNoError(t, os.MkdirAll(sourceDir, 0o700)) - writeAddress(t, filepath.Join(sourceDir, "utxo.addr"), testAddress) - writeSourceFile(t, filepath.Join(sourceDir, "utxo.vkey"), strings.Repeat("x", oversizedFileContents)) - writeKey(t, filepath.Join(sourceDir, "utxo.skey"), signingKeyType) - - err := NewStore(rootDir, testDefaultSource).Ready() - if err == nil { - t.Fatal("Ready succeeded, want oversized key error") - } - if !IsCode(err, CodeSourceInvalidKey) { - t.Fatalf("error = %v, want %s", err, CodeSourceInvalidKey) - } -} - -func TestStoreRejectsTooManySourceEntries(t *testing.T) { - t.Parallel() - - rootDir := t.TempDir() - for i := 1; i <= maxSourceEntries+1; i++ { - writeSource(t, rootDir, fmt.Sprintf("utxo%d", i)) - } - - _, err := NewStore(rootDir, testDefaultSource).List() - if err == nil { - t.Fatal("List succeeded, want source count cap error") - } - if !IsCode(err, CodeSourceReadFailed) { - t.Fatalf("error = %v, want %s", err, CodeSourceReadFailed) - } -} - -func writeSource(t *testing.T, rootDir string, name string) { - t.Helper() - - sourceDir := filepath.Join(rootDir, name) - requireNoError(t, os.MkdirAll(sourceDir, 0o700)) - writeAddress(t, filepath.Join(sourceDir, "utxo.addr"), testAddress) - writeKey(t, filepath.Join(sourceDir, "utxo.vkey"), verificationKeyType) - writeKey(t, filepath.Join(sourceDir, "utxo.skey"), signingKeyType) -} - -func writeAddress(t *testing.T, path string, address string) { - t.Helper() - - writeSourceFile(t, path, address) -} - -func writeKey(t *testing.T, path string, keyType string) { - t.Helper() - - cborHex := testVerificationKeyCBORHex - if keyType == signingKeyType { - cborHex = testSigningKeyCBORHex - } - writeKeyWithCBORHex(t, path, keyType, cborHex) -} - -func writeKeyWithCBORHex(t *testing.T, path string, keyType string, cborHex string) { - t.Helper() - - writeSourceFile(t, path, `{ - "type": "`+keyType+`", - "description": "Genesis Initial UTxO Key", - "cborHex": "`+cborHex+`", - "testSecretMaterial": "`+testSecretMaterial+`" -}`) -} - -func writeSourceFile(t *testing.T, path string, contents string) { - t.Helper() - - requireNoError(t, os.WriteFile(path, []byte(contents), 0o600)) -} - -func sourceNames(sources []Source) []string { - names := make([]string, 0, len(sources)) - for _, source := range sources { - names = append(names, source.Name) - } - - return names -} - -func equalStrings(left []string, right []string) bool { - if len(left) != len(right) { - return false - } - for i := range left { - if left[i] != right[i] { - return false - } - } - - return true -} - -func requireNoError(t *testing.T, err error) { - t.Helper() - - if err != nil { - t.Fatalf("unexpected error: %v", err) - } -} - -func deriveTestVerificationKeyHex(signingKeyHex string) string { - signingKey := mustDecodeHex(signingKeyHex) - privateKey := ed25519.NewKeyFromSeed(signingKey) - publicKey := privateKey.Public().(ed25519.PublicKey) - - return hex.EncodeToString(publicKey) -} - -func mustDeriveTestnetPaymentAddress(verificationKeyHex string) string { - address, err := DeriveTestnetPaymentAddress(verificationKeyHex) - if err != nil { - panic(err) - } - - return address -} - -func cborHexForRawKey(rawHex string) string { - return "5820" + rawHex -} - -func mustDecodeHex(value string) []byte { - decoded, err := hex.DecodeString(value) - if err != nil { - panic(err) - } - - return decoded -} diff --git a/services/faucet/internal/topup/service.go b/services/faucet/internal/topup/service.go deleted file mode 100644 index 54651412..00000000 --- a/services/faucet/internal/topup/service.go +++ /dev/null @@ -1,338 +0,0 @@ -package topup - -import ( - "context" - "errors" - "fmt" - "strings" - "sync" - - "github.com/meigma/yacd/internal/cardano/tx" - "github.com/meigma/yacd/services/faucet/internal/sources" -) - -const ( - // DefaultMinLovelace is the default lower bound for exact top-up requests. - DefaultMinLovelace int64 = 1_000_000 - - // DefaultMaxLovelace is the default upper bound for a single top-up request. - DefaultMaxLovelace int64 = 10_000_000_000 - - // CodeInvalidRequest identifies caller input that cannot be submitted. - CodeInvalidRequest = "invalid_request" - // CodeSourceNotFound identifies a missing faucet source. - CodeSourceNotFound = sources.CodeSourceNotFound - // CodeSourceUnavailable identifies a source that exists but cannot be used. - CodeSourceUnavailable = "source_unavailable" - // CodeChainUnavailable identifies a chain client or submission failure. - CodeChainUnavailable = "chain_unavailable" -) - -// SourceReader loads private faucet source material for top-up submission. -type SourceReader interface { - // DefaultName returns the configured default source name. - DefaultName() string - // ReadFundingSource returns the selected source with private key material. - ReadFundingSource(ctx context.Context, name string) (sources.FundingSource, error) -} - -// Service coordinates faucet top-up requests. -type Service struct { - sourceReader SourceReader - submitter tx.Submitter - config Config - locks *sourceLocks - pending *pendingInputs -} - -// Config describes top-up service limits. -type Config struct { - // MinLovelace is the lower bound for a single top-up request. - MinLovelace int64 - // MaxLovelace is the upper bound for a single top-up request. - MaxLovelace int64 -} - -type sourceLocks struct { - mu sync.Mutex - locks map[string]*sync.Mutex -} - -// Request describes one exact top-up submission request. -type Request struct { - // Source is the optional source name. Empty selects the configured default. - Source string - // DestinationAddress is the Cardano testnet recipient address. - DestinationAddress string - // Lovelace is the exact amount to submit. - Lovelace int64 -} - -// Result describes a submitted top-up transaction. -type Result struct { - // TxID is the submitted transaction id as lowercase hex. - TxID string `json:"txId"` - // Source is the faucet source name used for the transaction. - Source string `json:"source"` - // SourceAddress is the faucet source payment address. - SourceAddress string `json:"sourceAddress"` - // DestinationAddress is the requested recipient address. - DestinationAddress string `json:"destinationAddress"` - // Lovelace is the exact submitted amount. - Lovelace int64 `json:"lovelace"` -} - -// Error is a structured top-up error. -type Error struct { - // Code is a stable machine-readable error code. - Code string - // Message is a human-readable error message. - Message string - // Cause is the wrapped lower-level error, when one exists. - Cause error -} - -// NewService constructs a top-up service from source and transaction -// dependencies. -func NewService(sourceReader SourceReader, submitter tx.Submitter, config Config) Service { - return Service{ - sourceReader: sourceReader, - submitter: submitter, - config: config, - locks: newSourceLocks(), - pending: newPendingInputs(), - } -} - -// Submit submits one exact faucet top-up transaction. -func (s Service) Submit(ctx context.Context, request Request) (Result, error) { - if ctx == nil { - ctx = context.Background() - } - if err := ctx.Err(); err != nil { - return Result{}, WrapError(CodeChainUnavailable, "submit top-up: context canceled", err) - } - if s.sourceReader == nil { - return Result{}, Errorf(CodeSourceUnavailable, "submit top-up: source reader is not configured") - } - if s.submitter == nil { - return Result{}, Errorf(CodeChainUnavailable, "submit top-up: transaction submitter is not configured") - } - - sourceName := strings.TrimSpace(request.Source) - if sourceName == "" { - sourceName = s.sourceReader.DefaultName() - } - config := s.normalizedConfig() - if err := validateRequest(sourceName, request, config); err != nil { - return Result{}, err - } - - source, err := s.sourceReader.ReadFundingSource(ctx, sourceName) - if err != nil { - return Result{}, mapSourceError(sourceName, err) - } - if request.DestinationAddress == source.Address { - return Result{}, Errorf(CodeInvalidRequest, "destination address must not equal source address") - } - - unlock := s.lockSource(source.Name) - defer unlock() - - excludedInputKeys := s.pending.snapshot(source.Name) - chainResult, err := s.submitter.Submit(ctx, tx.Request{ - SourceName: source.Name, - SourceAddress: source.Address, - VerificationKeyHex: source.VerificationKeyHex, - SigningKeyHex: source.SigningKeyHex, - DestinationAddress: request.DestinationAddress, - Lovelace: request.Lovelace, - ExcludeInputKeys: excludedInputKeys, - }) - if err != nil { - return Result{}, mapChainError(err) - } - if strings.TrimSpace(chainResult.TxID) == "" { - return Result{}, Errorf(CodeChainUnavailable, "submit top-up transaction returned an empty transaction id") - } - if len(chainResult.SpentInputKeys) == 0 { - return Result{}, Errorf(CodeChainUnavailable, "submit top-up transaction returned no spent source inputs") - } - s.pending.add(source.Name, chainResult.SpentInputKeys) - - return Result{ - TxID: strings.ToLower(strings.TrimSpace(chainResult.TxID)), - Source: source.Name, - SourceAddress: source.Address, - DestinationAddress: request.DestinationAddress, - Lovelace: request.Lovelace, - }, nil -} - -func validateRequest(sourceName string, request Request, config Config) error { - if err := sources.ValidateName(sourceName); err != nil { - return WrapError(CodeInvalidRequest, "invalid source name", err) - } - if err := sources.ValidateTestnetAddress(request.DestinationAddress); err != nil { - return WrapError(CodeInvalidRequest, "invalid destination address", err) - } - if request.Lovelace <= 0 { - return Errorf(CodeInvalidRequest, "lovelace must be positive") - } - if request.Lovelace < config.MinLovelace { - return Errorf(CodeInvalidRequest, "lovelace must be at least %d", config.MinLovelace) - } - if request.Lovelace > config.MaxLovelace { - return Errorf(CodeInvalidRequest, "lovelace must be at most %d", config.MaxLovelace) - } - - return nil -} - -func mapSourceError(sourceName string, err error) error { - switch { - case sources.IsCode(err, sources.CodeSourceNotFound), sources.IsCode(err, sources.CodeSourceIncomplete): - return WrapError(CodeSourceNotFound, fmt.Sprintf("faucet source %q was not found", sourceName), err) - case sources.IsCode(err, sources.CodeInvalidSourceName): - return WrapError(CodeInvalidRequest, "invalid source name", err) - default: - return WrapError(CodeSourceUnavailable, fmt.Sprintf("faucet source %q is unavailable", sourceName), err) - } -} - -// mapChainError translates a transaction-engine error into a top-up error, -// preserving an already-structured top-up error from the source-read path. -func mapChainError(err error) error { - var topupErr *Error - if errors.As(err, &topupErr) { - return topupErr - } - switch { - case tx.IsCode(err, tx.CodeInvalidRequest): - return WrapError(CodeInvalidRequest, "submit top-up transaction", err) - default: - return WrapError(CodeChainUnavailable, "submit top-up transaction", err) - } -} - -func (s Service) normalizedConfig() Config { - config := s.config - if config.MinLovelace <= 0 { - config.MinLovelace = DefaultMinLovelace - } - if config.MaxLovelace <= 0 { - config.MaxLovelace = DefaultMaxLovelace - } - - return config -} - -func (s Service) lockSource(sourceName string) func() { - if s.locks == nil { - lock := &sync.Mutex{} - lock.Lock() - return lock.Unlock - } - - return s.locks.lock(sourceName) -} - -func newSourceLocks() *sourceLocks { - return &sourceLocks{ - locks: make(map[string]*sync.Mutex), - } -} - -type pendingInputs struct { - mu sync.Mutex - bySource map[string]map[string]struct{} -} - -func newPendingInputs() *pendingInputs { - return &pendingInputs{ - bySource: make(map[string]map[string]struct{}), - } -} - -func (p *pendingInputs) snapshot(sourceName string) []string { - if p == nil { - return nil - } - - p.mu.Lock() - defer p.mu.Unlock() - - inputs := p.bySource[sourceName] - if len(inputs) == 0 { - return nil - } - - result := make([]string, 0, len(inputs)) - for input := range inputs { - result = append(result, input) - } - - return result -} - -func (p *pendingInputs) add(sourceName string, inputKeys []string) { - if p == nil { - return - } - - p.mu.Lock() - defer p.mu.Unlock() - - inputs := p.bySource[sourceName] - if inputs == nil { - inputs = make(map[string]struct{}) - p.bySource[sourceName] = inputs - } - for _, inputKey := range inputKeys { - inputKey = strings.ToLower(strings.TrimSpace(inputKey)) - if inputKey == "" { - continue - } - inputs[inputKey] = struct{}{} - } -} - -func (s *sourceLocks) lock(sourceName string) func() { - s.mu.Lock() - lock, ok := s.locks[sourceName] - if !ok { - lock = &sync.Mutex{} - s.locks[sourceName] = lock - } - s.mu.Unlock() - - lock.Lock() - - return lock.Unlock -} - -func (e *Error) Error() string { - return e.Message -} - -// Unwrap returns the lower-level error that caused e. -func (e *Error) Unwrap() error { - return e.Cause -} - -// Errorf creates a structured top-up error. -func Errorf(code string, format string, args ...any) *Error { - return &Error{ - Code: code, - Message: fmt.Sprintf(format, args...), - } -} - -// WrapError creates a structured top-up error with a lower-level cause. -func WrapError(code string, message string, cause error) *Error { - return &Error{ - Code: code, - Message: message, - Cause: cause, - } -} diff --git a/services/faucet/internal/topup/service_test.go b/services/faucet/internal/topup/service_test.go deleted file mode 100644 index 26728046..00000000 --- a/services/faucet/internal/topup/service_test.go +++ /dev/null @@ -1,491 +0,0 @@ -package topup - -import ( - "context" - "encoding/json" - "errors" - "strings" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/meigma/yacd/internal/cardano/tx" - "github.com/meigma/yacd/services/faucet/internal/sources" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const ( - testDestinationAddress = "addr_test1vqy2n0vz5rlpykf6dcqn55xdcpey7mejyexlgj6370leayst4k6ta" - testSourceAddress = "addr_test1vqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs8fu43" - testVerificationHex = "0101010101010101010101010101010101010101010101010101010101010101" - testSigningHex = "0202020202020202020202020202020202020202020202020202020202020202" - testSpentInputKey = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:0" -) - -func TestServiceSubmitUsesDefaultSource(t *testing.T) { - t.Parallel() - - reader := &fakeSourceReader{ - defaultName: "utxo1", - sources: map[string]sources.FundingSource{ - "utxo1": testFundingSource("utxo1"), - }, - } - submitter := &fakeSubmitter{result: tx.Result{TxID: "ABC123", SpentInputKeys: []string{testSpentInputKey}}} - service := NewService(reader, submitter, testConfig()) - - result, err := service.Submit(context.Background(), Request{ - DestinationAddress: testDestinationAddress, - Lovelace: 1_000_000, - }) - - require.NoError(t, err) - assert.Equal(t, "utxo1", reader.names[0]) - require.Len(t, submitter.requests, 1) - assert.Equal(t, "utxo1", submitter.requests[0].SourceName) - assert.Equal(t, testDestinationAddress, submitter.requests[0].DestinationAddress) - assert.Equal(t, int64(1_000_000), submitter.requests[0].Lovelace) - assert.Equal(t, "abc123", result.TxID) - assert.Equal(t, "utxo1", result.Source) - assert.Equal(t, testSourceAddress, result.SourceAddress) - assert.Equal(t, testDestinationAddress, result.DestinationAddress) - assert.Equal(t, int64(1_000_000), result.Lovelace) -} - -func TestServiceSubmitUsesSelectedSource(t *testing.T) { - t.Parallel() - - reader := &fakeSourceReader{ - defaultName: "utxo1", - sources: map[string]sources.FundingSource{ - "utxo2": testFundingSource("utxo2"), - }, - } - submitter := &fakeSubmitter{result: tx.Result{TxID: "def456", SpentInputKeys: []string{testSpentInputKey}}} - service := NewService(reader, submitter, testConfig()) - - _, err := service.Submit(context.Background(), Request{ - Source: "utxo2", - DestinationAddress: testDestinationAddress, - Lovelace: 2_000_000, - }) - - require.NoError(t, err) - assert.Equal(t, "utxo2", reader.names[0]) - assert.Equal(t, "utxo2", submitter.requests[0].SourceName) -} - -func TestServiceSubmitRejectsInvalidRequests(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - request Request - }{ - { - name: "invalid source", - request: Request{ - Source: "wallet1", - DestinationAddress: testDestinationAddress, - Lovelace: 1_000_000, - }, - }, - { - name: "invalid address", - request: Request{ - DestinationAddress: "addr1qx2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3l62x5n0x", - Lovelace: 1_000_000, - }, - }, - { - name: "zero lovelace", - request: Request{ - DestinationAddress: testDestinationAddress, - }, - }, - { - name: "negative lovelace", - request: Request{ - DestinationAddress: testDestinationAddress, - Lovelace: -1, - }, - }, - { - name: "below min", - request: Request{ - DestinationAddress: testDestinationAddress, - Lovelace: 999_999, - }, - }, - { - name: "over max", - request: Request{ - DestinationAddress: testDestinationAddress, - Lovelace: 10_000_001, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - service := NewService(&fakeSourceReader{defaultName: "utxo1"}, &fakeSubmitter{}, testConfig()) - - _, err := service.Submit(context.Background(), tt.request) - - require.Error(t, err) - assertTopUpCode(t, err, CodeInvalidRequest) - }) - } -} - -func TestServiceSubmitMapsSourceErrors(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - sourceErr error - wantCode string - }{ - { - name: "not found", - sourceErr: &sources.Error{Code: sources.CodeSourceNotFound, Message: "missing"}, - wantCode: CodeSourceNotFound, - }, - { - name: "unavailable", - sourceErr: &sources.Error{Code: sources.CodeSourceInvalidKey, Message: "bad key"}, - wantCode: CodeSourceUnavailable, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - service := NewService( - &fakeSourceReader{defaultName: "utxo1", err: tt.sourceErr}, - &fakeSubmitter{}, - testConfig(), - ) - - _, err := service.Submit(context.Background(), Request{ - DestinationAddress: testDestinationAddress, - Lovelace: 1_000_000, - }) - - require.Error(t, err) - assertTopUpCode(t, err, tt.wantCode) - }) - } -} - -func TestServiceSubmitMapsTransactionFailure(t *testing.T) { - t.Parallel() - - service := NewService( - &fakeSourceReader{ - defaultName: "utxo1", - sources: map[string]sources.FundingSource{ - "utxo1": testFundingSource("utxo1"), - }, - }, - &fakeSubmitter{err: errors.New("chain failed")}, - testConfig(), - ) - - _, err := service.Submit(context.Background(), Request{ - DestinationAddress: testDestinationAddress, - Lovelace: 1_000_000, - }) - - require.Error(t, err) - assertTopUpCode(t, err, CodeChainUnavailable) -} - -func TestServiceSubmitRejectsSourceEqualsDestination(t *testing.T) { - t.Parallel() - - reader := &fakeSourceReader{ - defaultName: "utxo1", - sources: map[string]sources.FundingSource{ - "utxo1": testFundingSource("utxo1"), - }, - } - service := NewService(reader, &fakeSubmitter{}, testConfig()) - - _, err := service.Submit(context.Background(), Request{ - DestinationAddress: testSourceAddress, - Lovelace: 1_000_000, - }) - - require.Error(t, err) - assertTopUpCode(t, err, CodeInvalidRequest) -} - -func TestServiceSubmitSerializesSameSource(t *testing.T) { - t.Parallel() - - reader := &fakeSourceReader{ - defaultName: "utxo1", - sources: map[string]sources.FundingSource{ - "utxo1": testFundingSource("utxo1"), - }, - } - submitter := newBlockingSubmitter() - service := NewService(reader, submitter, testConfig()) - errs := make(chan error, 2) - - go func() { - _, err := service.Submit(context.Background(), Request{ - DestinationAddress: testDestinationAddress, - Lovelace: 1_000_000, - }) - errs <- err - }() - waitForSubmitStart(t, submitter) - - go func() { - _, err := service.Submit(context.Background(), Request{ - DestinationAddress: testDestinationAddress, - Lovelace: 1_000_000, - }) - errs <- err - }() - - select { - case <-submitter.started: - t.Fatal("second same-source top-up started before the first finished") - case <-time.After(50 * time.Millisecond): - } - - submitter.release() - waitForSubmitStart(t, submitter) - submitter.release() - - require.NoError(t, <-errs) - require.NoError(t, <-errs) - assert.Zero(t, submitter.overlaps.Load()) -} - -func TestServiceSubmitPassesPendingInputExclusions(t *testing.T) { - t.Parallel() - - reader := &fakeSourceReader{ - defaultName: "utxo1", - sources: map[string]sources.FundingSource{ - "utxo1": testFundingSource("utxo1"), - }, - } - submitter := &fakeSubmitter{ - results: []tx.Result{ - {TxID: "first", SpentInputKeys: []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:0"}}, - {TxID: "second", SpentInputKeys: []string{"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:1"}}, - }, - } - service := NewService(reader, submitter, testConfig()) - - _, err := service.Submit(context.Background(), Request{ - DestinationAddress: testDestinationAddress, - Lovelace: 1_000_000, - }) - require.NoError(t, err) - _, err = service.Submit(context.Background(), Request{ - DestinationAddress: testDestinationAddress, - Lovelace: 1_000_000, - }) - require.NoError(t, err) - - require.Len(t, submitter.requests, 2) - assert.Empty(t, submitter.requests[0].ExcludeInputKeys) - assert.ElementsMatch( - t, - []string{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:0"}, - submitter.requests[1].ExcludeInputKeys, - ) -} - -func TestServiceSubmitDoesNotRecordPendingInputsOnFailure(t *testing.T) { - t.Parallel() - - reader := &fakeSourceReader{ - defaultName: "utxo1", - sources: map[string]sources.FundingSource{ - "utxo1": testFundingSource("utxo1"), - }, - } - submitter := &fakeSubmitter{ - errs: []error{ - Errorf(CodeChainUnavailable, "chain failed"), - nil, - }, - results: []tx.Result{ - {}, - {TxID: "second", SpentInputKeys: []string{"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:1"}}, - }, - } - service := NewService(reader, submitter, testConfig()) - - _, err := service.Submit(context.Background(), Request{ - DestinationAddress: testDestinationAddress, - Lovelace: 1_000_000, - }) - require.Error(t, err) - _, err = service.Submit(context.Background(), Request{ - DestinationAddress: testDestinationAddress, - Lovelace: 1_000_000, - }) - require.NoError(t, err) - - require.Len(t, submitter.requests, 2) - assert.Empty(t, submitter.requests[1].ExcludeInputKeys) -} - -func TestResultJSONDoesNotExposeKeyMaterial(t *testing.T) { - t.Parallel() - - reader := &fakeSourceReader{ - defaultName: "utxo1", - sources: map[string]sources.FundingSource{ - "utxo1": testFundingSource("utxo1"), - }, - } - service := NewService(reader, &fakeSubmitter{result: tx.Result{TxID: "abc123", SpentInputKeys: []string{testSpentInputKey}}}, testConfig()) - - result, err := service.Submit(context.Background(), Request{ - DestinationAddress: testDestinationAddress, - Lovelace: 1_000_000, - }) - require.NoError(t, err) - - encoded, err := json.Marshal(result) - require.NoError(t, err) - assert.NotContains(t, string(encoded), testVerificationHex) - assert.NotContains(t, string(encoded), testSigningHex) - assert.False(t, strings.Contains(string(encoded), "SigningKey"), string(encoded)) -} - -func testConfig() Config { - return Config{ - MinLovelace: DefaultMinLovelace, - MaxLovelace: 10_000_000, - } -} - -func testFundingSource(name string) sources.FundingSource { - return sources.FundingSource{ - Name: name, - Address: testSourceAddress, - VerificationKeyHex: testVerificationHex, - SigningKeyHex: testSigningHex, - } -} - -type fakeSourceReader struct { - mu sync.Mutex - defaultName string - sources map[string]sources.FundingSource - err error - names []string -} - -func (f *fakeSourceReader) DefaultName() string { - return f.defaultName -} - -func (f *fakeSourceReader) ReadFundingSource(_ context.Context, name string) (sources.FundingSource, error) { - f.mu.Lock() - defer f.mu.Unlock() - - f.names = append(f.names, name) - if f.err != nil { - return sources.FundingSource{}, f.err - } - source, ok := f.sources[name] - if !ok { - return sources.FundingSource{}, &sources.Error{Code: sources.CodeSourceNotFound, Message: "missing"} - } - - return source, nil -} - -type fakeSubmitter struct { - mu sync.Mutex - result tx.Result - results []tx.Result - err error - errs []error - requests []tx.Request -} - -func (f *fakeSubmitter) Submit(_ context.Context, request tx.Request) (tx.Result, error) { - f.mu.Lock() - defer f.mu.Unlock() - - index := len(f.requests) - f.requests = append(f.requests, request) - if index < len(f.errs) && f.errs[index] != nil { - return tx.Result{}, f.errs[index] - } - if f.err != nil { - return tx.Result{}, f.err - } - - result := f.result - if index < len(f.results) { - result = f.results[index] - } - if len(result.SpentInputKeys) == 0 { - result.SpentInputKeys = []string{testSpentInputKey} - } - - return result, nil -} - -type blockingSubmitter struct { - started chan struct{} - releases chan struct{} - active atomic.Int32 - overlaps atomic.Int32 -} - -func newBlockingSubmitter() *blockingSubmitter { - return &blockingSubmitter{ - started: make(chan struct{}), - releases: make(chan struct{}), - } -} - -func (b *blockingSubmitter) Submit(_ context.Context, _ tx.Request) (tx.Result, error) { - if !b.active.CompareAndSwap(0, 1) { - b.overlaps.Add(1) - } - b.started <- struct{}{} - <-b.releases - b.active.Store(0) - - return tx.Result{TxID: "abc123", SpentInputKeys: []string{testSpentInputKey}}, nil -} - -func (b *blockingSubmitter) release() { - b.releases <- struct{}{} -} - -func waitForSubmitStart(t *testing.T, submitter *blockingSubmitter) { - t.Helper() - - select { - case <-submitter.started: - case <-time.After(time.Second): - t.Fatal("timed out waiting for top-up submission to start") - } -} - -func assertTopUpCode(t *testing.T, err error, code string) { - t.Helper() - - var topupErr *Error - require.ErrorAs(t, err, &topupErr) - assert.Equal(t, code, topupErr.Code) -} diff --git a/test/chainsaw/manager-smoke/chainsaw-test.yaml b/test/chainsaw/manager-smoke/chainsaw-test.yaml index 08c1b933..1b95f60d 100644 --- a/test/chainsaw/manager-smoke/chainsaw-test.yaml +++ b/test/chainsaw/manager-smoke/chainsaw-test.yaml @@ -3,7 +3,7 @@ kind: Test metadata: name: manager-smoke spec: - description: Verify the YACD operator foundation deploys, exposes protected metrics, and reconciles one CardanoNetwork to Kubernetes workload plus Ogmios, Kupo, and faucet readiness. + description: Verify the YACD operator foundation deploys, exposes protected metrics, and reconciles one CardanoNetwork to Kubernetes workload plus Ogmios and Kupo readiness with a genesis-funded faucet wallet. concurrent: false failFast: true catch: @@ -38,8 +38,6 @@ spec: kubectl describe service -n yacd-smoke phase4-smoke-node kubectl describe service -n yacd-smoke phase4-smoke-ogmios kubectl describe service -n yacd-smoke phase4-smoke-kupo - kubectl describe service -n yacd-smoke phase4-smoke-faucet - kubectl describe secret -n yacd-smoke phase4-smoke-faucet-auth kubectl describe service -n yacd-smoke phase4-smoke-artifacts kubectl describe deployment -n yacd-smoke phase4-smoke-node kubectl describe deployment -n yacd-smoke phase6-managed-postgres @@ -50,8 +48,6 @@ spec: kubectl logs -n yacd-smoke curl-ogmios || true echo "== Kupo curl logs ==" kubectl logs -n yacd-smoke curl-kupo || true - echo "== Faucet curl logs ==" - kubectl logs -n yacd-smoke curl-faucet || true echo "== db-sync psql logs ==" kubectl logs -n yacd-smoke dbsync-psql || true echo "== db-sync psql pod ==" @@ -72,7 +68,7 @@ spec: export KUBECTL_KUBERC="${KUBECTL_KUBERC:-false}" kubectl create namespace yacd-system --dry-run=client -o yaml | kubectl apply -f - kubectl label --overwrite namespace yacd-system pod-security.kubernetes.io/enforce=restricted - IMG="${IMG:-example.com/yacd:v0.0.1}" FAUCET_IMG="${FAUCET_IMG:-example.com/yacd-faucet:v0.0.1}" LOCAL_IMAGE=true moon run root:deploy + IMG="${IMG:-example.com/yacd:v0.0.1}" LOCAL_IMAGE=true moon run root:deploy - wait: apiVersion: apps/v1 kind: Deployment @@ -244,28 +240,6 @@ spec: protocol: TCP port: 1442 targetPort: kupo - - assert: - resource: - apiVersion: v1 - kind: Service - metadata: - namespace: yacd-smoke - name: phase4-smoke-faucet - spec: - type: ClusterIP - ports: - - name: faucet - protocol: TCP - port: 8080 - targetPort: faucet - - assert: - resource: - apiVersion: v1 - kind: Secret - metadata: - namespace: yacd-smoke - name: phase4-smoke-faucet-auth - type: Opaque - assert: # The genesis-funded faucet wallet Secret carries the controller- # generated payment key envelopes and the derived address. It is the @@ -321,7 +295,6 @@ spec: artifacts_ready="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.conditions[?(@.type=="ArtifactsReady")].status}' 2>/dev/null || true)" ogmios_ready="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.conditions[?(@.type=="OgmiosReady")].status}' 2>/dev/null || true)" kupo_ready="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.conditions[?(@.type=="KupoReady")].status}' 2>/dev/null || true)" - faucet_ready="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.conditions[?(@.type=="FaucetReady")].status}' 2>/dev/null || true)" progressing="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.conditions[?(@.type=="Progressing")].status}' 2>/dev/null || true)" degraded="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.conditions[?(@.type=="Degraded")].status}' 2>/dev/null || true)" service_name="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.endpoints.nodeToNode.serviceName}' 2>/dev/null || true)" @@ -333,10 +306,6 @@ spec: kupo_service_name="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.endpoints.kupo.serviceName}' 2>/dev/null || true)" kupo_port="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.endpoints.kupo.port}' 2>/dev/null || true)" kupo_url="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.endpoints.kupo.url}' 2>/dev/null || true)" - faucet_service_name="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.endpoints.faucet.serviceName}' 2>/dev/null || true)" - faucet_port="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.endpoints.faucet.port}' 2>/dev/null || true)" - faucet_url="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.endpoints.faucet.url}' 2>/dev/null || true)" - faucet_auth_secret="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.faucet.authSecretName}' 2>/dev/null || true)" artifacts_service_name="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.endpoints.artifacts.serviceName}' 2>/dev/null || true)" artifacts_endpoint_url="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.endpoints.artifacts.url}' 2>/dev/null || true)" if [ "${ready}" = "True" ] && @@ -344,7 +313,6 @@ spec: [ "${artifacts_ready}" = "True" ] && [ "${ogmios_ready}" = "True" ] && [ "${kupo_ready}" = "True" ] && - [ "${faucet_ready}" = "True" ] && [ "${progressing}" = "False" ] && [ "${degraded}" = "False" ] && [ "${service_name}" = "phase4-smoke-node" ] && @@ -356,10 +324,6 @@ spec: [ "${kupo_service_name}" = "phase4-smoke-kupo" ] && [ "${kupo_port}" = "1442" ] && [ "${kupo_url}" = "http://phase4-smoke-kupo.yacd-smoke.svc.cluster.local:1442" ] && - [ "${faucet_service_name}" = "phase4-smoke-faucet" ] && - [ "${faucet_port}" = "8080" ] && - [ "${faucet_url}" = "http://phase4-smoke-faucet.yacd-smoke.svc.cluster.local:8080" ] && - [ "${faucet_auth_secret}" = "phase4-smoke-faucet-auth" ] && [ "${artifacts_service_name}" = "phase4-smoke-artifacts" ] && [ "${artifacts_endpoint_url}" = "http://phase4-smoke-artifacts.yacd-smoke.svc.cluster.local:8090" ]; then exit 0 @@ -589,8 +553,6 @@ spec: assert data["namespace"] == "yacd-smoke" assert data["endpoints"]["ogmios"]["url"] == "ws://phase4-smoke-ogmios.yacd-smoke.svc.cluster.local:1337" assert data["endpoints"]["kupo"]["url"] == "http://phase4-smoke-kupo.yacd-smoke.svc.cluster.local:1442" - assert data["endpoints"]["faucet"]["url"] == "http://phase4-smoke-faucet.yacd-smoke.svc.cluster.local:8080" - assert data["faucet"]["authSecretName"] == "phase4-smoke-faucet-auth" assert any(condition["type"] == "Ready" and condition["status"] == "True" for condition in data["conditions"]) PY kubectl apply -f - <<'YAML' @@ -647,97 +609,6 @@ spec: --- apiVersion: v1 kind: Pod - metadata: - name: curl-faucet - namespace: yacd-smoke - spec: - restartPolicy: Never - securityContext: - runAsNonRoot: true - seccompProfile: - type: RuntimeDefault - volumes: - - name: tmp - emptyDir: {} - containers: - - name: curl - image: curlimages/curl:8.11.1 - env: - - name: FAUCET_TOKEN - valueFrom: - secretKeyRef: - name: phase4-smoke-faucet-auth - key: token - volumeMounts: - - name: tmp - mountPath: /tmp - command: - - /bin/sh - - -c - args: - - | - set -eu - for i in $(seq 1 30); do - ready_status="$(curl -sS -o /tmp/faucet-ready.json -w '%{http_code}' \ - 'http://phase4-smoke-faucet.yacd-smoke.svc.cluster.local:8080/readyz' || true)" - echo "faucet ready status: ${ready_status}" - cat /tmp/faucet-ready.json || true - if [ "${ready_status}" = "200" ]; then - break - fi - sleep 2 - done - [ "${ready_status}" = "200" ] - - unauthorized_status="$(curl -sS -o /tmp/faucet-unauthorized.json -w '%{http_code}' \ - -H 'Content-Type: application/json' \ - -d '{"address":"addr_test1invalid","lovelace":1000000}' \ - 'http://phase4-smoke-faucet.yacd-smoke.svc.cluster.local:8080/v1/topups' || true)" - echo "faucet unauthorized status: ${unauthorized_status}" - cat /tmp/faucet-unauthorized.json || true - [ "${unauthorized_status}" = "401" ] - - authed_status="$(curl -sS -o /tmp/faucet-authed.json -w '%{http_code}' \ - -H "Authorization: Bearer ${FAUCET_TOKEN}" \ - -H 'Content-Type: application/json' \ - -d '{"address":"","lovelace":1000000}' \ - 'http://phase4-smoke-faucet.yacd-smoke.svc.cluster.local:8080/v1/topups' || true)" - echo "faucet authed validation status: ${authed_status}" - cat /tmp/faucet-authed.json || true - [ "${authed_status}" = "400" ] - - source_status="$(curl -sS -o /tmp/faucet-source.json -w '%{http_code}' \ - 'http://phase4-smoke-faucet.yacd-smoke.svc.cluster.local:8080/v1/sources/utxo1' || true)" - echo "faucet source status: ${source_status}" - cat /tmp/faucet-source.json || true - [ "${source_status}" = "200" ] - destination="$(sed -n 's/.*"address":"\([^"]*\)".*/\1/p' /tmp/faucet-source.json)" - [ -n "${destination}" ] - - topup_status="$(curl -sS -o /tmp/faucet-topup.json -w '%{http_code}' \ - -H "Authorization: Bearer ${FAUCET_TOKEN}" \ - -H 'Content-Type: application/json' \ - -d "{\"address\":\"${destination}\",\"lovelace\":1000000,\"source\":\"utxo2\"}" \ - 'http://phase4-smoke-faucet.yacd-smoke.svc.cluster.local:8080/v1/topups' || true)" - echo "faucet top-up status: ${topup_status}" - cat /tmp/faucet-topup.json || true - [ "${topup_status}" = "200" ] - grep -q '"txId"' /tmp/faucet-topup.json - grep -q '"source":"utxo2"' /tmp/faucet-topup.json - grep -q '"lovelace":1000000' /tmp/faucet-topup.json - securityContext: - readOnlyRootFilesystem: true - allowPrivilegeEscalation: false - capabilities: - drop: - - ALL - runAsNonRoot: true - runAsUser: 1000 - seccompProfile: - type: RuntimeDefault - --- - apiVersion: v1 - kind: Pod metadata: name: curl-kupo namespace: yacd-smoke @@ -803,53 +674,28 @@ spec: jsonPath: path: '{.status.phase}' value: Succeeded - - wait: - apiVersion: v1 - kind: Pod - namespace: yacd-smoke - name: curl-faucet - timeout: 3m - for: - jsonPath: - path: '{.status.phase}' - value: Succeeded - script: timeout: 5m content: | set -eu export KUBECTL_KUBERC="${KUBECTL_KUBERC:-false}" - # The faucet requires kupo, so both are disabled in the same patch; - # otherwise the spec is rejected as unsupported. Disabling the faucet - # also tears down the owned genesis-funded faucet wallet Secret. - kubectl patch cardanonetwork -n yacd-smoke phase4-smoke --type=merge -p '{"spec":{"chainAPI":{"kupo":{"enabled":false,"image":"cardanosolutions/kupo:v2.11.0","port":1442},"faucet":{"enabled":false,"port":8080,"defaultSource":"utxo1","minTopUpLovelace":1000000,"maxTopUpLovelace":100000000000}}}}' + # Disabling kupo tears down its owned Service and clears its endpoint. + # The genesis-funded faucet wallet is gated on local mode alone (not on + # any chain API), so it MUST survive this patch; the success branch + # below asserts the Secret still exists after the chain-API change. + kubectl patch cardanonetwork -n yacd-smoke phase4-smoke --type=merge -p '{"spec":{"chainAPI":{"kupo":{"enabled":false,"image":"cardanosolutions/kupo:v2.11.0","port":1442}}}}' for _ in $(seq 1 60); do if kubectl get service -n yacd-smoke phase4-smoke-kupo >/dev/null 2>&1; then sleep 5 continue fi - if kubectl get service -n yacd-smoke phase4-smoke-faucet >/dev/null 2>&1; then - sleep 5 - continue - fi - if kubectl get secret -n yacd-smoke phase4-smoke-faucet-auth >/dev/null 2>&1; then - sleep 5 - continue - fi - if kubectl get secret -n yacd-smoke phase4-smoke-wallet-faucet >/dev/null 2>&1; then - sleep 5 - continue - fi ready="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || true)" node_ready="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.conditions[?(@.type=="NodeReady")].status}' 2>/dev/null || true)" artifacts_ready="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.conditions[?(@.type=="ArtifactsReady")].status}' 2>/dev/null || true)" ogmios_ready="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.conditions[?(@.type=="OgmiosReady")].status}' 2>/dev/null || true)" kupo_ready="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.conditions[?(@.type=="KupoReady")].status}' 2>/dev/null || true)" kupo_reason="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.conditions[?(@.type=="KupoReady")].reason}' 2>/dev/null || true)" - faucet_ready="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.conditions[?(@.type=="FaucetReady")].status}' 2>/dev/null || true)" - faucet_reason="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.conditions[?(@.type=="FaucetReady")].reason}' 2>/dev/null || true)" kupo_endpoint="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.endpoints.kupo.serviceName}' 2>/dev/null || true)" - faucet_endpoint="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.endpoints.faucet.serviceName}' 2>/dev/null || true)" - faucet_auth_secret="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.faucet.authSecretName}' 2>/dev/null || true)" artifacts_endpoint_url="$(kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o jsonpath='{.status.endpoints.artifacts.url}' 2>/dev/null || true)" if [ "${ready}" = "True" ] && [ "${node_ready}" = "True" ] && @@ -857,12 +703,15 @@ spec: [ "${ogmios_ready}" = "True" ] && [ "${kupo_ready}" = "False" ] && [ "${kupo_reason}" = "KupoDisabled" ] && - [ "${faucet_ready}" = "False" ] && - [ "${faucet_reason}" = "FaucetDisabled" ] && [ -z "${kupo_endpoint}" ] && - [ -z "${faucet_endpoint}" ] && - [ -z "${faucet_auth_secret}" ] && [ "${artifacts_endpoint_url}" = "http://phase4-smoke-artifacts.yacd-smoke.svc.cluster.local:8090" ]; then + # The faucet wallet is gated on local mode alone: disabling a + # chain API must not remove it. Assert the Secret survived. + if ! kubectl get secret -n yacd-smoke phase4-smoke-wallet-faucet >/dev/null 2>&1; then + echo "faucet wallet Secret was unexpectedly removed by the kupo-disable patch" >&2 + kubectl get secret -n yacd-smoke || true + exit 1 + fi exit 0 fi kubectl get cardanonetwork -n yacd-smoke phase4-smoke -o yaml || true diff --git a/test/chart/rbac_test.go b/test/chart/rbac_test.go index fa941d2a..12998436 100644 --- a/test/chart/rbac_test.go +++ b/test/chart/rbac_test.go @@ -86,7 +86,7 @@ func TestKyvernoImageVerificationPolicyRendersGitHubAttestationPolicy(t *testing requireNestedString( t, policy.Object, - "Verify release attestations for YACD manager and faucet images.", + "Verify release attestations for YACD manager images.", "metadata", "annotations", "policies.kyverno.io/description", @@ -104,8 +104,6 @@ func TestKyvernoImageVerificationPolicyRendersGitHubAttestationPolicy(t *testing wantRefs := []string{ "ghcr.io/meigma/yacd:*", "ghcr.io/meigma/yacd@*", - "ghcr.io/meigma/yacd/faucet:*", - "ghcr.io/meigma/yacd/faucet@*", } if !reflect.DeepEqual(gotRefs, wantRefs) { t.Fatalf("unexpected imageReferences: got %v, want %v", gotRefs, wantRefs)