All three of the post's additions survive at v1, and the one it published as optional with a default is now required with none — the pin never calls that default a default, and the only one left is what kubectl create ingress reads off an asterisk it puts at both ends of a path¶
Post — Improvements to the Ingress API in Kubernetes
1.18, 2
April 2020, by Rob Scott (Google) and Christopher M Luciano (IBM). 88 lines and 5,571 bytes. Three
additions, announced in a bulleted list at the top and then taken one at a time: a pathType field,
an IngressClass resource, and wildcards in hostnames. Two manifests, one match table, and a
closing section on where the API is going.
All three additions are still there. That is the unusual thing about this post: nothing it announces
was reverted, renamed or gated away. What happened instead is that one of the three was described in
a way the pin will not repeat, and the API it was added to stopped being developed at all. Work the
pathType thread first — it is the one where the post and the pin make incompatible statements
about the same field — and the manifests second.
As written
The lead-in (:11) names three "significant additions" to the Ingress API in Kubernetes 1.18: "a
new pathType field that can specify how Ingress paths should be matched", "a new IngressClass
resource that can specify how Ingresses should be implemented by controllers", and "support for
wildcards in hostnames".
Path types come first (:17-22), as three bullets. The first carries a parenthesis in its label,
set in bold in the post's own markup — ImplementationSpecific (default) — and then "with this
path type, matching is up to the controller implementing the IngressClass. Implementations can
treat this as a separate pathType or treat it identically to the Prefix or Exact path types."
Then "Exact: Matches the URL path exactly and with case sensitivity" and "Prefix: Matches
based on a URL path prefix split by /. Matching is case sensitive and done on a path element by
element basis." Three sentences for three types, and one parenthesis naming a default.
Ingress classes come second (:24-50), with a reason. The Ingress resource "was designed with
simplicity in mind, providing a simple set of fields that would be applicable in all use cases. Over
time, as use cases evolved, implementations began to rely on a long list of custom annotations for
further configuration. The new IngressClass resource provides a way to replace some of those
annotations." An IngressClass names the controller that should implement its Ingresses and may
point at a custom resource for extra parameters; the post prints one (:28-39). A new
ingressClassName field on the Ingress spec references it (:42). The
kubernetes.io/ingress.class annotation that did the same job before "never formally defined" but
"widely supported by Ingress controllers", and "should now be considered formally deprecated"
(:45). And an IngressClass annotated ingressclass.kubernetes.io/is-default-class: true becomes
the class assigned to "new Ingresses without an ingressClassName specified" (:48-50).
Wildcards come third (:52-59). "Many Ingress providers have supported wildcard hostname matching
like *.foo.com matching app1.foo.com, but until now the spec assumed an exact FQDN match of the
host." Precise matches require the host header to match the host setting; wildcard matches require
the host header to equal the suffix of the wildcard rule. A three-row table follows: bar.foo.com
matches *.foo.com, baz.bar.foo.com does not, foo.com does not, both because "wildcard only
covers a single DNS label".
Putting it All Together (:61-80) prints one Ingress using all three: ingressClassName:
"external-lb", host: "*.example.com", path: "/example", pathType: "Prefix", and a backend of
serviceName: "example-service" and servicePort: 80. Both of the post's manifests declare
apiVersion: "networking.k8s.io/v1beta1". Controller support is a caveat rather than a step
(:83): the features are new, "each Ingress controller implementation will need some time to
develop support", check your controller's documentation.
The Future of Ingress (:85-88) makes two forecasts. The API "is on pace to graduate from beta
to a stable API in Kubernetes 1.19", and it "will continue to provide a simple way to manage
inbound network traffic". Separately, "work is currently underway on a new highly configurable set
of APIs that will provide an alternative to Ingress in the future. These APIs are being referred to
as the new “Service APIs”. They are not intended to replace any existing APIs, but instead provide a
more configurable alternative for complex use cases." The post's only link is to that work's
repository, http://github.com/kubernetes-sigs/service-apis.
As it runs now — the pin is kubernetes/website at 7c76070faf9b19e6a417c446043dbafd10a7aa1d,
newest release v1.37. The Ingress concept page is 649 lines; pathType appears on eight pages in
the tree and ImplementationSpecific on three.
All three additions are at networking.k8s.io/v1, and the API they were added to is frozen.
ingress.md:21 stamps the page stable at v1.19, which is the release the post forecast. Above the
body, :24-34 and ingress-controllers.md:12-22 carry the same note twice: the project recommends
Gateway instead of Ingress, the API "has been frozen", and "is no longer being developed". What
that state is, and why an API can be generally available and finished at the same time, is already
read against a 2016
post; this exercise
takes the three additions themselves.
The field the post publishes as optional with a default is required, and has no default.
ingress-v1.md:155 lists pathType in the HTTPIngressPath table with the required marker beside
it, and the description at :156 runs to nine lines without the word default in it.
ingress.md:185-186 puts it as a rule: "Each path in an Ingress is required to have a
corresponding path type. Paths that do not include an explicit pathType will fail validation." A
field can be optional-with-a-default or required-with-none; the post announces the first and the pin
serves the second.
And the pin does not agree that there was ever a default. ImplementationSpecific occurs three
times in the whole tree: ingress.md:189-191, the generated description at ingress-v1.md:156, and
one bullet in the migration guide. That bullet, deprecation-guide.md:268-269, is the only one that
looks back: "pathType is now required for each specified path. Options are Prefix, Exact, and
ImplementationSpecific. To match the undefined v1beta1 behavior, use ImplementationSpecific."
The post says ImplementationSpecific was the default. The guide says the behaviour was undefined
and that ImplementationSpecific is what you write to reproduce it. Those are two different claims
about the same six months of the same API, and the pin holds no record of the version that would
settle it.
The default came back, in kubectl create ingress, and the flag help puts the asterisk at the
other end from every one of its own examples. kubectl_create_ingress.md:141 documents --rule:
"Rule in format host/path=service:port[,tls=secretname]. Paths containing the leading character ''
are considered pathType=Prefix." Leading. The page's own examples put it last — :52-55 is headed
"Create an ingress with multiple hosts and the pathType as Prefix"* and its rules are
foo.com/path* and bar.com/admin*; :64 and :69 use foo.com/*; :60 uses
foo.com/path/subpath*. Eleven rules appear across the examples, five carry an asterisk, and all
five put it last. Whatever the tool actually does, a path type is being chosen for you off a
character, on the one field the API refuses to choose for you.
The concept page's expansion of Prefix does not parse, and the same broken sentence is on two
pages. ingress.md:195-199 reads: "Prefix: Matches based on a URL path prefix split by /.
Matching is case sensitive and done on a path element by element basis. A path element refers to the
list of labels in the path split by the / separator. A request is a match for path p if every p is
an element-wise prefix of p of the request path." The last sentence names p three times for what
must be two different things. It is in ingress-v1.md:156 in the same words, which places it in the
Go field comment both pages are drawn from rather than in either page's prose. The post's one-line
version of the same rule — "matching is case sensitive and done on a path element by element
basis" — is the half that survived intact, and it is the half that is readable. The section drifted
in one other way: the post says ImplementationSpecific matching is "up to the controller
implementing the IngressClass" (:20), and ingress.md:189-190 says it is "up to the
IngressClass" — a resource that holds a controller's name and some parameters, and does no matching
itself.
One sentence about path types is only in the generated reference. ingress-v1.md:156 ends its
ImplementationSpecific clause with "Implementations are required to support all path types."
That requirement is on controllers, it is the strongest sentence about path types anywhere at the
pin, and ingress.md's Path types section (:183-205) does not contain it. The post does not
contain it either; the post's version is a caveat pointing outward — "each Ingress controller
implementation will need some time to develop support for these new features" (:83).
The post's match table survived word for word; the paragraph above it lost its first clause.
ingress.md:244-248 is the post's three-row wildcard table with not one byte changed, typographic
quotes and column padding included. The paragraph at :239-242 is the post's sentence at :53 with
the announcement removed: "Many Ingress providers have supported wildcard hostname matching like
*.foo.com matching app1.foo.com, but until now the spec assumed an exact FQDN match of the
host" is gone, and "http host header" and "the Host setting" have become "HTTP host header"
and "the host field". Six years of edits took the news out of the paragraph and left the table
alone.
The pin carries the post's IngressClass manifest with exactly two edits, and does not carry the
other one at all. examples/service/networking/external-lb.yaml, the file ingress.md:259
renders under Ingress class, is the post's manifest from :28-39 name for name and field for
field: an IngressClass called external-lb, controller example.com/ingress-controller,
parameters of kind IngressParameters named external-lb. Two things differ. apiVersion reads
networking.k8s.io/v1, and apiGroup reads k8s.example.com where the post writes
k8s.example.com/v1alpha. The post's other manifest, the one at :64-80 that uses all three
additions, has no counterpart in the examples tree; the pin's equivalents are minimal-ingress.yaml
and ingress-wildcard-host.yaml, and neither of them sets ingressClassName and a wildcard host
and a pathType in the same file.
The second of those two edits corrects a field the post filled in with the wrong kind of value.
ingress-class-v1.md:118-119 defines it: "apiGroup is the group for the resource being
referenced. If APIGroup is not specified, the specified Kind must be in the core API group." A
group, not a group and a version. k8s.example.com/v1alpha is a group with a version stapled to it,
in a field whose sibling kind (:122-123) carries no version either. This was not a change in the
API; apiGroup has meant the group since the field existed, so the post's example was describing
something that does not exist on the day it was published. Whether the API server refuses it is a
separate question from whether it means anything, and step 1 asks the first one.
The default-class annotation acquired an admission controller, and the two pages that describe its
refusal do not describe the same refusal. DefaultIngressClass is in the default-enabled plugin
list at admission-controllers.md:130, and its own entry at :194-210 says it is Mutating, that
it "observes creation of Ingress objects that do not request any specific ingress class and
automatically adds a default ingress class to them", that it "does not do anything when no default
ingress class is configured", and that it "ignores any Ingress updates; it acts only on
creation". On the two-default case it says: "when more than one ingress class is marked as
default, it rejects any creation of Ingress with an error". Any creation. ingress.md:377-382
says something narrower: "the admission controller prevents creating new Ingress objects that don't
have an ingressClassName specified". One of those pages says an Ingress that names its class
explicitly is refused along with everything else, and the other says it is not. Step 6 asks the
cluster.
The annotation the post formally deprecates has three different statuses at the pin.
ingress.md:357-368 is headed Deprecated annotation and its body never says the annotation is
deprecated: it says it "was never formally defined, but was widely supported by Ingress
controllers" and that ingressClassName "is a replacement for that annotation, but is not a
direct equivalent". labels-annotations-taints/_index.md:1568-1576 says it flatly — "starting in
v1.18, this annotation is deprecated in favor of spec.ingressClassName". And ingress-v1.md:77,
describing ingressClassName, says both halves at once: "even though the annotation is officially
deprecated, for backwards compatibility reasons, ingress controllers should still honor that
annotation if present." So the deprecation names a replacement that the page defining the
replacement says is not equivalent, and the reference tells implementations to keep honouring the
deprecated thing. The annotation string itself appears exactly three times in the tree, once in each
of those places.
The successor the post names does not exist under that name, and the pin calls it the successor.
Service APIs appears nowhere in the documentation tree, and neither does service-apis; the
repository the post links has a different name now. What does exist is gateway.md, 283 lines, a
concept page for Gateway API — "an add-on containing API kinds" (:13), defined as custom
resources (:32), with four stable kinds (:42). Its last section is headed Migrating from
Ingress, and reads: "Gateway API is the successor to the Ingress API. However, it does not include
the Ingress kind. As a result, a one-time conversion from your existing Ingress resources to Gateway
API resources is necessary." (:259-263) The tree even registers an annotation for the tooling
that does it — gateway.networking.k8s.io/generator, example value ingress2gateway
(labels-annotations-taints/_index.md:1543-1554). Against that, the post: "They are not intended
to replace any existing APIs, but instead provide a more configurable alternative for complex use
cases."
And one of the post's own reasons was answered by the successor rather than by the addition. The
post gives IngressClass a motive: implementations "began to rely on a long list of custom
annotations for further configuration", and the new resource "provides a way to replace some of
those annotations" (:25). Some. gateway.md:34-36 names what the rest were for: Gateway API
"support[s] functionality for common traffic routing use cases such as header-based matching,
traffic weighting, and others that were only possible in Ingress by using custom annotations." The
class annotations were replaced by a field in 1.18. The routing annotations were replaced by a
different API, and the post's hedge on the word some turns out to be the whole of the story.
What this exercise does not cover, and where it lives. The networking.k8s.io/v1beta1
group-version that both of the post's manifests declare, the four field renames between v1beta1
and v1, the refusal you get for a path with no pathType, and the IngressClass gate ladder are
all walked on the 2016 Ingress
post. kubectl
convert, which would do the version half of the porting and does not ship inside kubectl any
more, belongs to the 1.16 deprecations exercise. Nothing
here runs an Ingress controller, so no step in Do observes a path being matched or a host header
being compared; every claim the post makes about matching is a claim about controller behaviour, and
this exercise stops at what the API server will accept and what it writes back.
The diff, and why — six years is long enough for a three-item announcement to end up in five different states.
Still right, and one of them word for word. The wildcard table came through unedited, and the
forecast in the last section — "the Ingress API is on pace to graduate from beta to a stable API in
Kubernetes 1.19" — is stamped on the concept page as v1.19 stable. A post that predicts the
next release correctly and then has its own table copied into the reference documentation unchanged
for six years has done the job. The pressure that usually rewrites a table is a disagreement about
behaviour; hostname wildcards had none, because there was one rule and it was short.
The post broke, on one word in a parenthesis. ImplementationSpecific (default) was true of
networking.k8s.io/v1beta1 and is not true of anything served now. The mechanism was graduation:
the field went from optional to required across a group-version boundary, which is a thing the
deprecation policy permits and a thing no amount of care in the post could have anticipated, because
at the time of writing the field had shipped in exactly one version and that version was beta. What
makes this the interesting kind of breakage is that the pin does not simply say the default was
removed; it declines to record that there was one, and calls the old behaviour undefined instead.
The reader who arrives with the post in hand is not looking at a changed fact. They are looking at a
fact the documentation now denies.
Retired by being agreed with. IngressClass and ingressClassName are not just still there;
they picked up an admission controller that is on by default, a spec.parameters extension, a
namespaced-scope field with a gate ladder of its own, and an entry in the annotations reference that
names the field as the annotation's replacement. Everything the post asks for happened. The post's
own paragraph on the subject is now the shortest description of it in existence, which is the usual
fate of a good announcement: it gets replaced by reference documentation that is longer and harder
to read.
Wrong when it was published. apiGroup: "k8s.example.com/v1alpha" is a group with a version
attached, in a field defined to hold a group. Nothing changed underneath it; the field's meaning is
the same today as it was on the day the post went up. This is the case where the diff is not a diff
at all — the pin's copy of the same manifest has the version stripped, and the reason the example
survived in the documentation is that somebody fixed it there and not here.
A plan the project abandoned, and it is the plan about the plan. The post's forecast for the
Ingress API was right. Its forecast for the thing next to the Ingress API was not: the Service
APIs were renamed, the alternative became the successor, and "not intended to replace any existing
APIs" is now contradicted by a page that says a conversion away from Ingress is necessary. The
pressure is visible in the post's own text — the motive it gives IngressClass is that
implementations "began to rely on a long list of custom annotations", and it can only claim to
replace some of them. The rest needed a different API, and once a different API exists that can
express what the annotations expressed, the API that could not is finished whether anyone intended
that or not.
Overtaken by stasis, in the negative. The three additions of 1.18 are the last three additions. One field graduated in v1.19, one gate ran from v1.21 to v1.24, and after that the Ingress page acquired a note saying the API is frozen. So the post is not a snapshot of an API in motion; without knowing it, it is the last release note the Ingress API ever got. Every subsequent piece of Ingress documentation is either a rewording, a migration instruction, or a pointer somewhere else.
The ladder — the Ingress family has exactly one feature gate at the pin,
IngressClassNamespacedParams, and its four-row ladder is transcribed on the 2016 Ingress
post at lines 88–99,
where it belongs, because it is the last thing that happened to the family rather than anything this
post announces. What the table cannot say is the thing that matters here: none of the three
additions was ever behind a gate. All three shipped as schema on a beta group-version, and the
group-version was the gate — you opted in by writing networking.k8s.io/v1beta1 at the top of the
file, and you opted out by not having upgraded. A gate ladder records a decision the cluster
operator could make; the 1.18 Ingress additions arrived without one, so on the day of the post there
was nothing to enable and nothing to turn off, and the only lever was which apiVersion you typed.
That lever is also what took the default away.
Topology — solo. One node, no Ingress controller, and
none installed by any step below. That bounds the exercise hard, and the bound is worth stating
before you start: nothing here observes a path being matched or a host header being compared.
Path types and hostname wildcards are two thirds of what the post announces, and both are, in the
end, instructions to a controller. What a controller-less cluster can answer is the other question —
what the API server accepts, what it refuses, and what it writes into an object you did not write
yourself — and every one of the disagreements found above turns out to live on that side of the
line. If you provision from scratch, the node baseline and
install is the same one every exercise uses.
Do
- The
apiGroupvalue, three ways. Start with the post'sIngressClassfrom:28-39, changed in one place only —apiVersiontonetworking.k8s.io/v1, becausenetworking.k8s.io/v1beta1is not served and that refusal is not this exercise's subject. Everything else, quoting included, is the post's.
kubectl create namespace ing18
cat > post-class.yaml <<'EOF'
apiVersion: "networking.k8s.io/v1"
kind: "IngressClass"
metadata:
name: "external-lb"
spec:
controller: "example.com/ingress-controller"
parameters:
apiGroup: "k8s.example.com/v1alpha"
kind: "IngressParameters"
name: "external-lb"
EOF
kubectl apply -f post-class.yaml
PARAMS='import json,sys; print(json.dumps(json.load(sys.stdin)["spec"]["parameters"], indent=2))'
kubectl get ingressclass external-lb -o json | python3 -c "$PARAMS"
- Now the pin's copy of the same manifest, renamed so both can exist at once, and then a value that is definitely not a DNS subdomain. Three data points on one field.
cat > pin-class.yaml <<'EOF'
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: pin-lb
spec:
controller: example.com/ingress-controller
parameters:
apiGroup: k8s.example.com
kind: IngressParameters
name: external-lb
EOF
kubectl apply -f pin-class.yaml
kubectl get ingressclass pin-lb -o json | python3 -c "$PARAMS"
kubectl apply --dry-run=server -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: bad-group
spec:
controller: example.com/ingress-controller
parameters:
apiGroup: "not a group name"
kind: IngressParameters
name: external-lb
EOF
- The post's final manifest, the one at
:64-80that uses all three additions at once, ported tonetworking.k8s.io/v1. The four field renames the port needs are walked elsewhere; what matters here is that the object goes in and comes back out.
kubectl apply -n ing18 -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
spec:
ingressClassName: external-lb
rules:
- host: "*.example.com"
http:
paths:
- path: /example
pathType: Prefix
backend:
service:
name: example-service
port:
number: 80
EOF
kubectl get ingress -n ing18
SPEC='import json,sys; d=json.load(sys.stdin); print(json.dumps(d["spec"], indent=2))'
kubectl get ingress example-ingress -n ing18 -o json | python3 -c "$SPEC"
- What
pathTypesays about itself, and what an empty one does.explainreads the same schema the API server validates against, so it is the closest thing to an authority on whether the field is optional. Then two ways of not choosing a path type that are not the same as omitting the field.
kubectl explain ingress.spec.rules.http.paths.pathType
kubectl explain ingress.spec.rules.http.paths --recursive | grep -A1 pathType
for PT in '""' '"ImplementationSpecific"' '"Nonsense"'; do
echo "--- pathType: $PT"
kubectl apply --dry-run=server -n ing18 -f - <<EOF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: pt-probe
spec:
ingressClassName: external-lb
rules:
- host: probe.example.com
http:
paths:
- path: /p
pathType: $PT
backend:
service:
name: example-service
port:
number: 80
EOF
done
- The default that came back.
kubectl create ingressinfers a path type from an asterisk; the flag help says the asterisk is the leading character and all five of the page's own asterisk-bearing examples put it last. Ask the client, which needs no cluster to answer, and while you are there ask what a path with no asterisk at all becomes — the case the page does not document.
PT='import json,sys; d=json.load(sys.stdin)
for r in d["spec"]["rules"]:
for p in r["http"]["paths"]:
print(repr(p["path"]), "->", p["pathType"])'
RULES='foo.com/path=svc:8080
foo.com/path*=svc:8080
foo.com/*path=svc:8080
foo.com/*=svc:8080
foo.com/=svc:8080'
echo "$RULES" | while read RULE; do
echo "--- $RULE"
kubectl create ingress probe --class=external-lb --rule="$RULE" \
--dry-run=client -o json | python3 -c "$PT"
done
- Hostname wildcards, on the only side of them a cluster with no controller can see. The pin's
table (
ingress.md:244-248) is about matching, which needs a controller; validation is about what shape of host string the API server will store, which does not. Six hosts, one of them the post's own.
for H in '*.example.com' '*.*.example.com' 'foo.*.com' '*' 'a.*' '*.example.com.'; do
printf '%-20s ' "$H"
kubectl apply --dry-run=server -n ing18 -f - >/dev/null 2>host.err <<EOF
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: host-probe
spec:
ingressClassName: external-lb
rules:
- host: "$H"
http:
paths:
- path: /p
pathType: Prefix
backend:
service:
name: example-service
port:
number: 80
EOF
if [ $? -eq 0 ]; then echo accepted; else echo "refused: $(tr '\n' ' ' < host.err)"; fi
done
- One default class. Mark the post's class default with the annotation the post introduces, create
an
Ingressthat names no class, and read back a field you did not write. This is theDefaultIngressClassadmission plugin, which the post does not mention because it did not exist when the post was written.
kubectl annotate ingressclass external-lb ingressclass.kubernetes.io/is-default-class=true
NOCLASS='apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: NAME
spec:
rules:
- host: noclass.example.com
http:
paths:
- path: /p
pathType: Prefix
backend:
service:
name: example-service
port:
number: 80'
echo "$NOCLASS" | sed 's/name: NAME/name: noclass-one/' | kubectl apply -n ing18 -f -
CN='import json,sys; d=json.load(sys.stdin)
print("field:", repr(d["spec"].get("ingressClassName")))'
kubectl get ingress noclass-one -n ing18 -o json | python3 -c "$CN"
- Two default classes, which is the case two pages of the pin describe differently. One says the
plugin "rejects any creation of
Ingresswith an error"; the other says it prevents creating Ingresses "that don't have aningressClassNamespecified". So try both: one Ingress with no class, one with a class named explicitly. Then, because the plugin's own entry says it "ignores anyIngressupdates", edit the object that already exists.
kubectl annotate ingressclass pin-lb ingressclass.kubernetes.io/is-default-class=true
kubectl get ingressclass -o custom-columns=NAME:.metadata.name,\
DEFAULT:'.metadata.annotations.ingressclass\.kubernetes\.io/is-default-class'
echo "$NOCLASS" | sed 's/name: NAME/name: noclass-two/' | kubectl apply -n ing18 -f -
echo "$NOCLASS" | sed 's/name: NAME/name: withclass/' \
| sed 's/^spec:/spec:\n ingressClassName: external-lb/' | kubectl apply -n ing18 -f -
kubectl patch ingress example-ingress -n ing18 --type=json \
-p='[{"op":"replace","path":"/spec/rules/0/http/paths/0/path","value":"/example2"}]'
- Back to one default class, and then the deprecated annotation against the plugin that fills in
the field. An
Ingressthat carrieskubernetes.io/ingress.classand noingressClassNameis exactly the object the post's deprecation is about, and the pin does not say anywhere what the mutating plugin does with it. Two more probes for the neighbouring cases: anIngressthat names a class which does not exist, and one created while no class is marked default at all.
kubectl annotate ingressclass pin-lb ingressclass.kubernetes.io/is-default-class-
kubectl apply -n ing18 -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: annotated
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: annotated.example.com
http:
paths:
- path: /p
pathType: Prefix
backend:
service:
name: example-service
port:
number: 80
EOF
ANN='import json,sys; d=json.load(sys.stdin)
print("field:", repr(d["spec"].get("ingressClassName")))
print("annotations:", d["metadata"].get("annotations"))'
kubectl get ingress annotated -n ing18 -o json | python3 -c "$ANN"
echo "$NOCLASS" | sed 's/name: NAME/name: ghostclass/' \
| sed 's/^spec:/spec:\n ingressClassName: no-such-class/' | kubectl apply -n ing18 -f -
kubectl annotate ingressclass external-lb ingressclass.kubernetes.io/is-default-class-
echo "$NOCLASS" | sed 's/name: NAME/name: nodefault/' | kubectl apply -n ing18 -f -
kubectl get ingress nodefault -n ing18 -o json | python3 -c "$CN"
-
The successor, counted. The post names one future API; the pin recommends a different one by name and calls it the successor. Ask the cluster the pin's own install procedure builds how much of it is there.
Expect
Step 1 applies. What to look at is the read-back: the apiGroup field is a string, and the question
is whether the API server stores k8s.example.com/v1alpha as written or objects to it. Record
which. The field is a reference to a resource that does not exist in the cluster either — no
IngressParameters kind is registered — so if the object is accepted, note that nothing about
spec.parameters has been checked against reality at all.
Step 2 gives the comparison. The pin's version, with the group alone, should behave exactly as the
post's did or not; a difference between the two is the finding, and no difference is also the
finding. Then "not a group name" — a string with spaces in it, which no group has ever been. If
that is accepted too, apiGroup carries no validation, the post's value was never going to be
caught, and the fix in external-lb.yaml was a documentation fix rather than a bug report.
Step 3 should apply cleanly and give you the post's example running at v1. In the wide listing,
the CLASS column carries external-lb and HOSTS carries *.example.com; the ADDRESS column
stays empty forever, because that is a controller's job and there is no controller. The spec
read-back is worth reading in full — it is the post's manifest with four field names changed and
nothing else, which is a fair summary of what the v1beta1-to-v1 port costs.
Step 4 is where the post's parenthesis is settled. Read the explain output for two things: whether
it marks the field required, and whether the word default appears anywhere in it. Then the three
probes. "ImplementationSpecific" is accepted; that much the post and the pin agree on. For ""
and "Nonsense", record the message each produces, and compare them with each other — an empty
string is not a missing field, and whether the API server treats them as the same mistake is not
documented anywhere at the pin.
Step 5 needs no cluster and takes a second. Five rules go in and five path types come out. Write
them down as a table before reading any further, because the interesting result is not any one row:
it is whether the row with a leading asterisk and the row with a trailing asterisk get different
answers, since the flag description at kubectl_create_ingress.md:141 and the five examples above
it cannot both be right. The row with no asterisk at all is the one the page never mentions, and
whatever it produces is the default that the API removed, reinstated by the client.
Step 6 prints one line per host. *.example.com is accepted — the post's own manifest already
proved that in step 3. The other five are the edges: two wildcard labels, a wildcard in the middle,
a bare asterisk, a wildcard as the last label, and a trailing dot. Record each verdict. None of them
tells you what a controller would match; all of them tell you what the API server considers a
legal host, and the gap between those two sets is a thing the pin never puts on one page.
Step 7 is the first mutation. The manifest names no class; the object that comes back should, and
reading .spec.ingressClassName tells you whether the plugin ran. If the value is None, check
that the annotation actually landed on the class before concluding anything — the plugin "does not
do anything when no default ingress class is configured", and a mistyped annotation key is
indistinguishable from a plugin that did not fire.
Step 8 is the disagreement. Two classes are now marked default. noclass-two names no class, and
both pages of the pin agree it is refused. withclass names external-lb explicitly, and the two
pages do not agree: admission-controllers.md:203-204 says any creation is rejected,
ingress.md:378-380 says only the ones without a class name. Record which happened, and quote the
error if there is one — that error message is the only authority on the point that exists. The
patch on example-ingress should then succeed, because the plugin acts only on creation; an
object created before the second default class existed keeps working, which means a cluster can sit
in this state indefinitely and only notice when somebody creates something.
Step 9 has three probes and one of them is unanswerable from the documentation. With one default
class again, annotated carries kubernetes.io/ingress.class: nginx and no ingressClassName —
the exact object the post deprecates. Record whether the plugin overwrites the absent field anyway,
giving you an Ingress that claims one class in an annotation and a different one in a field, or
whether it stands off. Nothing at the pin says which. Then ghostclass names a class that does not
exist, and nodefault is created with no default class configured; expect the field to survive as
written in the first case and to stay absent in the second, and note that an Ingress with a class
name pointing at nothing is as inert as one with no class name at all, for the same reason — nobody
is listening.
Step 10 is a count and an absence. The Ingress resources are all there: ingresses and
ingressclasses in networking.k8s.io/v1, exactly as the post left them. The grep -ci gateway
prints 0; there are no Gateway API custom resource definitions, because Gateway API is an add-on
and nothing in the install procedure this cluster was built with installs add-ons. So the API the
post said would not replace anything is absent from the cluster, and the API the pin calls its
successor is absent too — the recommendation on the front of the Ingress page cannot be followed
here without installing something first, which is worth holding onto when you read the word
frozen.
Read on — four of these are answerable from the pin, and the fifth is the one the post is about.
-
ingress.md:183-205andingress-v1.md:155-156are the same three path types described twice. The reference carries one sentence the concept page does not: "implementations are required to support all path types." Decide which page you would put it on, given that the concept page is the one a person reads and the reference is the one generated from the code. -
admission-controllers.md:194-210in full, againstingress.md:370-382. Your step 8 result settles which is right about the two-default case. Work out what the wrong one would have to say instead, and whether the fix is a sentence or a paragraph. -
gateway.md:259-266, Migrating from Ingress, and thengateway.md:34-36. Read the second one as the answer to the post's:25— the paragraph that givesIngressClassits motive and concedes it replaces only some of the annotations. -
kubectl_create_ingress.md, the examples at:37-69against the--ruledescription at:141. Step 5 tells you what the tool does. Decide which of the two is the defect, and note that a path type is being inferred here for a field the API server refuses to infer. -
Unanswerable from the pin: whether
pathTypegenuinely defaulted toImplementationSpecificinnetworking.k8s.io/v1beta1. The post says it did, in a bold label. The migration guide atdeprecation-guide.md:268-269calls thev1beta1behaviour undefined and givesImplementationSpecificas the value you write to reproduce it, which is a different claim. Thev1beta1group-version has been unserved since v1.22 and its reference page is not in the pinned tree, so the schema that would settle it — whether the field was optional-with-a-default or optional-with-no-default-and-undefined-behaviour — is not here. This is the one case in the file where the post is the better witness, and there is nothing at the pin to check it against.
Teardown — one namespace, two cluster-scoped classes and three files.
kubectl delete namespace ing18
kubectl delete ingressclass external-lb pin-lb
rm -f post-class.yaml pin-class.yaml host.err
IngressClass is cluster-scoped, so deleting the namespace does not take the classes with it; that
is worth noticing on its way out, because it is the shape of the whole addition — a per-cluster
object that per-namespace Ingresses point at. Nothing on the node was reconfigured and no controller
was installed, so the cluster is back where it started.