mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-12 19:03:45 +00:00
enhance(k8s): reach Kubernetes services by their id when it is a DNS subdomain (#1320)
On Kubernetes a service is reached by its id made a DNS label, so some service ids might become unreachable. For example, the `devstoreaccount1.azurite.local` service of Gitea's [MSSQL tests](https://github.com/go-gitea/gitea/blob/2fe74d3cdd5184a7e22e32e47772caa78d077489/.github/workflows/pull-db-tests.yml#L229) is only in the pod's hosts as `devstoreaccount1-azurite-local`, while the tests connect to the id as written. Container names must be DNS labels, but `hostAliases` accepts DNS subdomains, so an id that is one is now added there too, and to `no_proxy`, as on Docker where a service is reached by its id. The warning for a service not reachable by its id now also covers ids that differ only in case, as Kubernetes hostnames are lowercase. --------- Co-authored-by: silverwind <[email protected]> Reviewed-on: https://gitea.com/gitea/runner/pulls/1320 Reviewed-by: Lunny Xiao <[email protected]> Reviewed-by: bircni <[email protected]> Reviewed-by: silverwind <[email protected]> Co-authored-by: Zettat123 <[email protected]>
This commit is contained in:
1 parent
b9aa47d1a5
commit
755af87095
4 files changed
+39
-11
No files matched your search
@@ -93,17 +93,29 @@ func (p *KubernetesPod) JobContainer(input *NewContainerInput) ExecutionsEnviron
|
|||||||
return p.job
|
return p.job
|
||||||
}
|
}
|
||||||
|
|
||||||
var invalidLabelChars = regexp.MustCompile(`[^a-z0-9-]+`)
|
var (
|
||||||
|
invalidLabelChars = regexp.MustCompile(`[^a-z0-9-]+`)
|
||||||
|
dnsSubdomain = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
|
||||||
|
)
|
||||||
|
|
||||||
// KubernetesServiceName returns the container name and hostname of service id, made a DNS label as both must be.
|
// kubernetesServiceName returns the container name of service id, made a DNS label as it must be.
|
||||||
func KubernetesServiceName(id string) string {
|
func kubernetesServiceName(id string) string {
|
||||||
name := strings.Trim(invalidLabelChars.ReplaceAllString(strings.ToLower(id), "-"), "-")
|
name := strings.Trim(invalidLabelChars.ReplaceAllString(strings.ToLower(id), "-"), "-")
|
||||||
return strings.TrimRight(name[:min(63, len(name))], "-")
|
return strings.TrimRight(name[:min(63, len(name))], "-")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceContainer returns a container of the pod the job reaches at hostname KubernetesServiceName(id).
|
// KubernetesServiceHostnames returns the hostnames of service id, its container name and, where hostAliases accept it as a DNS subdomain, id lowercased.
|
||||||
|
func KubernetesServiceHostnames(id string) []string {
|
||||||
|
name, host := kubernetesServiceName(id), strings.ToLower(id)
|
||||||
|
if host != name && len(host) <= 253 && dnsSubdomain.MatchString(host) { // https://github.com/kubernetes/apimachinery/blob/v0.35.1/pkg/util/validation/validation.go#L198-L207
|
||||||
|
return []string{name, host}
|
||||||
|
}
|
||||||
|
return []string{name}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceContainer returns a container of the pod the job reaches at KubernetesServiceHostnames(id).
|
||||||
func (p *KubernetesPod) ServiceContainer(id string, input *NewContainerInput) ExecutionsEnvironment {
|
func (p *KubernetesPod) ServiceContainer(id string, input *NewContainerInput) ExecutionsEnvironment {
|
||||||
service := &kubernetesContainer{pod: p, id: id, name: KubernetesServiceName(id), input: input}
|
service := &kubernetesContainer{pod: p, id: id, name: kubernetesServiceName(id), input: input}
|
||||||
p.services = append(p.services, service)
|
p.services = append(p.services, service)
|
||||||
return service
|
return service
|
||||||
}
|
}
|
||||||
@@ -131,7 +143,7 @@ func (p *KubernetesPod) create(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, service := range p.services {
|
for _, service := range p.services {
|
||||||
if !strings.EqualFold(service.id, service.name) {
|
if !slices.ContainsFunc(KubernetesServiceHostnames(service.id), func(hostname string) bool { return strings.EqualFold(hostname, service.id) }) { // glibc and Go match /etc/hosts case-insensitively
|
||||||
common.Logger(ctx).Warnf("Service %s is reachable as %s or localhost, as a Kubernetes hostname cannot be %q", service.id, service.name, service.id)
|
common.Logger(ctx).Warnf("Service %s is reachable as %s or localhost, as a Kubernetes hostname cannot be %q", service.id, service.name, service.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,7 +177,8 @@ func (p *KubernetesPod) manifest(secret string, secretData map[string]string, me
|
|||||||
serviceTemplate = templateContainers[index]
|
serviceTemplate = templateContainers[index]
|
||||||
templateSpec["containers"] = slices.Delete(slices.Clone(templateContainers), index, index+1)
|
templateSpec["containers"] = slices.Delete(slices.Clone(templateContainers), index, index+1)
|
||||||
}
|
}
|
||||||
var containers, hostnames, volumes []any
|
var containers, volumes []any
|
||||||
|
var hostnames []string
|
||||||
names := map[string]bool{}
|
names := map[string]bool{}
|
||||||
for _, container := range templateContainers {
|
for _, container := range templateContainers {
|
||||||
names[manifestField(container, "name")] = true
|
names[manifestField(container, "name")] = true
|
||||||
@@ -184,7 +197,7 @@ func (p *KubernetesPod) manifest(secret string, secretData map[string]string, me
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
containers = append(containers, mergeManifest(serviceTemplate, spec, ""))
|
containers = append(containers, mergeManifest(serviceTemplate, spec, ""))
|
||||||
hostnames = append(hostnames, c.name)
|
hostnames = append(hostnames, KubernetesServiceHostnames(c.id)...)
|
||||||
}
|
}
|
||||||
for _, name := range []string{"workspace", "act"} {
|
for _, name := range []string{"workspace", "act"} {
|
||||||
if !slices.ContainsFunc(templateVolumes, func(volume any) bool { return manifestField(volume, "name") == name }) {
|
if !slices.ContainsFunc(templateVolumes, func(volume any) bool { return manifestField(volume, "name") == name }) {
|
||||||
|
|||||||
@@ -129,6 +129,19 @@ func serveLocalExec(t *testing.T, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestKubernetesServiceHostnames(t *testing.T) {
|
||||||
|
for id, hostnames := range map[string][]string{
|
||||||
|
"db": {"db"},
|
||||||
|
"My_DB": {"my-db"},
|
||||||
|
"devstoreaccount1.azurite.local": {"devstoreaccount1-azurite-local", "devstoreaccount1.azurite.local"},
|
||||||
|
"DB.Local": {"db-local", "db.local"},
|
||||||
|
strings.Repeat("a", 64): {strings.Repeat("a", 63), strings.Repeat("a", 64)},
|
||||||
|
strings.Repeat("a", 254): {strings.Repeat("a", 63)},
|
||||||
|
} {
|
||||||
|
assert.Equal(t, hostnames, KubernetesServiceHostnames(id), id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestKubernetesPodRunsJobThroughAPI(t *testing.T) {
|
func TestKubernetesPodRunsJobThroughAPI(t *testing.T) {
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
t.Skip("uses POSIX shell")
|
t.Skip("uses POSIX shell")
|
||||||
|
|||||||
+1
-1
@@ -51,7 +51,7 @@ Job pods mount no service account token unless the template sets `automountServi
|
|||||||
|
|
||||||
## Services
|
## Services
|
||||||
|
|
||||||
Services share the pod's network, so a step reaches one by its id or `localhost`, and two services cannot listen on the same port. An id is lowercased and has characters other than letters, digits and `-` replaced by `-`, so the service `my_db` is reached as `my-db`. Pass secrets to services through `env`, as `command` is visible in the pod spec. Of `options`, only `--health-*` is supported. They become a startup probe with Docker's defaults, which stops a service that fails its retries, so the job fails as on Docker.
|
Services share the pod's network, so a step reaches one by its id or `localhost`, and two services cannot listen on the same port. An id that is not a valid hostname is sanitized, so `my_db` is reached as `my-db`. Pass secrets to services through `env`, as `command` is visible in the pod spec. Of `options`, only `--health-*` is supported. They become a startup probe with Docker's defaults, which stops a service that fails its retries, so the job fails as on Docker.
|
||||||
|
|
||||||
## Unsupported
|
## Unsupported
|
||||||
|
|
||||||
|
|||||||
@@ -501,9 +501,11 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
|
|||||||
_ = job.RawServices.Decode(&services) // a whole-value expression names no service before evaluation
|
_ = job.RawServices.Decode(&services) // a whole-value expression names no service before evaluation
|
||||||
serviceNames := slices.Sorted(maps.Keys(services))
|
serviceNames := slices.Sorted(maps.Keys(services))
|
||||||
if strings.HasPrefix(platform, labels.SchemeKubernetes+"://") {
|
if strings.HasPrefix(platform, labels.SchemeKubernetes+"://") {
|
||||||
for index, name := range serviceNames {
|
var hostnames []string
|
||||||
serviceNames[index] = container.KubernetesServiceName(name)
|
for _, name := range serviceNames {
|
||||||
|
hostnames = append(hostnames, container.KubernetesServiceHostnames(name)...)
|
||||||
}
|
}
|
||||||
|
serviceNames = hostnames
|
||||||
}
|
}
|
||||||
proxyEnv := JobProxyEnv(envs, cacheURL, serviceNames)
|
proxyEnv := JobProxyEnv(envs, cacheURL, serviceNames)
|
||||||
maps.Copy(envs, proxyEnv)
|
maps.Copy(envs, proxyEnv)
|
||||||
|
|||||||
Reference in new issue
Block a user