Repository: allenporter/flux-local (language: python)

Issue:
Placeholder raises ConstructorError
```
yaml.constructor.ConstructorError: could not determine a constructor for the tag 'tag:yaml.org,2002:PLACEHOLDER!!'
```

https://github.com/rafaribe/home-ops/actions/runs/9650191118/job/26615316089#step:8:1919
https://github.com/rafaribe/home-ops/actions/runs/9650337292/job/26615770418#step:8:1878


Discussion / hints:
This was changed in https://github.com/allenporter/flux-local/pull/620 because of a different parse error

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 52a0d8eb..afbff984 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,8 +4,10 @@ repos:
     rev: v4.6.0
     hooks:
       - id: trailing-whitespace
+        exclude: '^tests/.*/__snapshots__/.*.ambr$'
       - id: end-of-file-fixer
       - id: check-yaml
+        exclude: '^tests/testdata/cluster8/apps/.*\.yaml$'
       - id: check-added-large-files
   - repo: https://github.com/psf/black
     rev: 24.4.2
diff --git a/flux_local/manifest.py b/flux_local/manifest.py
index 1bef480c..2d033f23 100644
--- a/flux_local/manifest.py
+++ b/flux_local/manifest.py
@@ -46,7 +46,7 @@
 SECRET_KIND = "Secret"
 CONFIG_MAP_KIND = "ConfigMap"
 DEFAULT_NAMESPACE = "flux-system"
-VALUE_PLACEHOLDER = "!!PLACEHOLDER!!"
+VALUE_PLACEHOLDER = "..PLACEHOLDER.."
 VALUE_B64_PLACEHOLDER = base64.b64encode(VALUE_PLACEHOLDER.encode())
 HELM_REPOSITORY = "HelmRepository"
 GIT_REPOSITORY = "GitRepository"


Test patch:
diff --git a/tests/__snapshots__/test_values.ambr b/tests/__snapshots__/test_values.ambr
index 75b0705a..9133e805 100644
--- a/tests/__snapshots__/test_values.ambr
+++ b/tests/__snapshots__/test_values.ambr
@@ -22,7 +22,24 @@
       'tag': '7.0.6',
     }),
     'tls': dict({
-      'crt': '!!PLACEHOLDER!!',
+      'crt': '..PLACEHOLDER..',
+    }),
+  })
+# ---
+# name: test_value_references.1
+  dict({
+    'apiServerProxyConfig': dict({
+      'mode': 'true',
+    }),
+    'oauth': dict({
+      'clientId': '..PLACEHOLDER..',
+      'clientSecret': '..PLACEHOLDER..',
+    }),
+    'operatorConfig': dict({
+      'defaultTags': list([
+        'tag:k8s',
+      ]),
+      'hostname': 'tailscale-operator',
     }),
   })
 # ---
diff --git a/tests/test_values.py b/tests/test_values.py
index c00ca755..b5f797af 100644
--- a/tests/test_values.py
+++ b/tests/test_values.py
@@ -34,10 +34,13 @@ async def test_value_references(snapshot: SnapshotAssertion) -> None:
     assert len(manifest.clusters[0].kustomizations) == 2
     ks = manifest.clusters[0].kustomizations[0]
     assert ks.name == "apps"
-    assert len(ks.helm_releases) == 1
+    assert len(ks.helm_releases) == 2
     hr = ks.helm_releases[0]
     assert hr.name == "podinfo"
     assert hr.values == snapshot
+    hr = ks.helm_releases[1]
+    assert hr.name == "tailscale-operator"
+    assert hr.values == snapshot
 
 
 def test_values_references_with_values_key() -> None:
@@ -138,7 +141,6 @@ def test_values_references_with_missing_values_key() -> None:
     }
 
 
-
 def test_values_references_with_missing_secret() -> None:
     """Test for expanding a value reference with a missing secret."""
     hr = HelmRelease(
@@ -170,7 +172,7 @@ def test_values_references_with_missing_secret() -> None:
     assert updated_hr.values == {
         "test": "test",
         "target": {
-            "path": "!!PLACEHOLDER!!",
+            "path": "..PLACEHOLDER..",
         },
     }
 
@@ -435,8 +437,8 @@ def test_values_references_secret() -> None:
     assert updated_hr.values == {
         "test": "test",
         "target": {
-            "path1": "!!PLACEHOLDER!!",
-            "path2": "!!PLACEHOLDER!!",
+            "path1": "..PLACEHOLDER..",
+            "path2": "..PLACEHOLDER..",
         },
     }
 
@@ -569,4 +571,7 @@ def test_cluster_config() -> None:
     config = ks_cluster_config([ks1, ks2])
     # Ensure can be called repeatedly
     assert len(list(config.secrets)) == 2
-    assert [ s.name for s in config.secrets ] == ['test-values-secret','other-values-secret']
+    assert [s.name for s in config.secrets] == [
+        "test-values-secret",
+        "other-values-secret",
+    ]
diff --git a/tests/testdata/cluster8/apps/kustomization.yaml b/tests/testdata/cluster8/apps/kustomization.yaml
index eb70af57..356d9c51 100644
--- a/tests/testdata/cluster8/apps/kustomization.yaml
+++ b/tests/testdata/cluster8/apps/kustomization.yaml
@@ -6,3 +6,4 @@ resources:
   - podinfo.yaml
   - podinfo-values.yaml
   - podinfo-tls-values.yaml
+  - tailscale.yaml
diff --git a/tests/testdata/cluster8/apps/tailscale.yaml b/tests/testdata/cluster8/apps/tailscale.yaml
new file mode 100644
index 00000000..3982bdac
--- /dev/null
+++ b/tests/testdata/cluster8/apps/tailscale.yaml
@@ -0,0 +1,44 @@
+---
+# yaml-language-server: $schema=https://kubernetes-schemas.pages.dev/source.toolkit.fluxcd.io/helmrepository_v1.json
+apiVersion: source.toolkit.fluxcd.io/v1
+kind: HelmRepository
+metadata:
+  name: tailscale
+  namespace: flux-system
+spec:
+  interval: 30m
+  url: https://pkgs.tailscale.com/helmcharts
+  timeout: 3m
+---
+apiVersion: helm.toolkit.fluxcd.io/v2beta2
+kind: HelmRelease
+metadata:
+  name: tailscale-operator
+  namespace: network
+spec:
+  interval: 30m
+  chart:
+    spec:
+      chart: tailscale-operator
+      version: 1.68.1
+      interval: 30m
+      sourceRef:
+        kind: HelmRepository
+        name: tailscale
+        namespace: flux-system
+  values:
+    operatorConfig:
+      hostname: "tailscale-operator"
+      defaultTags:
+        - "tag:k8s"
+    apiServerProxyConfig:
+      mode: "true"
+  valuesFrom:
+    - kind: Secret
+      name: tailscale-operator
+      valuesKey: client_id
+      targetPath: oauth.clientId
+    - kind: Secret
+      name: tailscale-operator
+      valuesKey: client_secret
+      targetPath: oauth.clientSecret
diff --git a/tests/tool/__snapshots__/test_build.ambr b/tests/tool/__snapshots__/test_build.ambr
index 332e1e97..5c07f9e2 100644
--- a/tests/tool/__snapshots__/test_build.ambr
+++ b/tests/tool/__snapshots__/test_build.ambr
@@ -992,6 +992,61 @@
     annotations:
       config.kubernetes.io/index: '5'
       internal.config.kubernetes.io/index: '5'
+  ---
+  apiVersion: source.toolkit.fluxcd.io/v1
+  kind: HelmRepository
+  metadata:
+    labels:
+      kustomize.toolkit.fluxcd.io/name: apps
+      kustomize.toolkit.fluxcd.io/namespace: flux-system
+    name: tailscale
+    namespace: flux-system
+    annotations:
+      config.kubernetes.io/index: '6'
+      internal.config.kubernetes.io/index: '6'
+  spec:
+    interval: 30m
+    timeout: 3m
+    url: https://pkgs.tailscale.com/helmcharts
+  ---
+  apiVersion: helm.toolkit.fluxcd.io/v2beta2
+  kind: HelmRelease
+  metadata:
+    labels:
+      kustomize.toolkit.fluxcd.io/name: apps
+      kustomize.toolkit.fluxcd.io/namespace: flux-system
+    name: tailscale-operator
+    namespace: network
+    annotations:
+      config.kubernetes.io/index: '7'
+      internal.config.kubernetes.io/index: '7'
+  spec:
+    chart:
+      spec:
+        chart: tailscale-operator
+        interval: 30m
+        sourceRef:
+          kind: HelmRepository
+          name: tailscale
+          namespace: flux-system
+        version: 1.68.1
+    interval: 30m
+    values:
+      apiServerProxyConfig:
+        mode: "true"
+      operatorConfig:
+        defaultTags:
+        - tag:k8s
+        hostname: tailscale-operator
+    valuesFrom:
+    - kind: Secret
+      name: tailscale-operator
+      targetPath: oauth.clientId
+      valuesKey: client_id
+    - kind: Secret
+      name: tailscale-operator
+      targetPath: oauth.clientSecret
+      valuesKey: client_secret
   
   ---
   apiVersion: kustomize.toolkit.fluxcd.io/v1
@@ -1055,6 +1110,270 @@
       kind: GitRepository
       name: flux-system
   
+  ---
+  # Source: tailscale-operator/templates/operator-rbac.yaml
+  # Copyright (c) Tailscale Inc & AUTHORS
+  # SPDX-License-Identifier: BSD-3-Clause
+  apiVersion: v1
+  kind: ServiceAccount
+  metadata:
+    name: operator
+    namespace: network
+    annotations:
+      config.kubernetes.io/index: '0'
+      internal.config.kubernetes.io/index: '0'
+  ---
+  # Source: tailscale-operator/templates/proxy-rbac.yaml
+  # Copyright (c) Tailscale Inc & AUTHORS
+  # SPDX-License-Identifier: BSD-3-Clause
+  
+  apiVersion: v1
+  kind: ServiceAccount
+  metadata:
+    name: proxies
+    namespace: network
+    annotations:
+      config.kubernetes.io/index: '1'
+      internal.config.kubernetes.io/index: '1'
+  ---
+  # Source: tailscale-operator/templates/oauth-secret.yaml
+  # Copyright (c) Tailscale Inc & AUTHORS
+  # SPDX-License-Identifier: BSD-3-Clause
+  
+  apiVersion: v1
+  kind: Secret
+  metadata:
+    name: operator-oauth
+    namespace: network
+    annotations:
+      config.kubernetes.io/index: '2'
+      internal.config.kubernetes.io/index: '2'
+  stringData:
+    client_id: ..PLACEHOLDER..
+    client_secret: ..PLACEHOLDER..
+  ---
+  # Source: tailscale-operator/templates/apiserverproxy-rbac.yaml
+  # Copyright (c) Tailscale Inc & AUTHORS
+  # SPDX-License-Identifier: BSD-3-Clause
+  
+  apiVersion: rbac.authorization.k8s.io/v1
+  kind: ClusterRole
+  metadata:
+    name: tailscale-auth-proxy
+    annotations:
+      config.kubernetes.io/index: '6'
+      internal.config.kubernetes.io/index: '6'
+  rules:
+  - apiGroups: [""]
+    resources: ["users", "groups"]
+    verbs: ["impersonate"]
+  ---
+  # Source: tailscale-operator/templates/operator-rbac.yaml
+  apiVersion: rbac.authorization.k8s.io/v1
+  kind: ClusterRole
+  metadata:
+    name: tailscale-operator
+    annotations:
+      config.kubernetes.io/index: '7'
+      internal.config.kubernetes.io/index: '7'
+  rules:
+  - apiGroups: [""]
+    resources: ["events", "services", "services/status"]
+    verbs: ["*"]
+  - apiGroups: ["networking.k8s.io"]
+    resources: ["ingresses", "ingresses/status"]
+    verbs: ["*"]
+  - apiGroups: ["networking.k8s.io"]
+    resources: ["ingressclasses"]
+    verbs: ["get", "list", "watch"]
+  - apiGroups: ["tailscale.com"]
+    resources: ["connectors", "connectors/status", "proxyclasses", "proxyclasses/status"]
+    verbs: ["get", "list", "watch", "update"]
+  - apiGroups: ["tailscale.com"]
+    resources: ["dnsconfigs", "dnsconfigs/status"]
+    verbs: ["get", "list", "watch", "update"]
+  ---
+  # Source: tailscale-operator/templates/apiserverproxy-rbac.yaml
+  apiVersion: rbac.authorization.k8s.io/v1
+  kind: ClusterRoleBinding
+  metadata:
+    name: tailscale-auth-proxy
+    annotations:
+      config.kubernetes.io/index: '8'
+      internal.config.kubernetes.io/index: '8'
+  subjects:
+  - kind: ServiceAccount
+    name: operator
+    namespace: network
+  roleRef:
+    kind: ClusterRole
+    name: tailscale-auth-proxy
+    apiGroup: rbac.authorization.k8s.io
+  ---
+  # Source: tailscale-operator/templates/operator-rbac.yaml
+  apiVersion: rbac.authorization.k8s.io/v1
+  kind: ClusterRoleBinding
+  metadata:
+    name: tailscale-operator
+    annotations:
+      config.kubernetes.io/index: '9'
+      internal.config.kubernetes.io/index: '9'
+  subjects:
+  - kind: ServiceAccount
+    name: operator
+    namespace: network
+  roleRef:
+    kind: ClusterRole
+    name: tailscale-operator
+    apiGroup: rbac.authorization.k8s.io
+  ---
+  # Source: tailscale-operator/templates/operator-rbac.yaml
+  apiVersion: rbac.authorization.k8s.io/v1
+  kind: Role
+  metadata:
+    name: operator
+    namespace: network
+    annotations:
+      config.kubernetes.io/index: '10'
+      internal.config.kubernetes.io/index: '10'
+  rules:
+  - apiGroups: [""]
+    resources: ["secrets", "serviceaccounts", "configmaps"]
+    verbs: ["*"]
+  - apiGroups: ["apps"]
+    resources: ["statefulsets", "deployments"]
+    verbs: ["*"]
+  - apiGroups: ["discovery.k8s.io"]
+    resources: ["endpointslices"]
+    verbs: ["get", "list", "watch"]
+  ---
+  # Source: tailscale-operator/templates/proxy-rbac.yaml
+  apiVersion: rbac.authorization.k8s.io/v1
+  kind: Role
+  metadata:
+    name: proxies
+    namespace: network
+    annotations:
+      config.kubernetes.io/index: '11'
+      internal.config.kubernetes.io/index: '11'
+  rules:
+  - apiGroups: [""]
+    resources: ["secrets"]
+    verbs: ["*"]
+  ---
+  # Source: tailscale-operator/templates/operator-rbac.yaml
+  apiVersion: rbac.authorization.k8s.io/v1
+  kind: RoleBinding
+  metadata:
+    name: operator
+    namespace: network
+    annotations:
+      config.kubernetes.io/index: '12'
+      internal.config.kubernetes.io/index: '12'
+  subjects:
+  - kind: ServiceAccount
+    name: operator
+    namespace: network
+  roleRef:
+    kind: Role
+    name: operator
+    apiGroup: rbac.authorization.k8s.io
+  ---
+  # Source: tailscale-operator/templates/proxy-rbac.yaml
+  apiVersion: rbac.authorization.k8s.io/v1
+  kind: RoleBinding
+  metadata:
+    name: proxies
+    namespace: network
+    annotations:
+      config.kubernetes.io/index: '13'
+      internal.config.kubernetes.io/index: '13'
+  subjects:
+  - kind: ServiceAccount
+    name: proxies
+    namespace: network
+  roleRef:
+    kind: Role
+    name: proxies
+    apiGroup: rbac.authorization.k8s.io
+  ---
+  # Source: tailscale-operator/templates/deployment.yaml
+  # Copyright (c) Tailscale Inc & AUTHORS
+  # SPDX-License-Identifier: BSD-3-Clause
+  
+  apiVersion: apps/v1
+  kind: Deployment
+  metadata:
+    name: operator
+    namespace: network
+    annotations:
+      config.kubernetes.io/index: '14'
+      internal.config.kubernetes.io/index: '14'
+  spec:
+    replicas: 1
+    strategy:
+      type: Recreate
+    selector:
+      matchLabels:
+        app: operator
+    template:
+      metadata:
+        labels:
+          app: operator
+      spec:
+        serviceAccountName: operator
+        volumes:
+        - name: oauth
+          secret:
+            secretName: operator-oauth
+        containers:
+        - name: operator
+          image: tailscale/k8s-operator:v1.68.1
+          imagePullPolicy: Always
+          env:
+          - name: OPERATOR_INITIAL_TAGS
+            value: tag:k8s
+          - name: OPERATOR_HOSTNAME
+            value: tailscale-operator
+          - name: OPERATOR_SECRET
+            value: operator
+          - name: OPERATOR_LOGGING
+            value: info
+          - name: OPERATOR_NAMESPACE
+            valueFrom:
+              fieldRef:
+                fieldPath: metadata.namespace
+          - name: CLIENT_ID_FILE
+            value: /oauth/client_id
+          - name: CLIENT_SECRET_FILE
+            value: /oauth/client_secret
+          - name: PROXY_IMAGE
+            value: tailscale/tailscale:v1.68.1
+          - name: PROXY_TAGS
+            value: tag:k8s
+          - name: APISERVER_PROXY
+            value: "true"
+          - name: PROXY_FIREWALL_MODE
+            value: auto
+          volumeMounts:
+          - name: oauth
+            mountPath: /oauth
+            readOnly: true
+        nodeSelector:
+          kubernetes.io/os: linux
+  ---
+  # Source: tailscale-operator/templates/ingressclass.yaml
+  apiVersion: networking.k8s.io/v1
+  kind: IngressClass
+  metadata:
+    name: tailscale # class name currently can not be changed
+    annotations:
+      config.kubernetes.io/index: '15'
+      internal.config.kubernetes.io/index: '15'
+  spec:
+    controller: tailscale.com/ts-ingress # controller name currently can not be changed
+    # parameters: {} # currently no parameters are supported
+  
   ---
   # Source: podinfo/templates/redis/config.yaml
   apiVersion: v1
diff --git a/tests/tool/__snapshots__/test_get_cluster.ambr b/tests/tool/__snapshots__/test_get_cluster.ambr
index 012b7798..3970ae88 100644
--- a/tests/tool/__snapshots__/test_get_cluster.ambr
+++ b/tests/tool/__snapshots__/test_get_cluster.ambr
@@ -242,6 +242,10 @@
         namespace: flux-system
         url: oci://ghcr.io/stefanprodan/charts
         repo_type: oci
+      - name: tailscale
+        namespace: flux-system
+        url: https://pkgs.tailscale.com/helmcharts
+        repo_type: default
       helm_releases:
       - name: podinfo
         namespace: podinfo
@@ -253,6 +257,15 @@
         images:
         - ghcr.io/stefanprodan/podinfo:6.5.4
         - public.ecr.aws/docker/library/redis:7.0.6
+      - name: tailscale-operator
+        namespace: network
+        chart:
+          name: tailscale-operator
+          repo_name: tailscale
+          repo_namespace: flux-system
+          repo_kind: HelmRepository
+        images:
+        - tailscale/k8s-operator:v1.68.1
       cluster_policies: []
       config_maps:
       - name: podinfo-values
@@ -287,6 +300,10 @@
         namespace: flux-system
         url: oci://ghcr.io/stefanprodan/charts
         repo_type: oci
+      - name: tailscale
+        namespace: flux-system
+        url: https://pkgs.tailscale.com/helmcharts
+        repo_type: default
       helm_releases:
       - name: podinfo
         namespace: podinfo
@@ -298,6 +315,15 @@
         images:
         - ghcr.io/stefanprodan/podinfo:6.5.4
         - public.ecr.aws/docker/library/redis:7.0.6
+      - name: tailscale-operator
+        namespace: network
+        chart:
+          name: tailscale-operator
+          repo_name: tailscale
+          repo_namespace: flux-system
+          repo_kind: HelmRepository
+        images:
+        - tailscale/k8s-operator:v1.68.1
       cluster_policies: []
       config_maps:
       - name: podinfo-values
@@ -331,6 +357,10 @@
         namespace: flux-system
         url: oci://ghcr.io/stefanprodan/charts
         repo_type: oci
+      - name: tailscale
+        namespace: flux-system
+        url: https://pkgs.tailscale.com/helmcharts
+        repo_type: default
       helm_releases:
       - name: podinfo
         namespace: podinfo
@@ -339,6 +369,13 @@
           repo_name: podinfo
           repo_namespace: flux-system
           repo_kind: HelmRepository
+      - name: tailscale-operator
+        namespace: network
+        chart:
+          name: tailscale-operator
+          repo_name: tailscale
+          repo_namespace: flux-system
+          repo_kind: HelmRepository
       cluster_policies: []
       config_maps:
       - name: podinfo-values