diff --git a/go.mod b/go.mod index 3089b03925..b6058ee85c 100644 --- a/go.mod +++ b/go.mod @@ -62,6 +62,7 @@ require ( github.com/vbauerster/mpb/v8 v8.4.0 github.com/vishvananda/netlink v1.2.1-beta.2 go.etcd.io/bbolt v1.3.7 + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 golang.org/x/net v0.12.0 golang.org/x/sync v0.3.0 golang.org/x/sys v0.10.0 @@ -182,7 +183,6 @@ require ( go.opentelemetry.io/otel/trace v1.15.0 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.11.0 // indirect - golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/mod v0.10.0 // indirect golang.org/x/oauth2 v0.9.0 // indirect golang.org/x/tools v0.9.3 // indirect diff --git a/pkg/specgen/generate/kube/kube.go b/pkg/specgen/generate/kube/kube.go index bec3bc9569..82a26b9782 100644 --- a/pkg/specgen/generate/kube/kube.go +++ b/pkg/specgen/generate/kube/kube.go @@ -27,6 +27,7 @@ import ( "github.com/containers/podman/v4/pkg/domain/entities" v1 "github.com/containers/podman/v4/pkg/k8s.io/api/core/v1" "github.com/containers/podman/v4/pkg/k8s.io/apimachinery/pkg/api/resource" + "github.com/containers/podman/v4/pkg/k8s.io/apimachinery/pkg/util/intstr" "github.com/containers/podman/v4/pkg/specgen" "github.com/containers/podman/v4/pkg/specgen/generate" systemdDefine "github.com/containers/podman/v4/pkg/systemd/define" @@ -35,6 +36,7 @@ import ( "github.com/docker/go-units" spec "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" + "golang.org/x/exp/slices" "sigs.k8s.io/yaml" ) @@ -563,10 +565,11 @@ func parseMountPath(mountPath string, readOnly bool, propagationMode *v1.MountPr return dest, opts, nil } -func probeToHealthConfig(probe *v1.Probe) (*manifest.Schema2HealthConfig, error) { +func probeToHealthConfig(probe *v1.Probe, containerPorts []v1.ContainerPort) (*manifest.Schema2HealthConfig, error) { var commandString string failureCmd := "exit 1" probeHandler := probe.Handler + host := "localhost" // Kubernetes default is host IP, but with Podman currently we run inside the container // configure healthcheck on the basis of Handler Actions. switch { @@ -583,7 +586,6 @@ func probeToHealthConfig(probe *v1.Probe) (*manifest.Schema2HealthConfig, error) if probeHandler.HTTPGet.Scheme != "" { uriScheme = probeHandler.HTTPGet.Scheme } - host := "localhost" // Kubernetes default is host IP, but with Podman there is only one node if probeHandler.HTTPGet.Host != "" { host = probeHandler.HTTPGet.Host } @@ -591,13 +593,38 @@ func probeToHealthConfig(probe *v1.Probe) (*manifest.Schema2HealthConfig, error) if probeHandler.HTTPGet.Path != "" { path = probeHandler.HTTPGet.Path } - commandString = fmt.Sprintf("curl -f %s://%s:%d%s || %s", uriScheme, host, probeHandler.HTTPGet.Port.IntValue(), path, failureCmd) + portNum, err := getPortNumber(probeHandler.HTTPGet.Port, containerPorts) + if err != nil { + return nil, err + } + commandString = fmt.Sprintf("curl -f %s://%s:%d%s || %s", uriScheme, host, portNum, path, failureCmd) case probeHandler.TCPSocket != nil: - commandString = fmt.Sprintf("nc -z -v %s %d || %s", probeHandler.TCPSocket.Host, probeHandler.TCPSocket.Port.IntValue(), failureCmd) + portNum, err := getPortNumber(probeHandler.TCPSocket.Port, containerPorts) + if err != nil { + return nil, err + } + if probeHandler.TCPSocket.Host != "" { + host = probeHandler.TCPSocket.Host + } + commandString = fmt.Sprintf("nc -z -v %s %d || %s", host, portNum, failureCmd) } return makeHealthCheck(commandString, probe.PeriodSeconds, probe.FailureThreshold, probe.TimeoutSeconds, probe.InitialDelaySeconds) } +func getPortNumber(port intstr.IntOrString, containerPorts []v1.ContainerPort) (int, error) { + var portNum int + if port.Type == intstr.String && port.IntValue() == 0 { + idx := slices.IndexFunc(containerPorts, func(cp v1.ContainerPort) bool { return cp.Name == port.String() }) + if idx == -1 { + return 0, fmt.Errorf("unknown port: %s", port.String()) + } + portNum = int(containerPorts[idx].ContainerPort) + } else { + portNum = port.IntValue() + } + return portNum, nil +} + func setupLivenessProbe(s *specgen.SpecGenerator, containerYAML v1.Container, restartPolicy string) error { var err error if containerYAML.LivenessProbe == nil { @@ -605,7 +632,7 @@ func setupLivenessProbe(s *specgen.SpecGenerator, containerYAML v1.Container, re } emptyHandler := v1.Handler{} if containerYAML.LivenessProbe.Handler != emptyHandler { - s.HealthConfig, err = probeToHealthConfig(containerYAML.LivenessProbe) + s.HealthConfig, err = probeToHealthConfig(containerYAML.LivenessProbe, containerYAML.Ports) if err != nil { return err } @@ -624,7 +651,7 @@ func setupStartupProbe(s *specgen.SpecGenerator, containerYAML v1.Container, res } emptyHandler := v1.Handler{} if containerYAML.StartupProbe.Handler != emptyHandler { - healthConfig, err := probeToHealthConfig(containerYAML.StartupProbe) + healthConfig, err := probeToHealthConfig(containerYAML.StartupProbe, containerYAML.Ports) if err != nil { return err } diff --git a/pkg/specgen/generate/kube/kube_test.go b/pkg/specgen/generate/kube/kube_test.go index 9c52c03bbf..c1a8e6b64c 100644 --- a/pkg/specgen/generate/kube/kube_test.go +++ b/pkg/specgen/generate/kube/kube_test.go @@ -4,6 +4,7 @@ import ( "testing" v1 "github.com/containers/podman/v4/pkg/k8s.io/api/core/v1" + "github.com/containers/podman/v4/pkg/k8s.io/apimachinery/pkg/util/intstr" "github.com/stretchr/testify/assert" ) @@ -39,3 +40,26 @@ func TestParseMountPathRO(t *testing.T) { assert.NoError(t, err) assert.NotContains(t, options, "ro") } + +func TestGetPortNumber(t *testing.T) { + portSpec := intstr.IntOrString{Type: intstr.Int, IntVal: 3000, StrVal: "myport"} + cp1 := v1.ContainerPort{Name: "myport", ContainerPort: 4000} + cp2 := v1.ContainerPort{Name: "myport2", ContainerPort: 5000} + i, e := getPortNumber(portSpec, []v1.ContainerPort{cp1, cp2}) + assert.NoError(t, e) + assert.Equal(t, i, int(portSpec.IntVal)) + + portSpec.Type = intstr.String + i, e = getPortNumber(portSpec, []v1.ContainerPort{cp1, cp2}) + assert.NoError(t, e) + assert.Equal(t, i, 4000) + + portSpec.StrVal = "not_valid" + _, e = getPortNumber(portSpec, []v1.ContainerPort{cp1, cp2}) + assert.Error(t, e) + + portSpec.StrVal = "6000" + i, e = getPortNumber(portSpec, []v1.ContainerPort{cp1, cp2}) + assert.NoError(t, e) + assert.Equal(t, i, 6000) +} diff --git a/pkg/specgen/generate/kube/play_test.go b/pkg/specgen/generate/kube/play_test.go index 4099f9c1b4..d84d10b0fd 100644 --- a/pkg/specgen/generate/kube/play_test.go +++ b/pkg/specgen/generate/kube/play_test.go @@ -1247,6 +1247,131 @@ func TestHttpLivenessProbe(t *testing.T) { true, "http://localhost:80/", }, + { + "HttpLivenessProbeNamedPort", + specgen.SpecGenerator{}, + v1.Container{ + LivenessProbe: &v1.Probe{ + Handler: v1.Handler{ + HTTPGet: &v1.HTTPGetAction{ + Port: intstr.FromString("httpPort"), + }, + }, + }, + Ports: []v1.ContainerPort{ + {Name: "servicePort", ContainerPort: 7000}, + {Name: "httpPort", ContainerPort: 8000}, + }, + }, + "always", + true, + "http://localhost:8000/", + }, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + err := setupLivenessProbe(&test.specGenerator, test.container, test.restartPolicy) + if err == nil { + assert.Equal(t, err == nil, test.succeed) + assert.Contains(t, test.specGenerator.ContainerHealthCheckConfig.HealthConfig.Test, test.expectedURL) + } + }) + } +} + +func TestTCPLivenessProbe(t *testing.T) { + tests := []struct { + name string + specGenerator specgen.SpecGenerator + container v1.Container + restartPolicy string + succeed bool + expectedHost string + expectedPort string + }{ + { + "TCPLivenessProbeNormal", + specgen.SpecGenerator{}, + v1.Container{ + LivenessProbe: &v1.Probe{ + Handler: v1.Handler{ + TCPSocket: &v1.TCPSocketAction{ + Host: "127.0.0.1", + Port: intstr.FromInt(8080), + }, + }, + }, + }, + "always", + true, + "127.0.0.1", + "8080", + }, + { + "TCPLivenessProbeHostUsesDefault", + specgen.SpecGenerator{}, + v1.Container{ + LivenessProbe: &v1.Probe{ + Handler: v1.Handler{ + TCPSocket: &v1.TCPSocketAction{ + Port: intstr.FromInt(200), + }, + }, + }, + }, + "always", + true, + "localhost", + "200", + }, + { + "TCPLivenessProbeUseNamedPort", + specgen.SpecGenerator{}, + v1.Container{ + LivenessProbe: &v1.Probe{ + Handler: v1.Handler{ + TCPSocket: &v1.TCPSocketAction{ + Port: intstr.FromString("servicePort"), + Host: "myservice.domain.com", + }, + }, + }, + Ports: []v1.ContainerPort{ + {ContainerPort: 6000}, + {Name: "servicePort", ContainerPort: 4000}, + {Name: "2ndServicePort", ContainerPort: 3000}, + }, + }, + "always", + true, + "myservice.domain.com", + "4000", + }, + { + "TCPLivenessProbeInvalidPortName", + specgen.SpecGenerator{}, + v1.Container{ + LivenessProbe: &v1.Probe{ + Handler: v1.Handler{ + TCPSocket: &v1.TCPSocketAction{ + Port: intstr.FromString("3rdservicePort"), + Host: "myservice.domain.com", + }, + }, + }, + Ports: []v1.ContainerPort{ + {ContainerPort: 6000}, + {Name: "servicePort", ContainerPort: 4000}, + {Name: "2ndServicePort", ContainerPort: 3000}, + }, + }, + "always", + false, + "myservice.domain.com", + "4000", + }, } for _, test := range tests { @@ -1254,7 +1379,10 @@ func TestHttpLivenessProbe(t *testing.T) { t.Run(test.name, func(t *testing.T) { err := setupLivenessProbe(&test.specGenerator, test.container, test.restartPolicy) assert.Equal(t, err == nil, test.succeed) - assert.Contains(t, test.specGenerator.ContainerHealthCheckConfig.HealthConfig.Test, test.expectedURL) + if err == nil { + assert.Contains(t, test.specGenerator.ContainerHealthCheckConfig.HealthConfig.Test, test.expectedHost) + assert.Contains(t, test.specGenerator.ContainerHealthCheckConfig.HealthConfig.Test, test.expectedPort) + } }) } }