encoding/yaml: eliminate use of ghodss yaml pkg

This implementation translates a CUE AST to yaml.Node
for the yaml.v3 package.

The new builtins now also verify the concreteness of
the value.  Closes #288.

Also, the extra roundtrip by ghodss translates CUE to
a map, which then causes the fields to be ordered
lexically, which is undesirable.
The new encoder leaves fields in CUE ordering
which is closer to the original ordering of the
fields. This causes large (desirable) changes in the
Kubernetes' demo validation data.

Change-Id: Iedd32c46b9738d9c367628087c918c4c70310e1d
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5087
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/builtin.go b/cue/builtin.go
index 3f8c7b9..99e8fff 100644
--- a/cue/builtin.go
+++ b/cue/builtin.go
@@ -14,6 +14,7 @@
 
 //go:generate go run gen.go
 //go:generate goimports -w -local cuelang.org/go builtins.go
+//go:generate gofmt -s -w builtins.go
 
 package cue
 
diff --git a/cue/builtins.go b/cue/builtins.go
index 7b2e4e1..d350402 100644
--- a/cue/builtins.go
+++ b/cue/builtins.go
@@ -31,13 +31,13 @@
 	"unicode/utf8"
 
 	"github.com/cockroachdb/apd/v2"
-	goyaml "github.com/ghodss/yaml"
 	"golang.org/x/net/idna"
 
 	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/literal"
 	"cuelang.org/go/cue/parser"
 	"cuelang.org/go/internal"
+	cueyaml "cuelang.org/go/internal/encoding/yaml"
 	"cuelang.org/go/internal/third_party/yaml"
 )
 
@@ -147,21 +147,6 @@
 	return true, nil
 }
 
-var boolValues = map[string]bool{
-	"1":     true,
-	"0":     false,
-	"t":     true,
-	"f":     false,
-	"T":     true,
-	"F":     false,
-	"true":  true,
-	"false": false,
-	"TRUE":  true,
-	"FALSE": false,
-	"True":  true,
-	"False": false,
-}
-
 var builtinPackages = map[string]*builtinPkg{
 	"": {
 		native: []*builtin{{}},
@@ -652,7 +637,14 @@
 				v := c.value(0)
 				if c.do() {
 					c.ret, c.err = func() (interface{}, error) {
-						b, err := goyaml.Marshal(v)
+						if err := v.Validate(Concrete(true)); err != nil {
+							if err := v.Validate(); err != nil {
+								return "", err
+							}
+							return "", internal.ErrIncomplete
+						}
+						n := v.Syntax(Final())
+						b, err := cueyaml.Encode(n)
 						return string(b), err
 					}()
 				}
@@ -675,7 +667,15 @@
 							if i > 0 {
 								buf.WriteString("---\n")
 							}
-							b, err := goyaml.Marshal(iter.Value())
+							v := iter.Value()
+							if err := v.Validate(Concrete(true)); err != nil {
+								if err := v.Validate(); err != nil {
+									return "", err
+								}
+								return "", internal.ErrIncomplete
+							}
+							n := v.Syntax(Final())
+							b, err := cueyaml.Encode(n)
 							if err != nil {
 								return "", err
 							}
diff --git a/cue/resolve_test.go b/cue/resolve_test.go
index 548bd2e..9f1e555 100644
--- a/cue/resolve_test.go
+++ b/cue/resolve_test.go
@@ -2900,6 +2900,50 @@
 			`a: <1>.Marshal (<2>{a: string}), ` +
 			`foo: <3>{a: 3, b: <4>.foo.c}, ` +
 			`b: <1>.Marshal (<4>.foo)}`,
+	}, {
+		desc: "detectIncompleteYAML",
+		in: `
+		package foobar
+
+		import yaml "encoding/yaml"
+	
+		Spec :: {
+			_vars: {something: string}
+			data: {
+				foo :: {
+					use: _vars.something
+				}
+				baz:    yaml.Marshal(_vars.something)
+				foobar: yaml.Marshal(foo)
+			}
+		}
+		Val: Spec & {
+			_vars: something: "var-string"
+		}
+		`,
+		out: `<0>{Spec :: <1>C{_vars: <2>C{something: string}, data: <3>C{foo :: <4>C{use: string}, baz: <5>.Marshal (<6>._vars.something), foobar: <5>.Marshal (<7>.foo)}}, Val: <8>C{_vars: <9>C{something: "var-string"}, data: <10>C{foo :: <11>C{use: "var-string"}, baz: "var-string\n", foobar: "use: var-string\n"}}}`,
+	}, {
+		desc: "detectIncompleteJSON",
+		in: `
+			package foobar
+	
+			import "encoding/json"
+		
+			Spec :: {
+				_vars: {something: string}
+				data: {
+					foo :: {
+						use: _vars.something
+					}
+					baz:    json.Marshal(_vars.something)
+					foobar: json.Marshal(foo)
+				}
+			}
+			Val: Spec & {
+				_vars: something: "var-string"
+			}
+			`,
+		out: `<0>{Spec :: <1>C{_vars: <2>C{something: string}, data: <3>C{foo :: <4>C{use: string}, baz: <5>.Marshal (<6>._vars.something), foobar: <5>.Marshal (<7>.foo)}}, Val: <8>C{_vars: <9>C{something: "var-string"}, data: <10>C{foo :: <11>C{use: "var-string"}, baz: "\"var-string\"", foobar: "{\"use\":\"var-string\"}"}}}`,
 	}}
 	rewriteHelper(t, testCases, evalFull)
 }
diff --git a/doc/tutorial/kubernetes/testdata/manual.out b/doc/tutorial/kubernetes/testdata/manual.out
index 8eaae10..831fb1c 100644
--- a/doc/tutorial/kubernetes/testdata/manual.out
+++ b/doc/tutorial/kubernetes/testdata/manual.out
@@ -5496,18 +5496,18 @@
             data: {
                 "alerts.yaml": """
         receivers:
-        - name: pager
-          slack_configs:
-          - channel: '#cloudmon'
-            send_resolved: true
-            text: |-
-              {{ range .Alerts }}{{ .Annotations.description }}
-              {{ end }}
+          - name: pager
+            slack_configs:
+              - text: |-
+                    {{ range .Alerts }}{{ .Annotations.description }}
+                    {{ end }}
+                channel: '#cloudmon'
+                send_resolved: true
         route:
-          group_by:
-          - alertname
-          - cluster
-          receiver: pager
+            receiver: pager
+            group_by:
+              - alertname
+              - cluster
         
         """
             }
@@ -5607,18 +5607,18 @@
     alertmanager: {
         "alerts.yaml": """
         receivers:
-        - name: pager
-          slack_configs:
-          - channel: '#cloudmon'
-            send_resolved: true
-            text: |-
-              {{ range .Alerts }}{{ .Annotations.description }}
-              {{ end }}
+          - name: pager
+            slack_configs:
+              - text: |-
+                    {{ range .Alerts }}{{ .Annotations.description }}
+                    {{ end }}
+                channel: '#cloudmon'
+                send_resolved: true
         route:
-          group_by:
-          - alertname
-          - cluster
-          receiver: pager
+            receiver: pager
+            group_by:
+              - alertname
+              - cluster
         
         """
     }
@@ -6341,229 +6341,229 @@
             data: {
                 "alert.rules": """
         groups:
-        - name: rules.yaml
-          rules:
-          - alert: InstanceDown
-            annotations:
-              description: '{{$labels.app}} of job {{ $labels.job }} has been down for more
-                than 30 seconds.'
-              summary: Instance {{$labels.app}} down
-            expr: up == 0
-            for: 30s
-            labels:
-              severity: page
-          - alert: InsufficientPeers
-            annotations:
-              description: If one more etcd peer goes down the cluster will be unavailable
-              summary: etcd cluster small
-            expr: count(up{job=\"etcd\"} == 0) > (count(up{job=\"etcd\"}) / 2 - 1)
-            for: 3m
-            labels:
-              severity: page
-          - alert: EtcdNoMaster
-            annotations:
-              summary: No ETCD master elected.
-            expr: sum(etcd_server_has_leader{app=\"etcd\"}) == 0
-            for: 1s
-            labels:
-              severity: page
-          - alert: PodRestart
-            annotations:
-              description: '{{$labels.app}} {{ $labels.container }} resturted {{ $value }}
-                times in 5m.'
-              summary: Pod for {{$labels.container}} restarts too often
-            expr: (max_over_time(pod_container_status_restarts_total[5m]) - min_over_time(pod_container_status_restarts_total[5m]))
-              > 2
-            for: 1m
-            labels:
-              severity: page
+          - name: rules.yaml
+            rules:
+              - labels:
+                    severity: page
+                annotations:
+                    description: '{{$labels.app}} of job {{ $labels.job }} has been down for
+                        more than 30 seconds.'
+                    summary: Instance {{$labels.app}} down
+                alert: InstanceDown
+                expr: up == 0
+                for: 30s
+              - labels:
+                    severity: page
+                annotations:
+                    description: If one more etcd peer goes down the cluster will be unavailable
+                    summary: etcd cluster small
+                alert: InsufficientPeers
+                expr: count(up{job=\"etcd\"} == 0) > (count(up{job=\"etcd\"}) / 2 - 1)
+                for: 3m
+              - labels:
+                    severity: page
+                annotations:
+                    summary: No ETCD master elected.
+                alert: EtcdNoMaster
+                expr: sum(etcd_server_has_leader{app=\"etcd\"}) == 0
+                for: 1s
+              - labels:
+                    severity: page
+                annotations:
+                    description: '{{$labels.app}} {{ $labels.container }} resturted {{ $value
+                        }} times in 5m.'
+                    summary: Pod for {{$labels.container}} restarts too often
+                alert: PodRestart
+                expr: (max_over_time(pod_container_status_restarts_total[5m]) - min_over_time(pod_container_status_restarts_total[5m]))
+                    > 2
+                for: 1m
         
         """
                 "prometheus.yml": """
-        alerting:
-          alertmanagers:
-          - scheme: http
-            static_configs:
-            - targets:
-              - alertmanager:9093
         global:
-          scrape_interval: 15s
+            scrape_interval: 15s
         rule_files:
-        - /etc/prometheus/alert.rules
+          - /etc/prometheus/alert.rules
+        alerting:
+            alertmanagers:
+              - scheme: http
+                static_configs:
+                  - targets:
+                      - alertmanager:9093
         scrape_configs:
-        - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
-          job_name: kubernetes-apiservers
-          kubernetes_sd_configs:
-          - role: endpoints
-          relabel_configs:
-          - action: keep
-            regex: default;kubernetes;https
-            source_labels:
-            - __meta_kubernetes_namespace
-            - __meta_kubernetes_service_name
-            - __meta_kubernetes_endpoint_port_name
-          scheme: https
-          tls_config:
-            ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
-        - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
-          job_name: kubernetes-nodes
-          kubernetes_sd_configs:
-          - role: node
-          relabel_configs:
-          - action: labelmap
-            regex: __meta_kubernetes_node_label_(.+)
-          - replacement: kubernetes.default.svc:443
-            target_label: __address__
-          - regex: (.+)
-            replacement: /api/v1/nodes/${1}/proxy/metrics
-            source_labels:
-            - __meta_kubernetes_node_name
-            target_label: __metrics_path__
-          scheme: https
-          tls_config:
-            ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
-        - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
-          job_name: kubernetes-cadvisor
-          kubernetes_sd_configs:
-          - role: node
-          relabel_configs:
-          - action: labelmap
-            regex: __meta_kubernetes_node_label_(.+)
-          - replacement: kubernetes.default.svc:443
-            target_label: __address__
-          - regex: (.+)
-            replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor
-            source_labels:
-            - __meta_kubernetes_node_name
-            target_label: __metrics_path__
-          scheme: https
-          tls_config:
-            ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
-        - job_name: kubernetes-service-endpoints
-          kubernetes_sd_configs:
-          - role: endpoints
-          relabel_configs:
-          - action: keep
-            regex: true
-            source_labels:
-            - __meta_kubernetes_service_annotation_prometheus_io_scrape
-          - action: replace
-            regex: (https?)
-            source_labels:
-            - __meta_kubernetes_service_annotation_prometheus_io_scheme
-            target_label: __scheme__
-          - action: replace
-            regex: (.+)
-            source_labels:
-            - __meta_kubernetes_service_annotation_prometheus_io_path
-            target_label: __metrics_path__
-          - action: replace
-            regex: ([^:]+)(?::\\d+)?;(\\d+)
-            replacement: $1:$2
-            source_labels:
-            - __address__
-            - __meta_kubernetes_service_annotation_prometheus_io_port
-            target_label: __address__
-          - action: labelmap
-            regex: __meta_kubernetes_service_label_(.+)
-          - action: replace
-            source_labels:
-            - __meta_kubernetes_namespace
-            target_label: kubernetes_namespace
-          - action: replace
-            source_labels:
-            - __meta_kubernetes_service_name
-            target_label: kubernetes_name
-        - job_name: kubernetes-services
-          kubernetes_sd_configs:
-          - role: service
-          metrics_path: /probe
-          params:
-            module:
-            - http_2xx
-          relabel_configs:
-          - action: keep
-            regex: true
-            source_labels:
-            - __meta_kubernetes_service_annotation_prometheus_io_probe
-          - source_labels:
-            - __address__
-            target_label: __param_target
-          - replacement: blackbox-exporter.example.com:9115
-            target_label: __address__
-          - source_labels:
-            - __param_target
-            target_label: app
-          - action: labelmap
-            regex: __meta_kubernetes_service_label_(.+)
-          - source_labels:
-            - __meta_kubernetes_namespace
-            target_label: kubernetes_namespace
-          - source_labels:
-            - __meta_kubernetes_service_name
-            target_label: kubernetes_name
-        - job_name: kubernetes-ingresses
-          kubernetes_sd_configs:
-          - role: ingress
-          metrics_path: /probe
-          params:
-            module:
-            - http_2xx
-          relabel_configs:
-          - action: keep
-            regex: true
-            source_labels:
-            - __meta_kubernetes_ingress_annotation_prometheus_io_probe
-          - regex: (.+);(.+);(.+)
-            replacement: ${1}://${2}${3}
-            source_labels:
-            - __meta_kubernetes_ingress_scheme
-            - __address__
-            - __meta_kubernetes_ingress_path
-            target_label: __param_target
-          - replacement: blackbox-exporter.example.com:9115
-            target_label: __address__
-          - source_labels:
-            - __param_target
-            target_label: app
-          - action: labelmap
-            regex: __meta_kubernetes_ingress_label_(.+)
-          - source_labels:
-            - __meta_kubernetes_namespace
-            target_label: kubernetes_namespace
-          - source_labels:
-            - __meta_kubernetes_ingress_name
-            target_label: kubernetes_name
-        - job_name: kubernetes-pods
-          kubernetes_sd_configs:
-          - role: pod
-          relabel_configs:
-          - action: keep
-            regex: true
-            source_labels:
-            - __meta_kubernetes_pod_annotation_prometheus_io_scrape
-          - action: replace
-            regex: (.+)
-            source_labels:
-            - __meta_kubernetes_pod_annotation_prometheus_io_path
-            target_label: __metrics_path__
-          - action: replace
-            regex: ([^:]+)(?::\\d+)?;(\\d+)
-            replacement: $1:$2
-            source_labels:
-            - __address__
-            - __meta_kubernetes_pod_annotation_prometheus_io_port
-            target_label: __address__
-          - action: labelmap
-            regex: __meta_kubernetes_pod_label_(.+)
-          - action: replace
-            source_labels:
-            - __meta_kubernetes_namespace
-            target_label: kubernetes_namespace
-          - action: replace
-            source_labels:
-            - __meta_kubernetes_pod_name
-            target_label: kubernetes_pod_name
+          - scheme: https
+            job_name: kubernetes-apiservers
+            kubernetes_sd_configs:
+              - role: endpoints
+            tls_config:
+                ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
+            bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
+            relabel_configs:
+              - source_labels:
+                  - __meta_kubernetes_namespace
+                  - __meta_kubernetes_service_name
+                  - __meta_kubernetes_endpoint_port_name
+                action: keep
+                regex: default;kubernetes;https
+          - scheme: https
+            job_name: kubernetes-nodes
+            kubernetes_sd_configs:
+              - role: node
+            tls_config:
+                ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
+            bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
+            relabel_configs:
+              - action: labelmap
+                regex: __meta_kubernetes_node_label_(.+)
+              - target_label: __address__
+                replacement: kubernetes.default.svc:443
+              - source_labels:
+                  - __meta_kubernetes_node_name
+                regex: (.+)
+                target_label: __metrics_path__
+                replacement: /api/v1/nodes/${1}/proxy/metrics
+          - scheme: https
+            job_name: kubernetes-cadvisor
+            kubernetes_sd_configs:
+              - role: node
+            tls_config:
+                ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
+            bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
+            relabel_configs:
+              - action: labelmap
+                regex: __meta_kubernetes_node_label_(.+)
+              - target_label: __address__
+                replacement: kubernetes.default.svc:443
+              - source_labels:
+                  - __meta_kubernetes_node_name
+                regex: (.+)
+                target_label: __metrics_path__
+                replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor
+          - job_name: kubernetes-service-endpoints
+            kubernetes_sd_configs:
+              - role: endpoints
+            relabel_configs:
+              - source_labels:
+                  - __meta_kubernetes_service_annotation_prometheus_io_scrape
+                action: keep
+                regex: true
+              - source_labels:
+                  - __meta_kubernetes_service_annotation_prometheus_io_scheme
+                action: replace
+                regex: (https?)
+                target_label: __scheme__
+              - source_labels:
+                  - __meta_kubernetes_service_annotation_prometheus_io_path
+                action: replace
+                regex: (.+)
+                target_label: __metrics_path__
+              - source_labels:
+                  - __address__
+                  - __meta_kubernetes_service_annotation_prometheus_io_port
+                action: replace
+                regex: ([^:]+)(?::\\d+)?;(\\d+)
+                target_label: __address__
+                replacement: $1:$2
+              - action: labelmap
+                regex: __meta_kubernetes_service_label_(.+)
+              - source_labels:
+                  - __meta_kubernetes_namespace
+                action: replace
+                target_label: kubernetes_namespace
+              - source_labels:
+                  - __meta_kubernetes_service_name
+                action: replace
+                target_label: kubernetes_name
+          - job_name: kubernetes-services
+            kubernetes_sd_configs:
+              - role: service
+            relabel_configs:
+              - source_labels:
+                  - __meta_kubernetes_service_annotation_prometheus_io_probe
+                action: keep
+                regex: true
+              - source_labels:
+                  - __address__
+                target_label: __param_target
+              - target_label: __address__
+                replacement: blackbox-exporter.example.com:9115
+              - source_labels:
+                  - __param_target
+                target_label: app
+              - action: labelmap
+                regex: __meta_kubernetes_service_label_(.+)
+              - source_labels:
+                  - __meta_kubernetes_namespace
+                target_label: kubernetes_namespace
+              - source_labels:
+                  - __meta_kubernetes_service_name
+                target_label: kubernetes_name
+            metrics_path: /probe
+            params:
+                module:
+                  - http_2xx
+          - job_name: kubernetes-ingresses
+            kubernetes_sd_configs:
+              - role: ingress
+            relabel_configs:
+              - source_labels:
+                  - __meta_kubernetes_ingress_annotation_prometheus_io_probe
+                action: keep
+                regex: true
+              - source_labels:
+                  - __meta_kubernetes_ingress_scheme
+                  - __address__
+                  - __meta_kubernetes_ingress_path
+                regex: (.+);(.+);(.+)
+                target_label: __param_target
+                replacement: ${1}://${2}${3}
+              - target_label: __address__
+                replacement: blackbox-exporter.example.com:9115
+              - source_labels:
+                  - __param_target
+                target_label: app
+              - action: labelmap
+                regex: __meta_kubernetes_ingress_label_(.+)
+              - source_labels:
+                  - __meta_kubernetes_namespace
+                target_label: kubernetes_namespace
+              - source_labels:
+                  - __meta_kubernetes_ingress_name
+                target_label: kubernetes_name
+            metrics_path: /probe
+            params:
+                module:
+                  - http_2xx
+          - job_name: kubernetes-pods
+            kubernetes_sd_configs:
+              - role: pod
+            relabel_configs:
+              - source_labels:
+                  - __meta_kubernetes_pod_annotation_prometheus_io_scrape
+                action: keep
+                regex: true
+              - source_labels:
+                  - __meta_kubernetes_pod_annotation_prometheus_io_path
+                action: replace
+                regex: (.+)
+                target_label: __metrics_path__
+              - source_labels:
+                  - __address__
+                  - __meta_kubernetes_pod_annotation_prometheus_io_port
+                action: replace
+                regex: ([^:]+)(?::\\d+)?;(\\d+)
+                target_label: __address__
+                replacement: $1:$2
+              - action: labelmap
+                regex: __meta_kubernetes_pod_label_(.+)
+              - source_labels:
+                  - __meta_kubernetes_namespace
+                action: replace
+                target_label: kubernetes_namespace
+              - source_labels:
+                  - __meta_kubernetes_pod_name
+                action: replace
+                target_label: kubernetes_pod_name
         
         """
             }
@@ -6668,229 +6668,229 @@
     prometheus: {
         "alert.rules": """
         groups:
-        - name: rules.yaml
-          rules:
-          - alert: InstanceDown
-            annotations:
-              description: '{{$labels.app}} of job {{ $labels.job }} has been down for more
-                than 30 seconds.'
-              summary: Instance {{$labels.app}} down
-            expr: up == 0
-            for: 30s
-            labels:
-              severity: page
-          - alert: InsufficientPeers
-            annotations:
-              description: If one more etcd peer goes down the cluster will be unavailable
-              summary: etcd cluster small
-            expr: count(up{job=\"etcd\"} == 0) > (count(up{job=\"etcd\"}) / 2 - 1)
-            for: 3m
-            labels:
-              severity: page
-          - alert: EtcdNoMaster
-            annotations:
-              summary: No ETCD master elected.
-            expr: sum(etcd_server_has_leader{app=\"etcd\"}) == 0
-            for: 1s
-            labels:
-              severity: page
-          - alert: PodRestart
-            annotations:
-              description: '{{$labels.app}} {{ $labels.container }} resturted {{ $value }}
-                times in 5m.'
-              summary: Pod for {{$labels.container}} restarts too often
-            expr: (max_over_time(pod_container_status_restarts_total[5m]) - min_over_time(pod_container_status_restarts_total[5m]))
-              > 2
-            for: 1m
-            labels:
-              severity: page
+          - name: rules.yaml
+            rules:
+              - labels:
+                    severity: page
+                annotations:
+                    description: '{{$labels.app}} of job {{ $labels.job }} has been down for
+                        more than 30 seconds.'
+                    summary: Instance {{$labels.app}} down
+                alert: InstanceDown
+                expr: up == 0
+                for: 30s
+              - labels:
+                    severity: page
+                annotations:
+                    description: If one more etcd peer goes down the cluster will be unavailable
+                    summary: etcd cluster small
+                alert: InsufficientPeers
+                expr: count(up{job=\"etcd\"} == 0) > (count(up{job=\"etcd\"}) / 2 - 1)
+                for: 3m
+              - labels:
+                    severity: page
+                annotations:
+                    summary: No ETCD master elected.
+                alert: EtcdNoMaster
+                expr: sum(etcd_server_has_leader{app=\"etcd\"}) == 0
+                for: 1s
+              - labels:
+                    severity: page
+                annotations:
+                    description: '{{$labels.app}} {{ $labels.container }} resturted {{ $value
+                        }} times in 5m.'
+                    summary: Pod for {{$labels.container}} restarts too often
+                alert: PodRestart
+                expr: (max_over_time(pod_container_status_restarts_total[5m]) - min_over_time(pod_container_status_restarts_total[5m]))
+                    > 2
+                for: 1m
         
         """
         "prometheus.yml": """
-        alerting:
-          alertmanagers:
-          - scheme: http
-            static_configs:
-            - targets:
-              - alertmanager:9093
         global:
-          scrape_interval: 15s
+            scrape_interval: 15s
         rule_files:
-        - /etc/prometheus/alert.rules
+          - /etc/prometheus/alert.rules
+        alerting:
+            alertmanagers:
+              - scheme: http
+                static_configs:
+                  - targets:
+                      - alertmanager:9093
         scrape_configs:
-        - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
-          job_name: kubernetes-apiservers
-          kubernetes_sd_configs:
-          - role: endpoints
-          relabel_configs:
-          - action: keep
-            regex: default;kubernetes;https
-            source_labels:
-            - __meta_kubernetes_namespace
-            - __meta_kubernetes_service_name
-            - __meta_kubernetes_endpoint_port_name
-          scheme: https
-          tls_config:
-            ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
-        - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
-          job_name: kubernetes-nodes
-          kubernetes_sd_configs:
-          - role: node
-          relabel_configs:
-          - action: labelmap
-            regex: __meta_kubernetes_node_label_(.+)
-          - replacement: kubernetes.default.svc:443
-            target_label: __address__
-          - regex: (.+)
-            replacement: /api/v1/nodes/${1}/proxy/metrics
-            source_labels:
-            - __meta_kubernetes_node_name
-            target_label: __metrics_path__
-          scheme: https
-          tls_config:
-            ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
-        - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
-          job_name: kubernetes-cadvisor
-          kubernetes_sd_configs:
-          - role: node
-          relabel_configs:
-          - action: labelmap
-            regex: __meta_kubernetes_node_label_(.+)
-          - replacement: kubernetes.default.svc:443
-            target_label: __address__
-          - regex: (.+)
-            replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor
-            source_labels:
-            - __meta_kubernetes_node_name
-            target_label: __metrics_path__
-          scheme: https
-          tls_config:
-            ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
-        - job_name: kubernetes-service-endpoints
-          kubernetes_sd_configs:
-          - role: endpoints
-          relabel_configs:
-          - action: keep
-            regex: true
-            source_labels:
-            - __meta_kubernetes_service_annotation_prometheus_io_scrape
-          - action: replace
-            regex: (https?)
-            source_labels:
-            - __meta_kubernetes_service_annotation_prometheus_io_scheme
-            target_label: __scheme__
-          - action: replace
-            regex: (.+)
-            source_labels:
-            - __meta_kubernetes_service_annotation_prometheus_io_path
-            target_label: __metrics_path__
-          - action: replace
-            regex: ([^:]+)(?::\\d+)?;(\\d+)
-            replacement: $1:$2
-            source_labels:
-            - __address__
-            - __meta_kubernetes_service_annotation_prometheus_io_port
-            target_label: __address__
-          - action: labelmap
-            regex: __meta_kubernetes_service_label_(.+)
-          - action: replace
-            source_labels:
-            - __meta_kubernetes_namespace
-            target_label: kubernetes_namespace
-          - action: replace
-            source_labels:
-            - __meta_kubernetes_service_name
-            target_label: kubernetes_name
-        - job_name: kubernetes-services
-          kubernetes_sd_configs:
-          - role: service
-          metrics_path: /probe
-          params:
-            module:
-            - http_2xx
-          relabel_configs:
-          - action: keep
-            regex: true
-            source_labels:
-            - __meta_kubernetes_service_annotation_prometheus_io_probe
-          - source_labels:
-            - __address__
-            target_label: __param_target
-          - replacement: blackbox-exporter.example.com:9115
-            target_label: __address__
-          - source_labels:
-            - __param_target
-            target_label: app
-          - action: labelmap
-            regex: __meta_kubernetes_service_label_(.+)
-          - source_labels:
-            - __meta_kubernetes_namespace
-            target_label: kubernetes_namespace
-          - source_labels:
-            - __meta_kubernetes_service_name
-            target_label: kubernetes_name
-        - job_name: kubernetes-ingresses
-          kubernetes_sd_configs:
-          - role: ingress
-          metrics_path: /probe
-          params:
-            module:
-            - http_2xx
-          relabel_configs:
-          - action: keep
-            regex: true
-            source_labels:
-            - __meta_kubernetes_ingress_annotation_prometheus_io_probe
-          - regex: (.+);(.+);(.+)
-            replacement: ${1}://${2}${3}
-            source_labels:
-            - __meta_kubernetes_ingress_scheme
-            - __address__
-            - __meta_kubernetes_ingress_path
-            target_label: __param_target
-          - replacement: blackbox-exporter.example.com:9115
-            target_label: __address__
-          - source_labels:
-            - __param_target
-            target_label: app
-          - action: labelmap
-            regex: __meta_kubernetes_ingress_label_(.+)
-          - source_labels:
-            - __meta_kubernetes_namespace
-            target_label: kubernetes_namespace
-          - source_labels:
-            - __meta_kubernetes_ingress_name
-            target_label: kubernetes_name
-        - job_name: kubernetes-pods
-          kubernetes_sd_configs:
-          - role: pod
-          relabel_configs:
-          - action: keep
-            regex: true
-            source_labels:
-            - __meta_kubernetes_pod_annotation_prometheus_io_scrape
-          - action: replace
-            regex: (.+)
-            source_labels:
-            - __meta_kubernetes_pod_annotation_prometheus_io_path
-            target_label: __metrics_path__
-          - action: replace
-            regex: ([^:]+)(?::\\d+)?;(\\d+)
-            replacement: $1:$2
-            source_labels:
-            - __address__
-            - __meta_kubernetes_pod_annotation_prometheus_io_port
-            target_label: __address__
-          - action: labelmap
-            regex: __meta_kubernetes_pod_label_(.+)
-          - action: replace
-            source_labels:
-            - __meta_kubernetes_namespace
-            target_label: kubernetes_namespace
-          - action: replace
-            source_labels:
-            - __meta_kubernetes_pod_name
-            target_label: kubernetes_pod_name
+          - scheme: https
+            job_name: kubernetes-apiservers
+            kubernetes_sd_configs:
+              - role: endpoints
+            tls_config:
+                ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
+            bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
+            relabel_configs:
+              - source_labels:
+                  - __meta_kubernetes_namespace
+                  - __meta_kubernetes_service_name
+                  - __meta_kubernetes_endpoint_port_name
+                action: keep
+                regex: default;kubernetes;https
+          - scheme: https
+            job_name: kubernetes-nodes
+            kubernetes_sd_configs:
+              - role: node
+            tls_config:
+                ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
+            bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
+            relabel_configs:
+              - action: labelmap
+                regex: __meta_kubernetes_node_label_(.+)
+              - target_label: __address__
+                replacement: kubernetes.default.svc:443
+              - source_labels:
+                  - __meta_kubernetes_node_name
+                regex: (.+)
+                target_label: __metrics_path__
+                replacement: /api/v1/nodes/${1}/proxy/metrics
+          - scheme: https
+            job_name: kubernetes-cadvisor
+            kubernetes_sd_configs:
+              - role: node
+            tls_config:
+                ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
+            bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
+            relabel_configs:
+              - action: labelmap
+                regex: __meta_kubernetes_node_label_(.+)
+              - target_label: __address__
+                replacement: kubernetes.default.svc:443
+              - source_labels:
+                  - __meta_kubernetes_node_name
+                regex: (.+)
+                target_label: __metrics_path__
+                replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor
+          - job_name: kubernetes-service-endpoints
+            kubernetes_sd_configs:
+              - role: endpoints
+            relabel_configs:
+              - source_labels:
+                  - __meta_kubernetes_service_annotation_prometheus_io_scrape
+                action: keep
+                regex: true
+              - source_labels:
+                  - __meta_kubernetes_service_annotation_prometheus_io_scheme
+                action: replace
+                regex: (https?)
+                target_label: __scheme__
+              - source_labels:
+                  - __meta_kubernetes_service_annotation_prometheus_io_path
+                action: replace
+                regex: (.+)
+                target_label: __metrics_path__
+              - source_labels:
+                  - __address__
+                  - __meta_kubernetes_service_annotation_prometheus_io_port
+                action: replace
+                regex: ([^:]+)(?::\\d+)?;(\\d+)
+                target_label: __address__
+                replacement: $1:$2
+              - action: labelmap
+                regex: __meta_kubernetes_service_label_(.+)
+              - source_labels:
+                  - __meta_kubernetes_namespace
+                action: replace
+                target_label: kubernetes_namespace
+              - source_labels:
+                  - __meta_kubernetes_service_name
+                action: replace
+                target_label: kubernetes_name
+          - job_name: kubernetes-services
+            kubernetes_sd_configs:
+              - role: service
+            relabel_configs:
+              - source_labels:
+                  - __meta_kubernetes_service_annotation_prometheus_io_probe
+                action: keep
+                regex: true
+              - source_labels:
+                  - __address__
+                target_label: __param_target
+              - target_label: __address__
+                replacement: blackbox-exporter.example.com:9115
+              - source_labels:
+                  - __param_target
+                target_label: app
+              - action: labelmap
+                regex: __meta_kubernetes_service_label_(.+)
+              - source_labels:
+                  - __meta_kubernetes_namespace
+                target_label: kubernetes_namespace
+              - source_labels:
+                  - __meta_kubernetes_service_name
+                target_label: kubernetes_name
+            metrics_path: /probe
+            params:
+                module:
+                  - http_2xx
+          - job_name: kubernetes-ingresses
+            kubernetes_sd_configs:
+              - role: ingress
+            relabel_configs:
+              - source_labels:
+                  - __meta_kubernetes_ingress_annotation_prometheus_io_probe
+                action: keep
+                regex: true
+              - source_labels:
+                  - __meta_kubernetes_ingress_scheme
+                  - __address__
+                  - __meta_kubernetes_ingress_path
+                regex: (.+);(.+);(.+)
+                target_label: __param_target
+                replacement: ${1}://${2}${3}
+              - target_label: __address__
+                replacement: blackbox-exporter.example.com:9115
+              - source_labels:
+                  - __param_target
+                target_label: app
+              - action: labelmap
+                regex: __meta_kubernetes_ingress_label_(.+)
+              - source_labels:
+                  - __meta_kubernetes_namespace
+                target_label: kubernetes_namespace
+              - source_labels:
+                  - __meta_kubernetes_ingress_name
+                target_label: kubernetes_name
+            metrics_path: /probe
+            params:
+                module:
+                  - http_2xx
+          - job_name: kubernetes-pods
+            kubernetes_sd_configs:
+              - role: pod
+            relabel_configs:
+              - source_labels:
+                  - __meta_kubernetes_pod_annotation_prometheus_io_scrape
+                action: keep
+                regex: true
+              - source_labels:
+                  - __meta_kubernetes_pod_annotation_prometheus_io_path
+                action: replace
+                regex: (.+)
+                target_label: __metrics_path__
+              - source_labels:
+                  - __address__
+                  - __meta_kubernetes_pod_annotation_prometheus_io_port
+                action: replace
+                regex: ([^:]+)(?::\\d+)?;(\\d+)
+                target_label: __address__
+                replacement: $1:$2
+              - action: labelmap
+                regex: __meta_kubernetes_pod_label_(.+)
+              - source_labels:
+                  - __meta_kubernetes_namespace
+                action: replace
+                target_label: kubernetes_namespace
+              - source_labels:
+                  - __meta_kubernetes_pod_name
+                action: replace
+                target_label: kubernetes_pod_name
         
         """
     }
diff --git a/doc/tutorial/kubernetes/testdata/quick.out b/doc/tutorial/kubernetes/testdata/quick.out
index 6d4fa10..1da9042 100644
--- a/doc/tutorial/kubernetes/testdata/quick.out
+++ b/doc/tutorial/kubernetes/testdata/quick.out
@@ -2656,18 +2656,18 @@
         data: {
             "alerts.yaml": """
         receivers:
-        - name: pager
-          slack_configs:
-          - channel: '#cloudmon'
-            send_resolved: true
-            text: |-
-              {{ range .Alerts }}{{ .Annotations.description }}
-              {{ end }}
+          - name: pager
+            slack_configs:
+              - text: |-
+                    {{ range .Alerts }}{{ .Annotations.description }}
+                    {{ end }}
+                channel: '#cloudmon'
+                send_resolved: true
         route:
-          group_by:
-          - alertname
-          - cluster
-          receiver: pager
+            receiver: pager
+            group_by:
+              - alertname
+              - cluster
         
         """
         }
@@ -3088,229 +3088,229 @@
         data: {
             "alert.rules": """
         groups:
-        - name: rules.yaml
-          rules:
-          - alert: InstanceDown
-            annotations:
-              description: '{{$labels.app}} of job {{ $labels.job }} has been down for more
-                than 30 seconds.'
-              summary: Instance {{$labels.app}} down
-            expr: up == 0
-            for: 30s
-            labels:
-              severity: page
-          - alert: InsufficientPeers
-            annotations:
-              description: If one more etcd peer goes down the cluster will be unavailable
-              summary: etcd cluster small
-            expr: count(up{job=\"etcd\"} == 0) > (count(up{job=\"etcd\"}) / 2 - 1)
-            for: 3m
-            labels:
-              severity: page
-          - alert: EtcdNoMaster
-            annotations:
-              summary: No ETCD master elected.
-            expr: sum(etcd_server_has_leader{app=\"etcd\"}) == 0
-            for: 1s
-            labels:
-              severity: page
-          - alert: PodRestart
-            annotations:
-              description: '{{$labels.app}} {{ $labels.container }} resturted {{ $value }}
-                times in 5m.'
-              summary: Pod for {{$labels.container}} restarts too often
-            expr: (max_over_time(pod_container_status_restarts_total[5m]) - min_over_time(pod_container_status_restarts_total[5m]))
-              > 2
-            for: 1m
-            labels:
-              severity: page
+          - name: rules.yaml
+            rules:
+              - labels:
+                    severity: page
+                annotations:
+                    description: '{{$labels.app}} of job {{ $labels.job }} has been down for
+                        more than 30 seconds.'
+                    summary: Instance {{$labels.app}} down
+                alert: InstanceDown
+                expr: up == 0
+                for: 30s
+              - labels:
+                    severity: page
+                annotations:
+                    description: If one more etcd peer goes down the cluster will be unavailable
+                    summary: etcd cluster small
+                alert: InsufficientPeers
+                expr: count(up{job=\"etcd\"} == 0) > (count(up{job=\"etcd\"}) / 2 - 1)
+                for: 3m
+              - labels:
+                    severity: page
+                annotations:
+                    summary: No ETCD master elected.
+                alert: EtcdNoMaster
+                expr: sum(etcd_server_has_leader{app=\"etcd\"}) == 0
+                for: 1s
+              - labels:
+                    severity: page
+                annotations:
+                    description: '{{$labels.app}} {{ $labels.container }} resturted {{ $value
+                        }} times in 5m.'
+                    summary: Pod for {{$labels.container}} restarts too often
+                alert: PodRestart
+                expr: (max_over_time(pod_container_status_restarts_total[5m]) - min_over_time(pod_container_status_restarts_total[5m]))
+                    > 2
+                for: 1m
         
         """
             "prometheus.yml": """
-        alerting:
-          alertmanagers:
-          - scheme: http
-            static_configs:
-            - targets:
-              - alertmanager:9093
         global:
-          scrape_interval: 15s
+            scrape_interval: 15s
         rule_files:
-        - /etc/prometheus/alert.rules
+          - /etc/prometheus/alert.rules
+        alerting:
+            alertmanagers:
+              - scheme: http
+                static_configs:
+                  - targets:
+                      - alertmanager:9093
         scrape_configs:
-        - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
-          job_name: kubernetes-apiservers
-          kubernetes_sd_configs:
-          - role: endpoints
-          relabel_configs:
-          - action: keep
-            regex: default;kubernetes;https
-            source_labels:
-            - __meta_kubernetes_namespace
-            - __meta_kubernetes_service_name
-            - __meta_kubernetes_endpoint_port_name
-          scheme: https
-          tls_config:
-            ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
-        - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
-          job_name: kubernetes-nodes
-          kubernetes_sd_configs:
-          - role: node
-          relabel_configs:
-          - action: labelmap
-            regex: __meta_kubernetes_node_label_(.+)
-          - replacement: kubernetes.default.svc:443
-            target_label: __address__
-          - regex: (.+)
-            replacement: /api/v1/nodes/${1}/proxy/metrics
-            source_labels:
-            - __meta_kubernetes_node_name
-            target_label: __metrics_path__
-          scheme: https
-          tls_config:
-            ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
-        - bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
-          job_name: kubernetes-cadvisor
-          kubernetes_sd_configs:
-          - role: node
-          relabel_configs:
-          - action: labelmap
-            regex: __meta_kubernetes_node_label_(.+)
-          - replacement: kubernetes.default.svc:443
-            target_label: __address__
-          - regex: (.+)
-            replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor
-            source_labels:
-            - __meta_kubernetes_node_name
-            target_label: __metrics_path__
-          scheme: https
-          tls_config:
-            ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
-        - job_name: kubernetes-service-endpoints
-          kubernetes_sd_configs:
-          - role: endpoints
-          relabel_configs:
-          - action: keep
-            regex: true
-            source_labels:
-            - __meta_kubernetes_service_annotation_prometheus_io_scrape
-          - action: replace
-            regex: (https?)
-            source_labels:
-            - __meta_kubernetes_service_annotation_prometheus_io_scheme
-            target_label: __scheme__
-          - action: replace
-            regex: (.+)
-            source_labels:
-            - __meta_kubernetes_service_annotation_prometheus_io_path
-            target_label: __metrics_path__
-          - action: replace
-            regex: ([^:]+)(?::\\d+)?;(\\d+)
-            replacement: $1:$2
-            source_labels:
-            - __address__
-            - __meta_kubernetes_service_annotation_prometheus_io_port
-            target_label: __address__
-          - action: labelmap
-            regex: __meta_kubernetes_service_label_(.+)
-          - action: replace
-            source_labels:
-            - __meta_kubernetes_namespace
-            target_label: kubernetes_namespace
-          - action: replace
-            source_labels:
-            - __meta_kubernetes_service_name
-            target_label: kubernetes_name
-        - job_name: kubernetes-services
-          kubernetes_sd_configs:
-          - role: service
-          metrics_path: /probe
-          params:
-            module:
-            - http_2xx
-          relabel_configs:
-          - action: keep
-            regex: true
-            source_labels:
-            - __meta_kubernetes_service_annotation_prometheus_io_probe
-          - source_labels:
-            - __address__
-            target_label: __param_target
-          - replacement: blackbox-exporter.example.com:9115
-            target_label: __address__
-          - source_labels:
-            - __param_target
-            target_label: app
-          - action: labelmap
-            regex: __meta_kubernetes_service_label_(.+)
-          - source_labels:
-            - __meta_kubernetes_namespace
-            target_label: kubernetes_namespace
-          - source_labels:
-            - __meta_kubernetes_service_name
-            target_label: kubernetes_name
-        - job_name: kubernetes-ingresses
-          kubernetes_sd_configs:
-          - role: ingress
-          metrics_path: /probe
-          params:
-            module:
-            - http_2xx
-          relabel_configs:
-          - action: keep
-            regex: true
-            source_labels:
-            - __meta_kubernetes_ingress_annotation_prometheus_io_probe
-          - regex: (.+);(.+);(.+)
-            replacement: ${1}://${2}${3}
-            source_labels:
-            - __meta_kubernetes_ingress_scheme
-            - __address__
-            - __meta_kubernetes_ingress_path
-            target_label: __param_target
-          - replacement: blackbox-exporter.example.com:9115
-            target_label: __address__
-          - source_labels:
-            - __param_target
-            target_label: app
-          - action: labelmap
-            regex: __meta_kubernetes_ingress_label_(.+)
-          - source_labels:
-            - __meta_kubernetes_namespace
-            target_label: kubernetes_namespace
-          - source_labels:
-            - __meta_kubernetes_ingress_name
-            target_label: kubernetes_name
-        - job_name: kubernetes-pods
-          kubernetes_sd_configs:
-          - role: pod
-          relabel_configs:
-          - action: keep
-            regex: true
-            source_labels:
-            - __meta_kubernetes_pod_annotation_prometheus_io_scrape
-          - action: replace
-            regex: (.+)
-            source_labels:
-            - __meta_kubernetes_pod_annotation_prometheus_io_path
-            target_label: __metrics_path__
-          - action: replace
-            regex: ([^:]+)(?::\\d+)?;(\\d+)
-            replacement: $1:$2
-            source_labels:
-            - __address__
-            - __meta_kubernetes_pod_annotation_prometheus_io_port
-            target_label: __address__
-          - action: labelmap
-            regex: __meta_kubernetes_pod_label_(.+)
-          - action: replace
-            source_labels:
-            - __meta_kubernetes_namespace
-            target_label: kubernetes_namespace
-          - action: replace
-            source_labels:
-            - __meta_kubernetes_pod_name
-            target_label: kubernetes_pod_name
+          - scheme: https
+            job_name: kubernetes-apiservers
+            kubernetes_sd_configs:
+              - role: endpoints
+            tls_config:
+                ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
+            bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
+            relabel_configs:
+              - action: keep
+                source_labels:
+                  - __meta_kubernetes_namespace
+                  - __meta_kubernetes_service_name
+                  - __meta_kubernetes_endpoint_port_name
+                regex: default;kubernetes;https
+          - scheme: https
+            job_name: kubernetes-nodes
+            kubernetes_sd_configs:
+              - role: node
+            tls_config:
+                ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
+            bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
+            relabel_configs:
+              - action: labelmap
+                regex: __meta_kubernetes_node_label_(.+)
+              - target_label: __address__
+                replacement: kubernetes.default.svc:443
+              - source_labels:
+                  - __meta_kubernetes_node_name
+                regex: (.+)
+                target_label: __metrics_path__
+                replacement: /api/v1/nodes/${1}/proxy/metrics
+          - scheme: https
+            job_name: kubernetes-cadvisor
+            kubernetes_sd_configs:
+              - role: node
+            tls_config:
+                ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
+            bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
+            relabel_configs:
+              - action: labelmap
+                regex: __meta_kubernetes_node_label_(.+)
+              - target_label: __address__
+                replacement: kubernetes.default.svc:443
+              - source_labels:
+                  - __meta_kubernetes_node_name
+                regex: (.+)
+                target_label: __metrics_path__
+                replacement: /api/v1/nodes/${1}/proxy/metrics/cadvisor
+          - job_name: kubernetes-service-endpoints
+            kubernetes_sd_configs:
+              - role: endpoints
+            relabel_configs:
+              - action: keep
+                source_labels:
+                  - __meta_kubernetes_service_annotation_prometheus_io_scrape
+                regex: true
+              - action: replace
+                source_labels:
+                  - __meta_kubernetes_service_annotation_prometheus_io_scheme
+                regex: (https?)
+                target_label: __scheme__
+              - action: replace
+                source_labels:
+                  - __meta_kubernetes_service_annotation_prometheus_io_path
+                regex: (.+)
+                target_label: __metrics_path__
+              - action: replace
+                source_labels:
+                  - __address__
+                  - __meta_kubernetes_service_annotation_prometheus_io_port
+                regex: ([^:]+)(?::\\d+)?;(\\d+)
+                target_label: __address__
+                replacement: $1:$2
+              - action: labelmap
+                regex: __meta_kubernetes_service_label_(.+)
+              - action: replace
+                source_labels:
+                  - __meta_kubernetes_namespace
+                target_label: kubernetes_namespace
+              - action: replace
+                source_labels:
+                  - __meta_kubernetes_service_name
+                target_label: kubernetes_name
+          - job_name: kubernetes-services
+            kubernetes_sd_configs:
+              - role: service
+            relabel_configs:
+              - action: keep
+                source_labels:
+                  - __meta_kubernetes_service_annotation_prometheus_io_probe
+                regex: true
+              - source_labels:
+                  - __address__
+                target_label: __param_target
+              - target_label: __address__
+                replacement: blackbox-exporter.example.com:9115
+              - source_labels:
+                  - __param_target
+                target_label: app
+              - action: labelmap
+                regex: __meta_kubernetes_service_label_(.+)
+              - source_labels:
+                  - __meta_kubernetes_namespace
+                target_label: kubernetes_namespace
+              - source_labels:
+                  - __meta_kubernetes_service_name
+                target_label: kubernetes_name
+            metrics_path: /probe
+            params:
+                module:
+                  - http_2xx
+          - job_name: kubernetes-ingresses
+            kubernetes_sd_configs:
+              - role: ingress
+            relabel_configs:
+              - action: keep
+                source_labels:
+                  - __meta_kubernetes_ingress_annotation_prometheus_io_probe
+                regex: true
+              - source_labels:
+                  - __meta_kubernetes_ingress_scheme
+                  - __address__
+                  - __meta_kubernetes_ingress_path
+                regex: (.+);(.+);(.+)
+                target_label: __param_target
+                replacement: ${1}://${2}${3}
+              - target_label: __address__
+                replacement: blackbox-exporter.example.com:9115
+              - source_labels:
+                  - __param_target
+                target_label: app
+              - action: labelmap
+                regex: __meta_kubernetes_ingress_label_(.+)
+              - source_labels:
+                  - __meta_kubernetes_namespace
+                target_label: kubernetes_namespace
+              - source_labels:
+                  - __meta_kubernetes_ingress_name
+                target_label: kubernetes_name
+            metrics_path: /probe
+            params:
+                module:
+                  - http_2xx
+          - job_name: kubernetes-pods
+            kubernetes_sd_configs:
+              - role: pod
+            relabel_configs:
+              - action: keep
+                source_labels:
+                  - __meta_kubernetes_pod_annotation_prometheus_io_scrape
+                regex: true
+              - action: replace
+                source_labels:
+                  - __meta_kubernetes_pod_annotation_prometheus_io_path
+                regex: (.+)
+                target_label: __metrics_path__
+              - action: replace
+                source_labels:
+                  - __address__
+                  - __meta_kubernetes_pod_annotation_prometheus_io_port
+                regex: ([^:]+)(?::\\d+)?;(\\d+)
+                target_label: __address__
+                replacement: $1:$2
+              - action: labelmap
+                regex: __meta_kubernetes_pod_label_(.+)
+              - action: replace
+                source_labels:
+                  - __meta_kubernetes_namespace
+                target_label: kubernetes_namespace
+              - action: replace
+                source_labels:
+                  - __meta_kubernetes_pod_name
+                target_label: kubernetes_pod_name
         
         """
         }
diff --git a/encoding/yaml/yaml.go b/encoding/yaml/yaml.go
index f972bd2..80c19b9 100644
--- a/encoding/yaml/yaml.go
+++ b/encoding/yaml/yaml.go
@@ -20,19 +20,13 @@
 	"bytes"
 	"io"
 
-	goyaml "github.com/ghodss/yaml"
-
 	"cuelang.org/go/cue"
 	"cuelang.org/go/cue/ast"
+	cueyaml "cuelang.org/go/internal/encoding/yaml"
 	"cuelang.org/go/internal/third_party/yaml"
 	pkgyaml "cuelang.org/go/pkg/encoding/yaml"
 )
 
-// TODO: replace the ghodss YAML encoder. It has a few major issues:
-//   - it does not expose the underlying error, which means we lose valuable
-//     information.
-//   - comments and other meta data are lost.
-
 // Extract parses the YAML to a CUE expression. Streams are returned as a list
 // of the streamed values.
 func Extract(filename string, src interface{}) (*ast.File, error) {
@@ -79,7 +73,8 @@
 
 // Encode returns the YAML encoding of v.
 func Encode(v cue.Value) ([]byte, error) {
-	b, err := goyaml.Marshal(v)
+	n := v.Syntax(cue.Final())
+	b, err := cueyaml.Encode(n)
 	return b, err
 }
 
@@ -92,7 +87,8 @@
 		if i > 0 {
 			buf.WriteString("---\n")
 		}
-		b, err := goyaml.Marshal(iter.Value())
+		n := iter.Value().Syntax(cue.Final())
+		b, err := cueyaml.Encode(n)
 		if err != nil {
 			return nil, err
 		}
diff --git a/go.mod b/go.mod
index e96c5af..d2f461b 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,6 @@
 	github.com/cockroachdb/apd/v2 v2.0.1
 	github.com/davecgh/go-spew v1.1.0 // indirect
 	github.com/emicklei/proto v1.6.15
-	github.com/ghodss/yaml v1.0.0
 	github.com/google/go-cmp v0.4.0
 	github.com/inconshreveable/mousetrap v1.0.0 // indirect
 	github.com/kr/pretty v0.1.0
@@ -23,7 +22,7 @@
 	golang.org/x/text v0.3.2
 	golang.org/x/tools v0.0.0-20200113154838-30cae5f2fb06
 	golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543
-	gopkg.in/yaml.v2 v2.2.2 // indirect
+	gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71
 )
 
 go 1.12
diff --git a/go.sum b/go.sum
index aa4e3c4..5537cf7 100644
--- a/go.sum
+++ b/go.sum
@@ -6,8 +6,6 @@
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/emicklei/proto v1.6.15 h1:XbpwxmuOPrdES97FrSfpyy67SSCV/wBIKXqgJzh6hNw=
 github.com/emicklei/proto v1.6.15/go.mod h1:rn1FgRS/FANiZdD2djyH7TMA9jdRDcYQ9IEN9yvjX0A=
-github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
@@ -61,5 +59,5 @@
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71 h1:Xe2gvTZUJpsvOWUnvmL/tmhVBZUmHSvLbMjRj6NUUKo=
+gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/encoding/yaml/encode.go b/internal/encoding/yaml/encode.go
new file mode 100644
index 0000000..b0a247d
--- /dev/null
+++ b/internal/encoding/yaml/encode.go
@@ -0,0 +1,293 @@
+// Copyright 2020 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package yaml
+
+import (
+	"math/big"
+	"strings"
+
+	"gopkg.in/yaml.v3"
+
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/errors"
+	"cuelang.org/go/cue/literal"
+	"cuelang.org/go/cue/token"
+	"cuelang.org/go/internal"
+)
+
+// Encode converts a CUE AST to YAML.
+//
+// The given file must only contain values that can be directly supported by
+// YAML:
+//    Type          Restrictions
+//    BasicLit
+//    File          no imports, aliases, or definitions
+//    StructLit     no embeddings, aliases, or definitions
+//    List
+//    Field         must be regular; label must be a BasicLit or Ident
+//    CommentGroup
+//
+//    TODO: support anchors through Ident.
+func Encode(n ast.Node) (b []byte, err error) {
+	y, err := encode(n)
+	if err != nil {
+		return nil, err
+	}
+	return yaml.Marshal(y)
+}
+
+func encode(n ast.Node) (y *yaml.Node, err error) {
+	switch x := n.(type) {
+	case *ast.BasicLit:
+		y, err = encodeScalar(x)
+
+	case *ast.ListLit:
+		y, err = encodeExprs(x.Elts)
+		line := x.Lbrack.Line()
+		if err == nil && line > 0 && line == x.Rbrack.Line() {
+			y.Style = yaml.FlowStyle
+		}
+
+	case *ast.StructLit:
+		y, err = encodeDecls(x.Elts)
+		line := x.Lbrace.Line()
+		if err == nil && line > 0 && line == x.Rbrace.Line() {
+			y.Style = yaml.FlowStyle
+		}
+
+	case *ast.File:
+		y, err = encodeDecls(x.Decls)
+
+	case *ast.UnaryExpr:
+		b, ok := x.X.(*ast.BasicLit)
+		if ok && x.Op == token.SUB && (b.Kind == token.INT || b.Kind == token.FLOAT) {
+			y, err = encodeScalar(b)
+			if !strings.HasPrefix(y.Value, "-") {
+				y.Value = "-" + y.Value
+				break
+			}
+		}
+		return nil, errors.Newf(x.Pos(), "yaml: unsupported node %s (%T)", internal.DebugStr(x), x)
+	default:
+		return nil, errors.Newf(x.Pos(), "yaml: unsupported node %s (%T)", internal.DebugStr(x), x)
+	}
+	if err != nil {
+		return nil, err
+	}
+	addDocs(n, y, y)
+	return y, nil
+}
+
+func encodeScalar(b *ast.BasicLit) (n *yaml.Node, err error) {
+	n = &yaml.Node{Kind: yaml.ScalarNode}
+
+	switch b.Kind {
+	case token.INT:
+		var x big.Int
+		if err := setNum(n, b.Value, &x); err != nil {
+			return nil, err
+		}
+
+	case token.FLOAT:
+		var x big.Float
+		if err := setNum(n, b.Value, &x); err != nil {
+			return nil, err
+		}
+
+	case token.TRUE, token.FALSE, token.NULL:
+		n.Value = b.Value
+
+	case token.STRING:
+		str, err := literal.Unquote(b.Value)
+		if err != nil {
+			return nil, err
+		}
+		n.SetString(str)
+
+	default:
+		return nil, errors.Newf(b.Pos(), "unknown literal type %v", b.Kind)
+	}
+	return n, nil
+}
+
+func setNum(n *yaml.Node, s string, x interface{}) error {
+	if yaml.Unmarshal([]byte(s), x) == nil {
+		n.Value = s
+		return nil
+	}
+
+	var ni literal.NumInfo
+	if err := literal.ParseNum(s, &ni); err != nil {
+		return err
+	}
+	n.Value = ni.String()
+	return nil
+}
+
+func encodeExprs(exprs []ast.Expr) (n *yaml.Node, err error) {
+	n = &yaml.Node{Kind: yaml.SequenceNode}
+
+	for _, elem := range exprs {
+		e, err := encode(elem)
+		if err != nil {
+			return nil, err
+		}
+		n.Content = append(n.Content, e)
+	}
+	return n, nil
+}
+
+// encodeDecls converts a sequence of declarations to a value. If it encounters
+// an embedded value, it will return this expression. This is more relaxed for
+// structs than is currently allowed for CUE, but the expectation is that this
+// will be allowed at some point. The input would still be illegal CUE.
+func encodeDecls(decls []ast.Decl) (n *yaml.Node, err error) {
+	n = &yaml.Node{Kind: yaml.MappingNode}
+
+	docForNext := strings.Builder{}
+	var lastHead, lastFoot *yaml.Node
+	hasEmbed := false
+	for _, d := range decls {
+		switch x := d.(type) {
+		default:
+			return nil, errors.Newf(x.Pos(), "yaml: unsupported node %s (%T)", internal.DebugStr(x), x)
+
+		case *ast.Package:
+			if len(n.Content) > 0 {
+				return nil, errors.Newf(x.Pos(), "invalid package clause")
+			}
+			continue
+
+		case *ast.CommentGroup:
+			docForNext.WriteString(docToYAML(x))
+			docForNext.WriteString("\n\n")
+			continue
+
+		case *ast.Field:
+			if x.Token == token.ISA {
+				return nil, errors.Newf(x.TokenPos, "yaml: definition not allowed")
+			}
+			if x.Optional != token.NoPos {
+				return nil, errors.Newf(x.Optional, "yaml: optional fields not allowed")
+			}
+			if hasEmbed {
+				return nil, errors.Newf(x.TokenPos, "yaml: embedding mixed with fields")
+			}
+			name, _, err := ast.LabelName(x.Label)
+			if err != nil {
+				return nil, errors.Newf(x.Label.Pos(), "yaml: only literal labels allowed")
+			}
+
+			label := &yaml.Node{}
+			addDocs(x.Label, label, label)
+			label.SetString(name)
+
+			value, err := encode(x.Value)
+			if err != nil {
+				return nil, err
+			}
+			lastHead = label
+			lastFoot = value
+			addDocs(x, label, value)
+			n.Content = append(n.Content, label)
+			n.Content = append(n.Content, value)
+
+		case *ast.EmbedDecl:
+			if hasEmbed {
+				return nil, errors.Newf(x.Pos(), "yaml: multiple embedded values")
+			}
+			hasEmbed = true
+			e, err := encode(x.Expr)
+			if err != nil {
+				return nil, err
+			}
+			addDocs(x, e, e)
+			lastHead = e
+			lastFoot = e
+			n.Content = append(n.Content, e)
+		}
+		if docForNext.Len() > 0 {
+			docForNext.WriteString(lastHead.HeadComment)
+			lastHead.HeadComment = docForNext.String()
+			docForNext.Reset()
+		}
+	}
+
+	if docForNext.Len() > 0 && lastFoot != nil {
+		if !strings.HasSuffix(lastFoot.FootComment, "\n") {
+			lastFoot.FootComment += "\n"
+		}
+		n := docForNext.Len()
+		lastFoot.FootComment += docForNext.String()[:n-1]
+	}
+
+	if hasEmbed {
+		return n.Content[0], nil
+	}
+
+	return n, nil
+}
+
+// addDocs prefixes head, replaces line and appends foot comments.
+func addDocs(n ast.Node, h, f *yaml.Node) {
+	head := ""
+	isDoc := false
+	for _, c := range ast.Comments(n) {
+		switch {
+		case c.Line:
+			f.LineComment = docToYAML(c)
+
+		case c.Position > 0:
+			if f.FootComment != "" {
+				f.FootComment += "\n\n"
+			} else if relPos := c.Pos().RelPos(); relPos == token.NewSection {
+				f.FootComment += "\n"
+			}
+			f.FootComment += docToYAML(c)
+
+		default:
+			if head != "" {
+				head += "\n\n"
+			}
+			head += docToYAML(c)
+			isDoc = isDoc || c.Doc
+		}
+	}
+
+	if head != "" {
+		if h.HeadComment != "" || !isDoc {
+			head += "\n\n"
+		}
+		h.HeadComment = head + h.HeadComment
+	}
+}
+
+// docToYAML converts a CUE CommentGroup to a YAML comment string. This ensures
+// that comments with empty lines get properly converted.
+func docToYAML(c *ast.CommentGroup) string {
+	s := c.Text()
+	if strings.HasSuffix(s, "\n") { // always true
+		s = s[:len(s)-1]
+	}
+	lines := strings.Split(s, "\n")
+	for i, l := range lines {
+		if l == "" {
+			lines[i] = "#"
+		} else {
+			lines[i] = "# " + l
+		}
+	}
+	return strings.Join(lines, "\n")
+}
diff --git a/internal/encoding/yaml/encode_test.go b/internal/encoding/yaml/encode_test.go
new file mode 100644
index 0000000..c73a2cc
--- /dev/null
+++ b/internal/encoding/yaml/encode_test.go
@@ -0,0 +1,343 @@
+// Copyright 2020 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package yaml
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/kr/pretty"
+	"gopkg.in/yaml.v3"
+
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/parser"
+)
+
+func TestEncodeFile(t *testing.T) {
+	testCases := []struct {
+		name string
+		in   string
+		out  string
+	}{{
+		name: "foo",
+		in: `
+		package test
+
+		seq: [
+			1, 2, 3, {
+				a: 1
+				b: 2
+			}
+		]
+		a: b: c: 3
+		b: {
+			x: 0
+			y: 1
+			z: 2
+		}
+		`,
+		out: `
+seq:
+  - 1
+  - 2
+  - 3
+  - a: 1
+    b: 2
+a:
+    b:
+        c: 3
+b:
+    x: 0
+    y: 1
+    z: 2
+		`,
+	}, {
+		name: "oneLineFields",
+		in: `
+		seq: [1, 2, 3]
+		map: {a: 3}
+		str: "str"
+		int: 1K
+		bin: 0b11
+		hex: 0x11
+		oct: 0o11
+		dec: .3
+		dat: '\x80'
+		nil: null
+		yes: true
+		non: false
+		`,
+		out: `
+seq: [1, 2, 3]
+map: {a: 3}
+str: str
+int: 1000
+bin: 0b11
+hex: 0x11
+oct: 0o11
+dec: .3
+dat: !!binary gA==
+nil: null
+yes: true
+non: false
+`,
+	}, {
+		name: "comments",
+		in: `
+// Document
+
+// head 1
+f1: 1
+// foot 1
+
+// head 2
+f2: 2 // line 2
+
+// intermezzo f2
+//
+// with multiline
+
+// head 3
+f3:
+	// struct doc
+	{
+		a: 1
+	}
+
+f4: {
+} // line 4
+
+// Trailing
+`,
+		out: `
+# Document
+
+# head 1
+f1: 1
+# foot 1
+
+# head 2
+f2: 2 # line 2
+
+# intermezzo f2
+#
+# with multiline
+
+# head 3
+f3:
+    # struct doc
+    a: 1
+f4: {} # line 4
+
+# Trailing
+`,
+	}, {
+		// TODO: support this at some point
+		name: "embed",
+		in: `
+	// octal
+	0o755 // line
+	// trail
+	`,
+		out: `
+# octal
+0o755 # line
+# trail
+`,
+	}, {
+		// TODO: support this at some point
+		name: "anchors",
+		in: `
+		a: b
+		b: 3
+		`,
+		out: "yaml: unsupported node b (*ast.Ident)",
+	}, {
+		name: "errors",
+		in: `
+			m: {
+				a: 1
+				b: 3
+			}
+			c: [1, [ x for x in m ]]
+			`,
+		out: "yaml: unsupported node [x for x in m ] (*ast.ListComprehension)",
+	}, {
+		name: "disallowMultipleEmbeddings",
+		in: `
+		1
+		1
+		`,
+		out: "yaml: multiple embedded values",
+	}, {
+		name: "disallowDefinitions",
+		in:   `a :: 2 `,
+		out:  "yaml: definition not allowed",
+	}, {
+		name: "disallowOptionals",
+		in:   `a?: 2`,
+		out:  "yaml: optional fields not allowed",
+	}, {
+		name: "disallowBulkOptionals",
+		in:   `[string]: 2`,
+		out:  "yaml: only literal labels allowed",
+	}, {
+		name: "noImports",
+		in: `
+		import "foo"
+
+		a: 1
+		`,
+		out: `yaml: unsupported node import "foo" (*ast.ImportDecl)`,
+	}, {
+		name: "disallowMultipleEmbeddings",
+		in: `
+		1
+		a: 2
+		`,
+		out: "yaml: embedding mixed with fields",
+	}, {
+		name: "prometheus",
+		in: `
+		{
+			receivers: [{
+				name: "pager"
+				slack_configs: [{
+					text: """
+						{{ range .Alerts }}{{ .Annotations.description }}
+						{{ end }}
+						"""
+					channel:       "#cloudmon"
+					send_resolved: true
+				}]
+			}]
+			route: {
+				receiver: "pager"
+				group_by: ["alertname", "cluster"]
+			}
+		}`,
+		out: `
+receivers:
+  - name: pager
+    slack_configs:
+      - text: |-
+            {{ range .Alerts }}{{ .Annotations.description }}
+            {{ end }}
+        channel: '#cloudmon'
+        send_resolved: true
+route:
+    receiver: pager
+    group_by: [alertname, cluster]
+		`,
+	}}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			f, err := parser.ParseFile(tc.name, tc.in, parser.ParseComments)
+			if err != nil {
+				t.Fatal(err)
+			}
+			b, err := Encode(f)
+			var got string
+			if err != nil {
+				got = err.Error()
+			} else {
+				got = strings.TrimSpace(string(b))
+			}
+			want := strings.TrimSpace(tc.out)
+			if got != want {
+				t.Error(cmp.Diff(got, want))
+			}
+		})
+	}
+}
+
+func TestEncodeAST(t *testing.T) {
+	comment := func(s string) *ast.CommentGroup {
+		return &ast.CommentGroup{List: []*ast.Comment{
+			&ast.Comment{Text: "// " + s},
+		}}
+	}
+	testCases := []struct {
+		name string
+		in   ast.Expr
+		out  string
+	}{{
+		in: ast.NewStruct(
+			comment("foo"),
+			comment("bar"),
+			"field", ast.NewString("value"),
+			"field2", ast.NewString("value"),
+			comment("trail1"),
+			comment("trail2"),
+		),
+		out: `
+# foo
+
+# bar
+
+field: value
+field2: value
+
+# trail1
+
+# trail2
+		`,
+	}, {
+		in: &ast.StructLit{Elts: []ast.Decl{
+			comment("bar"),
+			&ast.EmbedDecl{Expr: ast.NewBool(true)},
+		}},
+		out: `
+# bar
+
+true
+		`,
+	}}
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			n, err := encode(tc.in)
+			if err != nil {
+				t.Fatal(err)
+			}
+			b, err := yaml.Marshal(n)
+			if err != nil {
+				t.Fatal(err)
+			}
+			got := strings.TrimSpace(string(b))
+			want := strings.TrimSpace(tc.out)
+			if got != want {
+				t.Error(cmp.Diff(got, want))
+			}
+		})
+	}
+}
+
+// TestX is for experimentation with the YAML package to figure out the
+// semantics of the Node type.
+func TestX(t *testing.T) {
+	t.Skip()
+	var n yaml.Node
+	yaml.Unmarshal([]byte(`
+# map
+map: {
+	# doc
+} # line 1
+
+`), &n)
+
+	pretty.Print(n)
+	t.Fail()
+}
diff --git a/pkg/encoding/yaml/manual.go b/pkg/encoding/yaml/manual.go
index 053ab6b..4697982 100644
--- a/pkg/encoding/yaml/manual.go
+++ b/pkg/encoding/yaml/manual.go
@@ -18,17 +18,23 @@
 	"bytes"
 	"io"
 
-	goyaml "github.com/ghodss/yaml"
-
 	"cuelang.org/go/cue"
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/internal"
+	cueyaml "cuelang.org/go/internal/encoding/yaml"
 	"cuelang.org/go/internal/third_party/yaml"
 )
 
 // Marshal returns the YAML encoding of v.
 func Marshal(v cue.Value) (string, error) {
-	b, err := goyaml.Marshal(v)
+	if err := v.Validate(cue.Concrete(true)); err != nil {
+		if err := v.Validate(); err != nil {
+			return "", err
+		}
+		return "", internal.ErrIncomplete
+	}
+	n := v.Syntax(cue.Final())
+	b, err := cueyaml.Encode(n)
 	return string(b), err
 }
 
@@ -44,7 +50,15 @@
 		if i > 0 {
 			buf.WriteString("---\n")
 		}
-		b, err := goyaml.Marshal(iter.Value())
+		v := iter.Value()
+		if err := v.Validate(cue.Concrete(true)); err != nil {
+			if err := v.Validate(); err != nil {
+				return "", err
+			}
+			return "", internal.ErrIncomplete
+		}
+		n := v.Syntax(cue.Final())
+		b, err := cueyaml.Encode(n)
 		if err != nil {
 			return "", err
 		}