cri: use upstream Kubernetes modules

Switch the CRI integration layer from containerd's forked Kubernetes helpers
and clients to the upstream Kubernetes modules, and finalize the dependency
update to Kubernetes v0.36.0.

Replace the remaining internal helper copies with upstream packages:
- internal/cri/clock -> k8s.io/utils/clock
- internal/cri/executil -> upstream CRI exec helpers
- internal/cri/resourcequantity -> k8s.io/apimachinery/pkg/api/resource
- internal/cri/setutils -> k8s.io/apimachinery/pkg/util/sets
- internal/cri/types/labels.go -> internal/cri/labels
- integration/cri-api/pkg/apis/services.go -> k8s.io/cri-api/pkg/apis/services.go

Adopt the upstream CRI clients directly:
- add k8s.io/cri-client v0.36.0, k8s.io/cri-streaming v0.36.0, and
  k8s.io/streaming v0.36.0 as direct dependencies
- promote k8s.io/utils to a direct dependency and pull in
  k8s.io/component-base v0.36.0 indirectly
- keep integration/remote as a thin containerd adapter around cri-client,
  because the integration tests still need the stream-shaped
  GetContainerEvents RPC

Finalize the Kubernetes dependency update from v0.36.0-rc.0 to v0.36.0,
refresh vendor/, and drop the obsolete internal utility copies.

Also fix the protobuf MessageState mutex-copy vet failures exposed by the new
APIs and close the temporary integration CRI clients explicitly.

Signed-off-by: Davanum Srinivas <davanum@gmail.com>
This commit is contained in:
Davanum Srinivas committed 2026-04-23 12:59:58 +02:00
1 parent b3e484a57a
commit c30f23452c
161 files changed
+3338 -6754

No files matched your search

+3 -3
View File
@@ -23,7 +23,7 @@ import (
"sync"
"time"
internalapi "github.com/containerd/containerd/v2/integration/cri-api/pkg/apis"
"github.com/containerd/containerd/v2/integration/remote"
"github.com/containerd/containerd/v2/internal/cri/util"
"github.com/containerd/log"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
@@ -34,7 +34,7 @@ type criWorker struct {
wg *sync.WaitGroup
count int
failures int
client internalapi.RuntimeService
client *remote.RuntimeService
commit string
runtimeHandler string
@@ -142,7 +142,7 @@ func (w *criWorker) getID() string {
}
// cleanup cleans up any containers in the "stress" namespace before the test run
func criCleanup(ctx context.Context, client internalapi.RuntimeService) error {
func criCleanup(ctx context.Context, client *remote.RuntimeService) error {
filter := &runtime.PodSandboxFilter{
LabelSelector: map[string]string{podNamespaceLabel: stressNs},
}
+5
View File
@@ -298,6 +298,11 @@ func criTest(c config) error {
if err != nil {
return fmt.Errorf("failed to create runtime service: %w", err)
}
defer func() {
if err := client.Close(context.Background()); err != nil {
log.L.WithError(err).Warn("failed to close CRI runtime client")
}
}()
if err := criCleanup(ctx, client); err != nil {
return err
+5 -2
View File
@@ -28,7 +28,7 @@ import (
)
func TestParseStartResponse(t *testing.T) {
for _, tc := range []struct {
testCases := []struct {
Name string
Response string
Expected bootapi.BootstrapResult
@@ -76,7 +76,10 @@ func TestParseStartResponse(t *testing.T) {
Expected: bootapi.BootstrapResult{},
Err: errdefs.ErrNotImplemented,
},
} {
}
for i := range testCases {
tc := &testCases[i]
t.Run(tc.Name, func(t *testing.T) {
params, err := parseStartResponse([]byte(tc.Response))
if err != nil {
+12 -10
View File
@@ -82,13 +82,14 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9
google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af
gopkg.in/inf.v0 v0.9.1
k8s.io/apimachinery v0.36.0-rc.0
k8s.io/client-go v0.36.0-rc.0
k8s.io/cri-api v0.36.0-rc.0
k8s.io/cri-streaming v0.36.0-rc.0
k8s.io/apimachinery v0.36.0
k8s.io/client-go v0.36.0
k8s.io/cri-api v0.36.0
k8s.io/cri-client v0.36.0
k8s.io/cri-streaming v0.36.0
k8s.io/klog/v2 v2.140.0
k8s.io/streaming v0.36.0-rc.0
k8s.io/streaming v0.36.0
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5
tags.cncf.io/container-device-interface v1.1.0
)
@@ -132,8 +133,8 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sasha-s/go-deadlock v0.3.5 // indirect
github.com/smallstep/pkcs7 v0.1.1 // indirect
@@ -153,10 +154,11 @@ require (
golang.org/x/term v0.41.0 // indirect
golang.org/x/text v0.35.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.36.0-rc.0 // indirect
k8s.io/api v0.36.0 // indirect
k8s.io/component-base v0.36.0 // indirect
k8s.io/kube-openapi v0.0.0-20260319004828-5883c5ee87b9 // indirect
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
+20 -16
View File
@@ -299,13 +299,13 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@@ -564,22 +564,26 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.36.0-rc.0 h1:M/2omF5AlJ/ULd9X4pz1NE5j887d1ALIfyJb8xJ/ARs=
k8s.io/api v0.36.0-rc.0/go.mod h1:ifW/PmEIDEffdtzB6peMut2XrvdPcQbhNGnYNM121LY=
k8s.io/apimachinery v0.36.0-rc.0 h1:Bs4pa3NYboSsHUtMqHwEL+JyQU8ZbvG+JMWjaa/Tqho=
k8s.io/apimachinery v0.36.0-rc.0/go.mod h1:WKMNqQ1Bs5vSE4GwgcfetEd4IVEj77mpDYwEfl6g4x0=
k8s.io/client-go v0.36.0-rc.0 h1:DEZKLAzkKQwfjTW161IsjKSXIWUQwP+xo7sFp7sVRyE=
k8s.io/client-go v0.36.0-rc.0/go.mod h1:lyEjolpZ67DD3lgcBxEIIPDC8VAa2IZ2YH9FrWhvOjA=
k8s.io/cri-api v0.36.0-rc.0 h1:wYThctd23rzfQhifrjX/MWfSkguy5h8AoNabTZ+wcEw=
k8s.io/cri-api v0.36.0-rc.0/go.mod h1:0pB0LCI98OZ8oXa4FLukaereSw6wK+Oyb2vt9eubr/g=
k8s.io/cri-streaming v0.36.0-rc.0 h1:P2sOA+op6yzmhGsOudoD7/gfL9e6PwwwoP/YjkHz3n8=
k8s.io/cri-streaming v0.36.0-rc.0/go.mod h1:kjiVz1e3MTXBZZXqC17HtFc9W+pWEP/OUYdQrDh2p2k=
k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80=
k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34=
k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ=
k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc=
k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c=
k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y=
k8s.io/component-base v0.36.0 h1:hFjEktssxiJhrK1zfybkH4kJOi8iZuF+mIDCqS5+jRo=
k8s.io/component-base v0.36.0/go.mod h1:JZvIfcNHk+uck+8LhJzhSBtydWXaZNQwX2OdL+Mnwsk=
k8s.io/cri-api v0.36.0 h1:DSuUPB3HjUPIFBXmXIWbooJlr1euKXzPSdhpeCRgLFA=
k8s.io/cri-api v0.36.0/go.mod h1:1gMX7udEAiRCWGS4uxscdbxq6vufwhZt38Ri+XH6P00=
k8s.io/cri-client v0.36.0 h1:1xca/ii9afypuGzSnWAnaPSyYIZGC+7P2EkaEsk+RDc=
k8s.io/cri-client v0.36.0/go.mod h1:sMNSZqkBxzc/8IqPQyVg+QaKnntLn6bnP5xjOQ9OX6U=
k8s.io/cri-streaming v0.36.0 h1:ZCULHNFtJj3taZj6XEQVS5atO2M+qfJkXUMkea0hsj8=
k8s.io/cri-streaming v0.36.0/go.mod h1:AGYm+qv2gm7CTj9Gotc6CxPy7xEvyJGVvc3RhWNK5NQ=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260319004828-5883c5ee87b9 h1:Sztf7ESG9tAXRW/ACJZjrj5jhdOUqS2KFRQT+CTvu78=
k8s.io/kube-openapi v0.0.0-20260319004828-5883c5ee87b9/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
k8s.io/streaming v0.36.0-rc.0 h1:y/CrjmAvz4Fkza9Vt3Bep+rRJrLiu2Gj6Zw2I+eQizU=
k8s.io/streaming v0.36.0-rc.0/go.mod h1:5Zm1U2Duu3uu1nG/ijKuwNWDkQk24aXVpScAhzRkzkk=
k8s.io/streaming v0.36.0 h1:agnTxU+NFulUrtYzXUGKO3ndEa8jKwht1Kwn9nu9x+4=
k8s.io/streaming v0.36.0/go.mod h1:z6fV3D+NVkoeqRMtWwlUZK6U17SY/LqNzOxWL6GyR/s=
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 h1:kBawHLSnx/mYHmRnNUf9d4CpjREbeZuxoSGOX/J+aYM=
k8s.io/utils v0.0.0-20260319190234-28399d86e0b5/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
@@ -17,6 +17,7 @@
package integration
import (
"context"
"fmt"
"os"
"path/filepath"
@@ -81,6 +82,7 @@ func TestContainerCgroupWritable(t *testing.T) {
t.Cleanup(func() {
cleanupPods(t, runtimeService)
assert.NoError(t, runtimeService.Close(context.Background()))
t.Log("Stopping containerd process")
require.NoError(t, currentProc.kill(syscall.SIGTERM))
require.NoError(t, currentProc.wait(5*time.Minute))
+5 -4
View File
@@ -38,9 +38,9 @@ func TestContainerEvents(t *testing.T) {
t.Log("Set up container events streaming clients")
containerEventsStreamingClient1, err := runtimeService.GetContainerEvents(ctx, &runtime.GetEventsRequest{})
assert.NoError(t, err)
require.NoError(t, err)
containerEventsStreamingClient2, err := runtimeService.GetContainerEvents(ctx, &runtime.GetEventsRequest{})
assert.NoError(t, err)
require.NoError(t, err)
containerEventsChan1 := make(chan *runtime.ContainerEventResponse)
containerEventsChan2 := make(chan *runtime.ContainerEventResponse)
@@ -147,9 +147,10 @@ func checkContainerEventResponse(t *testing.T, containerEventsChans []chan *runt
select {
case resp = <-ch:
case <-time.After(readContainerEventChannelTimeout):
t.Error("assertContainerEventResponse: timeout waiting for events from channel")
t.Fatal("assertContainerEventResponse: timeout waiting for events from channel")
}
t.Logf("Container Event response received: %+v", *resp)
require.NotNil(t, resp)
t.Logf("Container Event response received: %+v", resp)
assert.Equal(t, expectedType, resp.ContainerEventType)
// Checking only the State field of PodSandboxStatus
+2 -1
View File
@@ -101,8 +101,9 @@ func TestContainerStopCancellation(t *testing.T) {
t.Log("Stop the container with 3s timeout, but 1s context timeout")
// Note that with container pid namespace, the sleep process
// is pid 1, and SIGTERM sent by `StopContainer` will be ignored.
rawClient, err := RawRuntimeClient()
rawClient, conn, err := RawRuntimeClient()
require.NoError(t, err)
t.Cleanup(func() { _ = conn.Close() })
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
_, err = rawClient.StopContainer(ctx, &runtime.StopContainerRequest{
-150
View File
@@ -1,150 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2016 The Kubernetes 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.
*/
// this file is from k8s.io/cri-api/pkg/apis only it points to v1 as the runtimeapi not v1alpha
package cri
import (
"context"
"time"
"google.golang.org/grpc"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
)
// RuntimeVersioner contains methods for runtime name, version and API version.
type RuntimeVersioner interface {
// Version returns the runtime name, runtime version and runtime API version
Version(apiVersion string, opts ...grpc.CallOption) (*runtimeapi.VersionResponse, error)
}
// ContainerManager contains methods to manipulate containers managed by a
// container runtime. The methods are thread-safe.
type ContainerManager interface {
// CreateContainer creates a new container in specified PodSandbox.
CreateContainer(podSandboxID string, config *runtimeapi.ContainerConfig, sandboxConfig *runtimeapi.PodSandboxConfig, opts ...grpc.CallOption) (string, error)
// StartContainer starts the container.
StartContainer(containerID string, opts ...grpc.CallOption) error
// StopContainer stops a running container with a grace period (i.e., timeout).
StopContainer(containerID string, timeout int64, opts ...grpc.CallOption) error
// RemoveContainer removes the container.
RemoveContainer(containerID string, opts ...grpc.CallOption) error
// ListContainers lists all containers by filters.
ListContainers(filter *runtimeapi.ContainerFilter, opts ...grpc.CallOption) ([]*runtimeapi.Container, error)
// ContainerStatus returns the status of the container.
ContainerStatus(containerID string, opts ...grpc.CallOption) (*runtimeapi.ContainerStatus, error)
// UpdateContainerResources updates the cgroup resources for the container.
UpdateContainerResources(containerID string, resources *runtimeapi.LinuxContainerResources, windowsResources *runtimeapi.WindowsContainerResources, opts ...grpc.CallOption) error
// ExecSync executes a command in the container, and returns the stdout output.
// If command exits with a non-zero exit code, an error is returned.
ExecSync(containerID string, cmd []string, timeout time.Duration, opts ...grpc.CallOption) (stdout []byte, stderr []byte, err error)
// Exec prepares a streaming endpoint to execute a command in the container, and returns the address.
Exec(req *runtimeapi.ExecRequest, opts ...grpc.CallOption) (*runtimeapi.ExecResponse, error)
// Attach prepares a streaming endpoint to attach to a running container, and returns the address.
Attach(req *runtimeapi.AttachRequest, opts ...grpc.CallOption) (*runtimeapi.AttachResponse, error)
// ReopenContainerLog asks runtime to reopen the stdout/stderr log file
// for the container. If it returns error, new container log file MUST NOT
// be created.
ReopenContainerLog(ContainerID string, opts ...grpc.CallOption) error
GetContainerEvents(ctx context.Context, request *runtimeapi.GetEventsRequest, opts ...grpc.CallOption) (runtimeapi.RuntimeService_GetContainerEventsClient, error)
}
// PodSandboxManager contains methods for operating on PodSandboxes. The methods
// are thread-safe.
type PodSandboxManager interface {
// RunPodSandbox creates and starts a pod-level sandbox. Runtimes should ensure
// the sandbox is in ready state.
RunPodSandbox(config *runtimeapi.PodSandboxConfig, runtimeHandler string, opts ...grpc.CallOption) (string, error)
// UpdatePodSandboxResources updates the cgroup resources for the sandbox.
UpdatePodSandboxResources(podSandboxID string, overhead *runtimeapi.LinuxContainerResources, resources *runtimeapi.LinuxContainerResources, opts ...grpc.CallOption) error
// StopPodSandbox stops the sandbox. If there are any running containers in the
// sandbox, they should be force terminated.
StopPodSandbox(podSandboxID string, opts ...grpc.CallOption) error
// RemovePodSandbox removes the sandbox. If there are running containers in the
// sandbox, they should be forcibly removed.
RemovePodSandbox(podSandboxID string, opts ...grpc.CallOption) error
// PodSandboxStatus returns the Status of the PodSandbox.
PodSandboxStatus(podSandboxID string, opts ...grpc.CallOption) (*runtimeapi.PodSandboxStatus, error)
// ListPodSandbox returns a list of Sandbox.
ListPodSandbox(filter *runtimeapi.PodSandboxFilter, opts ...grpc.CallOption) ([]*runtimeapi.PodSandbox, error)
// PortForward prepares a streaming endpoint to forward ports from a PodSandbox, and returns the address.
PortForward(req *runtimeapi.PortForwardRequest, opts ...grpc.CallOption) (*runtimeapi.PortForwardResponse, error)
}
// ContainerStatsManager contains methods for retrieving the container
// statistics.
type ContainerStatsManager interface {
// ContainerStats returns stats of the container. If the container does not
// exist, the call returns an error.
ContainerStats(containerID string, opts ...grpc.CallOption) (*runtimeapi.ContainerStats, error)
// ListContainerStats returns stats of all running containers.
ListContainerStats(filter *runtimeapi.ContainerStatsFilter, opts ...grpc.CallOption) ([]*runtimeapi.ContainerStats, error)
}
// RuntimeService interface should be implemented by a container runtime.
// The methods should be thread-safe.
type RuntimeService interface {
RuntimeVersioner
ContainerManager
PodSandboxManager
ContainerStatsManager
// UpdateRuntimeConfig updates runtime configuration if specified
UpdateRuntimeConfig(runtimeConfig *runtimeapi.RuntimeConfig, opts ...grpc.CallOption) error
// Status returns the status of the runtime.
Status(opts ...grpc.CallOption) (*runtimeapi.StatusResponse, error)
// RuntimeConfig returns configuration information of the runtime.
// A couple of notes:
// - The RuntimeConfigRequest object is not to be confused with the contents of UpdateRuntimeConfigRequest.
// The former is for having runtime tell Kubelet what to do, the latter vice versa.
// - It is the expectation of the Kubelet that these fields are static for the lifecycle of the Kubelet.
// The Kubelet will not re-request the RuntimeConfiguration after startup, and CRI implementations should
// avoid updating them without a full node reboot.
RuntimeConfig(in *runtimeapi.RuntimeConfigRequest, opts ...grpc.CallOption) (*runtimeapi.RuntimeConfigResponse, error)
}
// ImageManagerService interface should be implemented by a container image
// manager.
// The methods should be thread-safe.
type ImageManagerService interface {
// ListImages lists the existing images.
ListImages(filter *runtimeapi.ImageFilter, opts ...grpc.CallOption) ([]*runtimeapi.Image, error)
// ImageStatus returns the status of the image.
ImageStatus(image *runtimeapi.ImageSpec, opts ...grpc.CallOption) (*runtimeapi.Image, error)
// PullImage pulls an image with the authentication config.
PullImage(image *runtimeapi.ImageSpec, auth *runtimeapi.AuthConfig, podSandboxConfig *runtimeapi.PodSandboxConfig, runtimeHandler string, opts ...grpc.CallOption) (string, error)
// RemoveImage removes the image.
RemoveImage(image *runtimeapi.ImageSpec, opts ...grpc.CallOption) error
// ImageFsInfo returns information of the filesystem that is used to store images.
ImageFsInfo(opts ...grpc.CallOption) ([]*runtimeapi.FilesystemUsage, error)
}
+69 -24
View File
@@ -45,14 +45,13 @@ import (
containerd "github.com/containerd/containerd/v2/client"
"github.com/containerd/containerd/v2/core/containers"
cri "github.com/containerd/containerd/v2/integration/cri-api/pkg/apis"
_ "github.com/containerd/containerd/v2/integration/images" // Keep this around to parse `imageListFile` command line var
"github.com/containerd/containerd/v2/integration/remote"
dialer "github.com/containerd/containerd/v2/integration/remote/util"
criconfig "github.com/containerd/containerd/v2/internal/cri/config"
"github.com/containerd/containerd/v2/internal/cri/constants"
"github.com/containerd/containerd/v2/internal/cri/types"
podsandboxtypes "github.com/containerd/containerd/v2/internal/cri/server/podsandbox/types"
"github.com/containerd/containerd/v2/internal/cri/util"
dialer "k8s.io/cri-client/pkg/util"
)
const (
@@ -62,9 +61,9 @@ const (
)
var (
runtimeService cri.RuntimeService
runtimeService2 cri.RuntimeService // to test GetContainerEvents broadcast
imageService cri.ImageManagerService
runtimeService *remote.RuntimeService
runtimeService2 *remote.RuntimeService // to test GetContainerEvents broadcast
imageService *remote.ImageService
containerdClient *containerd.Client
containerdEndpoint string
)
@@ -79,49 +78,86 @@ func TestMain(m *testing.M) {
if err := ConnectDaemons(); err != nil {
log.L.WithError(err).Fatalf("Failed to connect daemons")
}
os.Exit(m.Run())
code := m.Run()
if err := DisconnectDaemons(); err != nil {
log.L.WithError(err).Error("Failed to disconnect daemons")
}
os.Exit(code)
}
// ConnectDaemons connect cri plugin and containerd, and initialize the clients.
func ConnectDaemons() error {
fail := func(format string, args ...any) error {
err := fmt.Errorf(format, args...)
if cleanupErr := DisconnectDaemons(); cleanupErr != nil {
return errors.Join(err, cleanupErr)
}
return err
}
var err error
runtimeService, err = remote.NewRuntimeService(*criEndpoint, timeout)
if err != nil {
return fmt.Errorf("failed to create runtime service: %w", err)
return fail("failed to create runtime service: %w", err)
}
runtimeService2, err = remote.NewRuntimeService(*criEndpoint, timeout)
if err != nil {
return fmt.Errorf("failed to create runtime service: %w", err)
return fail("failed to create runtime service: %w", err)
}
imageService, err = remote.NewImageService(*criEndpoint, timeout)
if err != nil {
return fmt.Errorf("failed to create image service: %w", err)
return fail("failed to create image service: %w", err)
}
// Since CRI grpc client doesn't have `WithBlock` specified, we
// need to check whether it is actually connected.
// TODO(#6069) Use grpc options to block on connect and remove for this list containers request.
_, err = runtimeService.ListContainers(&runtime.ContainerFilter{})
if err != nil {
return fmt.Errorf("failed to list containers: %w", err)
return fail("failed to list containers: %w", err)
}
_, err = runtimeService2.ListContainers(&runtime.ContainerFilter{})
if err != nil {
return fmt.Errorf("failed to list containers: %w", err)
return fail("failed to list containers: %w", err)
}
_, err = imageService.ListImages(&runtime.ImageFilter{})
if err != nil {
return fmt.Errorf("failed to list images: %w", err)
return fail("failed to list images: %w", err)
}
// containerdEndpoint is the same with criEndpoint now
containerdEndpoint = strings.TrimPrefix(*criEndpoint, "unix://")
containerdEndpoint = strings.TrimPrefix(containerdEndpoint, "npipe:")
containerdClient, err = containerd.New(containerdEndpoint, containerd.WithDefaultNamespace(k8sNamespace))
if err != nil {
return fmt.Errorf("failed to connect containerd: %w", err)
return fail("failed to connect containerd: %w", err)
}
return nil
}
// DisconnectDaemons tears down the shared CRI and containerd clients.
func DisconnectDaemons() error {
ctx := context.Background()
var err error
if runtimeService != nil {
err = errors.Join(err, runtimeService.Close(ctx))
runtimeService = nil
}
if runtimeService2 != nil {
err = errors.Join(err, runtimeService2.Close(ctx))
runtimeService2 = nil
}
if imageService != nil {
err = errors.Join(err, imageService.Close(ctx))
imageService = nil
}
if containerdClient != nil {
err = errors.Join(err, containerdClient.Close())
containerdClient = nil
}
return err
}
// Opts sets specific information in pod sandbox config.
type PodSandboxOpts func(*runtime.PodSandboxConfig)
@@ -735,28 +771,36 @@ func PidEnvs(pid int) (map[string]string, error) {
return res, nil
}
// RawRuntimeClient returns a raw grpc runtime service client.
func RawRuntimeClient() (runtime.RuntimeServiceClient, error) {
// RawRuntimeClient returns a raw grpc runtime service client and its connection.
func RawRuntimeClient() (runtime.RuntimeServiceClient, *grpc.ClientConn, error) {
addr, dialer, err := dialer.GetAddressAndDialer(*criEndpoint)
if err != nil {
return nil, fmt.Errorf("failed to get dialer: %w", err)
return nil, nil, fmt.Errorf("failed to get dialer: %w", err)
}
conn, err := grpc.NewClient(addr,
conn, err := grpc.NewClient(clientTargetForAddress(addr),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithContextDialer(dialer),
)
if err != nil {
return nil, fmt.Errorf("failed to connect cri endpoint: %w", err)
return nil, nil, fmt.Errorf("failed to connect cri endpoint: %w", err)
}
return runtime.NewRuntimeServiceClient(conn), nil
return runtime.NewRuntimeServiceClient(conn), conn, nil
}
func clientTargetForAddress(addr string) string {
if strings.HasPrefix(addr, "/") {
return "passthrough:///" + addr
}
return addr
}
// CRIConfig gets current cri config from containerd.
func CRIConfig() (*criconfig.Config, error) {
client, err := RawRuntimeClient()
client, conn, err := RawRuntimeClient()
if err != nil {
return nil, fmt.Errorf("failed to get raw runtime client: %w", err)
}
defer conn.Close()
resp, err := client.Status(context.Background(), &runtime.StatusRequest{Verbose: true})
if err != nil {
return nil, fmt.Errorf("failed to get status: %w", err)
@@ -769,11 +813,12 @@ func CRIConfig() (*criconfig.Config, error) {
}
// SandboxInfo gets sandbox info.
func SandboxInfo(id string) (*runtime.PodSandboxStatus, *types.SandboxInfo, error) {
client, err := RawRuntimeClient()
func SandboxInfo(id string) (*runtime.PodSandboxStatus, *podsandboxtypes.SandboxInfo, error) {
client, conn, err := RawRuntimeClient()
if err != nil {
return nil, nil, fmt.Errorf("failed to get raw runtime client: %w", err)
}
defer conn.Close()
resp, err := client.PodSandboxStatus(context.Background(), &runtime.PodSandboxStatusRequest{
PodSandboxId: id,
Verbose: true,
@@ -782,7 +827,7 @@ func SandboxInfo(id string) (*runtime.PodSandboxStatus, *types.SandboxInfo, erro
return nil, nil, fmt.Errorf("failed to get sandbox status: %w", err)
}
status := resp.GetStatus()
var info types.SandboxInfo
var info podsandboxtypes.SandboxInfo
if err := json.Unmarshal([]byte(resp.GetInfo()["info"]), &info); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal sandbox info: %w", err)
}
+2 -3
View File
@@ -34,12 +34,11 @@ import (
"github.com/opencontainers/selinux/go-selinux"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
cri "github.com/containerd/containerd/v2/integration/cri-api/pkg/apis"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/containerd/containerd/v2/integration/images"
"github.com/containerd/containerd/v2/integration/remote"
)
const (
@@ -671,7 +670,7 @@ type nriTest struct {
t *testing.T
name string
prefix string
runtime cri.RuntimeService
runtime *remote.RuntimeService
plugins []*mockPlugin
namespace string
sbCfg map[string]*runtime.PodSandboxConfig
+40 -33
View File
@@ -20,6 +20,7 @@ import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
@@ -38,7 +39,6 @@ import (
apitask "github.com/containerd/containerd/api/runtime/task/v3"
shimcore "github.com/containerd/containerd/v2/core/runtime/v2"
cri "github.com/containerd/containerd/v2/integration/cri-api/pkg/apis"
"github.com/containerd/containerd/v2/integration/images"
"github.com/containerd/containerd/v2/integration/remote"
"github.com/containerd/containerd/v2/pkg/namespaces"
@@ -47,7 +47,7 @@ import (
)
// upgradeVerifyCaseFunc is used to verify the behavior after upgrade.
type upgradeVerifyCaseFunc func(*testing.T, cri.RuntimeService, cri.ImageManagerService)
type upgradeVerifyCaseFunc func(*testing.T, *remote.RuntimeService, *remote.ImageService)
// beforeUpgradeHookFunc is a hook before upgrade.
type beforeUpgradeHookFunc func(*testing.T)
@@ -56,7 +56,7 @@ type beforeUpgradeHookFunc func(*testing.T)
//
// Each upgradeVerifyCaseFunc is used to verify the behavior after restarting
// with current release.
type setupUpgradeVerifyCase func(*testing.T, int, cri.RuntimeService, cri.ImageManagerService) ([]upgradeVerifyCaseFunc, beforeUpgradeHookFunc)
type setupUpgradeVerifyCase func(*testing.T, int, *remote.RuntimeService, *remote.ImageService) ([]upgradeVerifyCaseFunc, beforeUpgradeHookFunc)
// TODO: Support Windows
func TestUpgrade(t *testing.T) {
@@ -85,7 +85,7 @@ func TestUpgrade(t *testing.T) {
func runUpgradeTestCase(
previousVersion string,
previousReleaseBinDir string,
setupUpgradeVerifyCase func(*testing.T, int, cri.RuntimeService, cri.ImageManagerService) ([]upgradeVerifyCaseFunc, beforeUpgradeHookFunc),
setupUpgradeVerifyCase func(*testing.T, int, *remote.RuntimeService, *remote.ImageService) ([]upgradeVerifyCaseFunc, beforeUpgradeHookFunc),
) func(t *testing.T) {
return runUpgradeTestCaseWithExistingConfig(
previousVersion,
@@ -101,7 +101,7 @@ func runUpgradeTestCaseWithExistingConfig(
previousVersion string,
previousReleaseBinDir string,
usingExistingConfig bool,
setupUpgradeVerifyCase func(*testing.T, int, cri.RuntimeService, cri.ImageManagerService) ([]upgradeVerifyCaseFunc, beforeUpgradeHookFunc),
setupUpgradeVerifyCase func(*testing.T, int, *remote.RuntimeService, *remote.ImageService) ([]upgradeVerifyCaseFunc, beforeUpgradeHookFunc),
) func(t *testing.T) {
return func(t *testing.T) {
// NOTE: Using t.TempDir() here is to ensure there are no leaky
@@ -194,7 +194,7 @@ func runUpgradeTestCaseWithExistingConfig(
// The shim manager auto-downgrades the version to 2, but does not update bootstrap.json.
// 3. Restart the containerd process; the new container should be recovered successfully.
func shouldAdjustShimVersionDuringRestarting(t *testing.T, _ int,
rSvc cri.RuntimeService, iSvc cri.ImageManagerService) ([]upgradeVerifyCaseFunc, beforeUpgradeHookFunc) {
rSvc *remote.RuntimeService, iSvc *remote.ImageService) ([]upgradeVerifyCaseFunc, beforeUpgradeHookFunc) {
var busyboxImage = images.Get(images.BusyBox)
@@ -208,14 +208,14 @@ func shouldAdjustShimVersionDuringRestarting(t *testing.T, _ int,
var cntr2 string
createNewContainerInPodFunc := func(t *testing.T, rSvc cri.RuntimeService, _ cri.ImageManagerService) {
createNewContainerInPodFunc := func(t *testing.T, rSvc *remote.RuntimeService, _ *remote.ImageService) {
t.Log("Creating new container in the previous pod")
cntr2 = podCtx.createContainer("new-container", busyboxImage,
criruntime.ContainerState_CONTAINER_RUNNING,
WithCommand("sleep", "1d"))
}
shouldBeRunningFunc := func(t *testing.T, rSvc cri.RuntimeService, _ cri.ImageManagerService) {
shouldBeRunningFunc := func(t *testing.T, rSvc *remote.RuntimeService, _ *remote.ImageService) {
t.Log("Checking the running container in the previous pod")
pods, err := rSvc.ListPodSandbox(nil)
@@ -246,7 +246,7 @@ func shouldAdjustShimVersionDuringRestarting(t *testing.T, _ int,
}
func shouldRecoverAllThePodsAfterUpgrade(t *testing.T, taskVersion int,
rSvc cri.RuntimeService, iSvc cri.ImageManagerService) ([]upgradeVerifyCaseFunc, beforeUpgradeHookFunc) {
rSvc *remote.RuntimeService, iSvc *remote.ImageService) ([]upgradeVerifyCaseFunc, beforeUpgradeHookFunc) {
var busyboxImage = images.Get(images.BusyBox)
@@ -282,7 +282,7 @@ func shouldRecoverAllThePodsAfterUpgrade(t *testing.T, taskVersion int,
syscall.Kill(thirdPodShimPid, syscall.SIGKILL)
}
verifyFunc := func(t *testing.T, rSvc cri.RuntimeService, _ cri.ImageManagerService) {
verifyFunc := func(t *testing.T, rSvc *remote.RuntimeService, _ *remote.ImageService) {
t.Log("List Pods")
pods, err := rSvc.ListPodSandbox(nil)
@@ -336,7 +336,7 @@ func shouldRecoverAllThePodsAfterUpgrade(t *testing.T, taskVersion int,
}
func execToExistingContainer(t *testing.T, _ int,
rSvc cri.RuntimeService, iSvc cri.ImageManagerService) ([]upgradeVerifyCaseFunc, beforeUpgradeHookFunc) {
rSvc *remote.RuntimeService, iSvc *remote.ImageService) ([]upgradeVerifyCaseFunc, beforeUpgradeHookFunc) {
var busyboxImage = images.Get(images.BusyBox)
@@ -355,7 +355,7 @@ func execToExistingContainer(t *testing.T, _ int,
// NOTE: Wait for containerd to flush data into log
time.Sleep(2 * time.Second)
verifyFunc := func(t *testing.T, rSvc cri.RuntimeService, _ cri.ImageManagerService) {
verifyFunc := func(t *testing.T, rSvc *remote.RuntimeService, _ *remote.ImageService) {
pods, err := rSvc.ListPodSandbox(nil)
require.NoError(t, err)
require.Len(t, pods, 1)
@@ -402,7 +402,7 @@ func getFileSize(t *testing.T, filePath string) int64 {
}
func shouldManipulateContainersInPodAfterUpgrade(runtimeHandler string) setupUpgradeVerifyCase {
return func(t *testing.T, taskVersion int, rSvc cri.RuntimeService, iSvc cri.ImageManagerService) ([]upgradeVerifyCaseFunc, beforeUpgradeHookFunc) {
return func(t *testing.T, taskVersion int, rSvc *remote.RuntimeService, iSvc *remote.ImageService) ([]upgradeVerifyCaseFunc, beforeUpgradeHookFunc) {
shimConns := []shimConn{}
var busyboxImage = images.Get(images.BusyBox)
@@ -426,7 +426,7 @@ func shouldManipulateContainersInPodAfterUpgrade(runtimeHandler string) setupUpg
criruntime.ContainerState_CONTAINER_EXITED,
WithCommand("sleep", "1d"))
verifyFunc := func(t *testing.T, rSvc cri.RuntimeService, _ cri.ImageManagerService) {
verifyFunc := func(t *testing.T, rSvc *remote.RuntimeService, _ *remote.ImageService) {
// TODO(fuweid): make svc re-connect to new socket
podCtx.rSvc = rSvc
@@ -526,12 +526,12 @@ func shouldManipulateContainersInPodAfterUpgrade(runtimeHandler string) setupUpg
}
func shouldRecoverExistingImages(t *testing.T, _ int,
_ cri.RuntimeService, iSvc cri.ImageManagerService) ([]upgradeVerifyCaseFunc, beforeUpgradeHookFunc) {
_ *remote.RuntimeService, iSvc *remote.ImageService) ([]upgradeVerifyCaseFunc, beforeUpgradeHookFunc) {
images := []string{images.Get(images.BusyBox), images.Get(images.Alpine)}
expectedRefs := pullImagesByCRI(t, iSvc, images...)
verifyFunc := func(t *testing.T, _ cri.RuntimeService, iSvc cri.ImageManagerService) {
verifyFunc := func(t *testing.T, _ *remote.RuntimeService, iSvc *remote.ImageService) {
t.Log("List all images")
res, err := iSvc.ListImages(nil)
require.NoError(t, err)
@@ -550,7 +550,7 @@ func shouldRecoverExistingImages(t *testing.T, _ int,
// shouldParseMetricDataCorrectly is to check new release containerd can parse
// metric data from existing shim created by previous release.
func shouldParseMetricDataCorrectly(t *testing.T, _ int,
rSvc cri.RuntimeService, iSvc cri.ImageManagerService) ([]upgradeVerifyCaseFunc, beforeUpgradeHookFunc) {
rSvc *remote.RuntimeService, iSvc *remote.ImageService) ([]upgradeVerifyCaseFunc, beforeUpgradeHookFunc) {
imageName := images.Get(images.BusyBox)
pullImagesByCRI(t, iSvc, imageName)
@@ -591,7 +591,7 @@ done
WithLogPath(cntrLogName),
)
verifyFunc := func(t *testing.T, rSvc cri.RuntimeService, _ cri.ImageManagerService) {
verifyFunc := func(t *testing.T, rSvc *remote.RuntimeService, _ *remote.ImageService) {
checkContainerState(t, rSvc, cntr, criruntime.ContainerState_CONTAINER_RUNNING)
logPath := filepath.Join(podLogDir, cntrLogName)
@@ -622,13 +622,13 @@ done
return []upgradeVerifyCaseFunc{verifyFunc}, nil
}
func newPodTCtx(t *testing.T, rSvc cri.RuntimeService,
func newPodTCtx(t *testing.T, rSvc *remote.RuntimeService,
name, ns string, opts ...PodSandboxOpts) *podTCtx {
return newPodTCtxWithRuntimeHandler(t, rSvc, name, ns, "", opts...)
}
func newPodTCtxWithRuntimeHandler(t *testing.T, rSvc cri.RuntimeService,
func newPodTCtxWithRuntimeHandler(t *testing.T, rSvc *remote.RuntimeService,
name, ns, runtimeHandler string, opts ...PodSandboxOpts) *podTCtx {
sbConfig := PodSandboxConfig(name, ns, opts...)
@@ -652,7 +652,7 @@ type podTCtx struct {
name string
ns string
cfg *criruntime.PodSandboxConfig
rSvc cri.RuntimeService
rSvc *remote.RuntimeService
}
// createContainer creates a container in that pod.
@@ -712,7 +712,7 @@ type shimConn struct {
}
// buildShimClientFromBundle builds a shim client from the bundle directory of the container.
func buildShimClientFromBundle(t *testing.T, rSvc cri.RuntimeService, cid string) shimConn {
func buildShimClientFromBundle(t *testing.T, rSvc *remote.RuntimeService, cid string) shimConn {
cfg := criRuntimeInfo(t, rSvc)
bundleDir := filepath.Join(
@@ -793,7 +793,7 @@ func (pCtx *podTCtx) stop(remove bool) {
}
// criRuntimeInfo dumps CRI config.
func criRuntimeInfo(t *testing.T, svc cri.RuntimeService) map[string]any {
func criRuntimeInfo(t *testing.T, svc *remote.RuntimeService) map[string]any {
resp, err := svc.Status()
require.NoError(t, err)
@@ -805,7 +805,7 @@ func criRuntimeInfo(t *testing.T, svc cri.RuntimeService) map[string]any {
}
// checkContainerState checks container's state.
func checkContainerState(t *testing.T, svc cri.RuntimeService, name string, expected criruntime.ContainerState) {
func checkContainerState(t *testing.T, svc *remote.RuntimeService, name string, expected criruntime.ContainerState) {
t.Logf("Checking container %s state", name)
status, err := svc.ContainerStatus(name)
require.NoError(t, err)
@@ -813,7 +813,7 @@ func checkContainerState(t *testing.T, svc cri.RuntimeService, name string, expe
}
// pullImagesByCRI pulls images by CRI.
func pullImagesByCRI(t *testing.T, svc cri.ImageManagerService, images ...string) []string {
func pullImagesByCRI(t *testing.T, svc *remote.ImageService, images ...string) []string {
expectedRefs := make([]string, 0, len(images))
for _, image := range images {
@@ -825,8 +825,8 @@ func pullImagesByCRI(t *testing.T, svc cri.ImageManagerService, images ...string
return expectedRefs
}
// cleanupPods deletes all the pods based on the cri.RuntimeService connection.
func cleanupPods(t *testing.T, criRuntimeService cri.RuntimeService) {
// cleanupPods deletes all the pods based on the CRI runtime connection.
func cleanupPods(t *testing.T, criRuntimeService *remote.RuntimeService) {
pods, err := criRuntimeService.ListPodSandbox(nil)
require.NoError(t, err)
@@ -889,17 +889,23 @@ version = 2
require.NoError(t, err, "failed to create config for previous release")
}
// criRuntimeService returns cri.RuntimeService based on the grpc address.
func (p *ctrdProc) criRuntimeService(t *testing.T) cri.RuntimeService {
// criRuntimeService returns a CRI runtime client based on the grpc address.
func (p *ctrdProc) criRuntimeService(t *testing.T) *remote.RuntimeService {
service, err := remote.NewRuntimeService(p.grpcAddress(), 1*time.Minute)
require.NoError(t, err)
t.Cleanup(func() {
assert.NoError(t, service.Close(context.Background()))
})
return service
}
// criImageService returns cri.ImageManagerService based on the grpc address.
func (p *ctrdProc) criImageService(t *testing.T) cri.ImageManagerService {
// criImageService returns a CRI image client based on the grpc address.
func (p *ctrdProc) criImageService(t *testing.T) *remote.ImageService {
service, err := remote.NewImageService(p.grpcAddress(), 1*time.Minute)
require.NoError(t, err)
t.Cleanup(func() {
assert.NoError(t, service.Close(context.Background()))
})
return service
}
@@ -1007,7 +1013,7 @@ func (p *ctrdProc) logPath() string {
// isReady checks the containerd is ready or not.
func (p *ctrdProc) isReady() error {
var (
service cri.RuntimeService
service *remote.RuntimeService
err error
ticker = time.NewTicker(1 * time.Second)
ctx, cancel = context.WithTimeout(context.Background(), 2*time.Minute)
@@ -1026,9 +1032,10 @@ func (p *ctrdProc) isReady() error {
}
if _, err = service.Status(); err != nil {
err = errors.Join(err, service.Close(context.Background()))
continue
}
return nil
return service.Close(context.Background())
case <-ctx.Done():
return fmt.Errorf("context deadline exceeded: %w", err)
}
+2 -2
View File
@@ -30,6 +30,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// Package remote contains gRPC implementation of internalapi.RuntimeService
// and internalapi.ImageManagerService.
// Package remote contains legacy-style CRI client adapters for the integration
// tests and stress tooling.
package remote
+37 -103
View File
@@ -33,140 +33,74 @@ limitations under the License.
package remote
import (
"errors"
"fmt"
"context"
"maps"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
internalapi "github.com/containerd/containerd/v2/integration/cri-api/pkg/apis"
"github.com/containerd/log"
upstreamapi "k8s.io/cri-api/pkg/apis"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
"github.com/containerd/containerd/v2/integration/remote/util"
upstreamcri "k8s.io/cri-client/pkg"
)
// ImageService is a gRPC implementation of internalapi.ImageManagerService.
// ImageService adapts the upstream CRI image client to the legacy integration
// interface used by containerd's integration tests.
type ImageService struct {
timeout time.Duration
imageClient runtimeapi.ImageServiceClient
imageService upstreamapi.ImageManagerService
}
// NewImageService creates a new internalapi.ImageManagerService.
func NewImageService(endpoint string, connectionTimeout time.Duration) (internalapi.ImageManagerService, error) {
log.L.Infof("Connecting to image service %s", endpoint)
addr, dialer, err := util.GetAddressAndDialer(endpoint)
// NewImageService creates a legacy-style CRI image client backed by the
// upstream Kubernetes CRI client.
func NewImageService(endpoint string, connectionTimeout time.Duration) (*ImageService, error) {
imageService, err := upstreamcri.NewRemoteImageService(context.Background(), endpoint, connectionTimeout, nil, false)
if err != nil {
return nil, err
}
conn, err := grpc.NewClient(addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithContextDialer(dialer),
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxMsgSize)),
)
if err != nil {
log.L.WithError(err).Errorf("Connect remote image service %s failed", addr)
return nil, err
}
return &ImageService{
timeout: connectionTimeout,
imageClient: runtimeapi.NewImageServiceClient(conn),
}, nil
return &ImageService{imageService: imageService}, nil
}
// ListImages lists available images.
func (r *ImageService) ListImages(filter *runtimeapi.ImageFilter, opts ...grpc.CallOption) ([]*runtimeapi.Image, error) {
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
resp, err := r.imageClient.ListImages(ctx, &runtimeapi.ListImagesRequest{
Filter: filter,
}, opts...)
if err != nil {
log.L.WithError(err).Errorf("ListImages with filter %+v from image service failed", filter)
return nil, err
func (r *ImageService) Close(ctx context.Context) error {
if r == nil {
return nil
}
return resp.Images, nil
return r.imageService.Close(ctx)
}
// ImageStatus returns the status of the image.
func (r *ImageService) ImageStatus(image *runtimeapi.ImageSpec, opts ...grpc.CallOption) (*runtimeapi.Image, error) {
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
func (r *ImageService) ListImages(filter *runtimeapi.ImageFilter, _ ...grpc.CallOption) ([]*runtimeapi.Image, error) {
return r.imageService.ListImages(context.Background(), filter)
}
resp, err := r.imageClient.ImageStatus(ctx, &runtimeapi.ImageStatusRequest{
Image: image,
}, opts...)
func (r *ImageService) ImageStatus(image *runtimeapi.ImageSpec, _ ...grpc.CallOption) (*runtimeapi.Image, error) {
resp, err := r.imageService.ImageStatus(context.Background(), image, false)
if err != nil {
log.L.WithError(err).Errorf("ImageStatus %q from image service failed", image.Image)
return nil, err
}
return resp.GetImage(), nil
}
if resp.Image != nil {
if resp.Image.Id == "" || resp.Image.Size == 0 {
errorMessage := fmt.Sprintf("Id or size of image %q is not set", image.Image)
log.L.Errorf("ImageStatus failed: %s", errorMessage)
return nil, errors.New(errorMessage)
func (r *ImageService) PullImage(image *runtimeapi.ImageSpec, auth *runtimeapi.AuthConfig, podSandboxConfig *runtimeapi.PodSandboxConfig, runtimeHandler string, _ ...grpc.CallOption) (string, error) {
requestImage := image
if image != nil && runtimeHandler != "" {
requestImage = &runtimeapi.ImageSpec{
Image: image.Image,
Annotations: maps.Clone(image.Annotations),
UserSpecifiedImage: image.UserSpecifiedImage,
RuntimeHandler: runtimeHandler,
ImageRef: image.ImageRef,
}
}
return resp.Image, nil
return r.imageService.PullImage(context.Background(), requestImage, auth, podSandboxConfig)
}
// PullImage pulls an image with authentication config.
func (r *ImageService) PullImage(image *runtimeapi.ImageSpec, auth *runtimeapi.AuthConfig, podSandboxConfig *runtimeapi.PodSandboxConfig, runtimeHandler string, opts ...grpc.CallOption) (string, error) {
ctx, cancel := getContextWithCancel()
defer cancel()
resp, err := r.imageClient.PullImage(ctx, &runtimeapi.PullImageRequest{
Image: image,
Auth: auth,
SandboxConfig: podSandboxConfig,
}, opts...)
if err != nil {
log.L.WithError(err).Errorf("PullImage %q from image service failed", image.Image)
return "", err
}
if resp.ImageRef == "" {
errorMessage := fmt.Sprintf("imageRef of image %q is not set", image.Image)
log.L.Errorf("PullImage failed: %s", errorMessage)
return "", errors.New(errorMessage)
}
return resp.ImageRef, nil
func (r *ImageService) RemoveImage(image *runtimeapi.ImageSpec, _ ...grpc.CallOption) error {
return r.imageService.RemoveImage(context.Background(), image)
}
// RemoveImage removes the image.
func (r *ImageService) RemoveImage(image *runtimeapi.ImageSpec, opts ...grpc.CallOption) error {
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
_, err := r.imageClient.RemoveImage(ctx, &runtimeapi.RemoveImageRequest{
Image: image,
}, opts...)
func (r *ImageService) ImageFsInfo(_ ...grpc.CallOption) ([]*runtimeapi.FilesystemUsage, error) {
resp, err := r.imageService.ImageFsInfo(context.Background())
if err != nil {
log.L.WithError(err).Errorf("RemoveImage %q from image service failed", image.Image)
return err
}
return nil
}
// ImageFsInfo returns information of the filesystem that is used to store images.
func (r *ImageService) ImageFsInfo(opts ...grpc.CallOption) ([]*runtimeapi.FilesystemUsage, error) {
// Do not set timeout, because `ImageFsInfo` takes time.
// TODO(random-liu): Should we assume runtime should cache the result, and set timeout here?
ctx, cancel := getContextWithCancel()
defer cancel()
resp, err := r.imageClient.ImageFsInfo(ctx, &runtimeapi.ImageFsInfoRequest{}, opts...)
if err != nil {
log.L.WithError(err).Error("ImageFsInfo from image service failed")
return nil, err
}
return resp.GetImageFilesystems(), nil
+123 -522
View File
@@ -35,602 +35,203 @@ package remote
import (
"context"
"errors"
"fmt"
"strings"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
upstreamapi "k8s.io/cri-api/pkg/apis"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
internalapi "github.com/containerd/containerd/v2/integration/cri-api/pkg/apis"
"github.com/containerd/containerd/v2/integration/remote/logreduction"
"github.com/containerd/containerd/v2/integration/remote/util"
executil "github.com/containerd/containerd/v2/internal/cri/executil"
"github.com/containerd/log"
upstreamcri "k8s.io/cri-client/pkg"
criutil "k8s.io/cri-client/pkg/util"
)
// RuntimeService is a gRPC implementation of internalapi.RuntimeService.
const maxMsgSize = 1024 * 1024 * 16
// RuntimeService adapts the upstream CRI client to the legacy integration
// interface used by containerd's integration tests.
type RuntimeService struct {
timeout time.Duration
runtimeClient runtimeapi.RuntimeServiceClient
// Cache last per-container error message to reduce log spam
logReduction *logreduction.LogReduction
runtimeService upstreamapi.RuntimeService
runtimeClient runtimeapi.RuntimeServiceClient
runtimeConn *grpc.ClientConn
}
const (
// How frequently to report identical errors
identicalErrorDelay = 1 * time.Minute
)
// NewRuntimeService creates a new internalapi.RuntimeService.
func NewRuntimeService(endpoint string, connectionTimeout time.Duration) (internalapi.RuntimeService, error) {
log.L.Infof("Connecting to runtime service %s", endpoint)
addr, dialer, err := util.GetAddressAndDialer(endpoint)
// NewRuntimeService creates a legacy-style CRI runtime client backed by the
// upstream Kubernetes CRI client.
func NewRuntimeService(endpoint string, connectionTimeout time.Duration) (*RuntimeService, error) {
runtimeService, err := upstreamcri.NewRemoteRuntimeService(context.Background(), endpoint, connectionTimeout, nil, false)
if err != nil {
return nil, err
}
conn, err := grpc.NewClient(addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithContextDialer(dialer),
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxMsgSize)),
)
runtimeConn, err := newRuntimeClientConn(endpoint)
if err != nil {
log.L.WithError(err).Errorf("Connect remote runtime %s failed", addr)
_ = runtimeService.Close(context.Background())
return nil, err
}
return &RuntimeService{
timeout: connectionTimeout,
runtimeClient: runtimeapi.NewRuntimeServiceClient(conn),
logReduction: logreduction.NewLogReduction(identicalErrorDelay),
runtimeService: runtimeService,
runtimeClient: runtimeapi.NewRuntimeServiceClient(runtimeConn),
runtimeConn: runtimeConn,
}, nil
}
// Version returns the runtime name, runtime version and runtime API version.
func (r *RuntimeService) Version(apiVersion string, opts ...grpc.CallOption) (*runtimeapi.VersionResponse, error) {
log.L.Infof("[RuntimeService] Version (apiVersion=%v, timeout=%v)", apiVersion, r.timeout)
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
typedVersion, err := r.runtimeClient.Version(ctx, &runtimeapi.VersionRequest{
Version: apiVersion,
}, opts...)
func newRuntimeClientConn(endpoint string) (*grpc.ClientConn, error) {
addr, dialer, err := criutil.GetAddressAndDialer(endpoint)
if err != nil {
log.L.WithError(err).Errorf("Version from runtime service failed")
return nil, err
}
log.L.Infof("[RuntimeService] Version Response (typedVersion=%v)", typedVersion)
if typedVersion.Version == "" || typedVersion.RuntimeName == "" || typedVersion.RuntimeApiVersion == "" || typedVersion.RuntimeVersion == "" {
return nil, fmt.Errorf("not all fields are set in VersionResponse (%q)", typedVersion)
}
return typedVersion, err
return grpc.NewClient(clientTargetForAddress(addr),
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithAuthority("localhost"),
grpc.WithContextDialer(dialer),
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxMsgSize)),
)
}
// RunPodSandbox creates and starts a pod-level sandbox. Runtimes should ensure
// the sandbox is in ready state.
func (r *RuntimeService) RunPodSandbox(config *runtimeapi.PodSandboxConfig, runtimeHandler string, opts ...grpc.CallOption) (string, error) {
// Use 2 times longer timeout for sandbox operation (4 mins by default)
// TODO: Make the pod sandbox timeout configurable.
timeout := r.timeout * 2
log.L.Infof("[RuntimeService] RunPodSandbox (config=%v, runtimeHandler=%v, timeout=%v)", config, runtimeHandler, timeout)
ctx, cancel := getContextWithTimeout(timeout)
defer cancel()
resp, err := r.runtimeClient.RunPodSandbox(ctx, &runtimeapi.RunPodSandboxRequest{
Config: config,
RuntimeHandler: runtimeHandler,
}, opts...)
if err != nil {
log.L.WithError(err).Errorf("RunPodSandbox from runtime service failed")
return "", err
func clientTargetForAddress(addr string) string {
// grpc.NewClient defaults to the DNS resolver. Use the passthrough resolver
// for socket paths so the custom dialer receives the raw endpoint string.
if strings.HasPrefix(addr, "/") {
return "passthrough:///" + addr
}
if resp.PodSandboxId == "" {
errorMessage := fmt.Sprintf("PodSandboxId is not set for sandbox %q", config.GetMetadata())
log.L.Errorf("RunPodSandbox failed: %s", errorMessage)
return "", errors.New(errorMessage)
}
log.L.Infof("[RuntimeService] RunPodSandbox Response (PodSandboxId=%v)", resp.PodSandboxId)
return resp.PodSandboxId, nil
return addr
}
// UpdatePodSandboxResources updates the cgroup resources for the sandbox.
func (r *RuntimeService) UpdatePodSandboxResources(podSandBoxID string, overhead *runtimeapi.LinuxContainerResources, resources *runtimeapi.LinuxContainerResources, opts ...grpc.CallOption) error {
log.L.Infof("[RuntimeService] UpdatePodSandboxResources (podSandboxID=%v, timeout=%v)", podSandBoxID, r.timeout)
func (r *RuntimeService) Close(ctx context.Context) error {
if r == nil {
return nil
}
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
var err error
if r.runtimeService != nil {
err = r.runtimeService.Close(ctx)
}
if r.runtimeConn != nil {
err = errors.Join(err, r.runtimeConn.Close())
}
return err
}
_, err := r.runtimeClient.UpdatePodSandboxResources(ctx, &runtimeapi.UpdatePodSandboxResourcesRequest{
PodSandboxId: podSandBoxID,
func (r *RuntimeService) Version(apiVersion string, _ ...grpc.CallOption) (*runtimeapi.VersionResponse, error) {
return r.runtimeService.Version(context.Background(), apiVersion)
}
func (r *RuntimeService) RunPodSandbox(config *runtimeapi.PodSandboxConfig, runtimeHandler string, _ ...grpc.CallOption) (string, error) {
return r.runtimeService.RunPodSandbox(context.Background(), config, runtimeHandler)
}
func (r *RuntimeService) UpdatePodSandboxResources(podSandboxID string, overhead *runtimeapi.LinuxContainerResources, resources *runtimeapi.LinuxContainerResources, _ ...grpc.CallOption) error {
_, err := r.runtimeService.UpdatePodSandboxResources(context.Background(), &runtimeapi.UpdatePodSandboxResourcesRequest{
PodSandboxId: podSandboxID,
Overhead: overhead,
Resources: resources,
}, opts...)
if err != nil {
log.L.WithError(err).Errorf("UpdatePodSandboxResources %q from runtime service failed", podSandBoxID)
return err
}
log.L.Infof("[RuntimeService] UpdatePodSandboxResources Response (podSandboxID=%v)", podSandBoxID)
return nil
})
return err
}
// StopPodSandbox stops the sandbox. If there are any running containers in the
// sandbox, they should be forced to termination.
func (r *RuntimeService) StopPodSandbox(podSandBoxID string, opts ...grpc.CallOption) error {
log.L.Infof("[RuntimeService] StopPodSandbox (podSandboxID=%v, timeout=%v)", podSandBoxID, r.timeout)
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
_, err := r.runtimeClient.StopPodSandbox(ctx, &runtimeapi.StopPodSandboxRequest{
PodSandboxId: podSandBoxID,
}, opts...)
if err != nil {
log.L.WithError(err).Errorf("StopPodSandbox %q from runtime service failed", podSandBoxID)
return err
}
log.L.Infof("[RuntimeService] StopPodSandbox Response (podSandboxID=%v)", podSandBoxID)
return nil
func (r *RuntimeService) StopPodSandbox(podSandboxID string, _ ...grpc.CallOption) error {
return r.runtimeService.StopPodSandbox(context.Background(), podSandboxID)
}
// RemovePodSandbox removes the sandbox. If there are any containers in the
// sandbox, they should be forcibly removed.
func (r *RuntimeService) RemovePodSandbox(podSandBoxID string, opts ...grpc.CallOption) error {
log.L.Infof("[RuntimeService] RemovePodSandbox (podSandboxID=%v, timeout=%v)", podSandBoxID, r.timeout)
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
_, err := r.runtimeClient.RemovePodSandbox(ctx, &runtimeapi.RemovePodSandboxRequest{
PodSandboxId: podSandBoxID,
}, opts...)
if err != nil {
log.L.WithError(err).Errorf("RemovePodSandbox %q from runtime service failed", podSandBoxID)
return err
}
log.L.Infof("[RuntimeService] RemovePodSandbox Response (podSandboxID=%v)", podSandBoxID)
return nil
func (r *RuntimeService) RemovePodSandbox(podSandboxID string, _ ...grpc.CallOption) error {
return r.runtimeService.RemovePodSandbox(context.Background(), podSandboxID)
}
// PodSandboxStatus returns the status of the PodSandbox.
func (r *RuntimeService) PodSandboxStatus(podSandBoxID string, opts ...grpc.CallOption) (*runtimeapi.PodSandboxStatus, error) {
log.L.Infof("[RuntimeService] PodSandboxStatus (podSandboxID=%v, timeout=%v)", podSandBoxID, r.timeout)
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
resp, err := r.runtimeClient.PodSandboxStatus(ctx, &runtimeapi.PodSandboxStatusRequest{
PodSandboxId: podSandBoxID,
}, opts...)
func (r *RuntimeService) PodSandboxStatus(podSandboxID string, _ ...grpc.CallOption) (*runtimeapi.PodSandboxStatus, error) {
resp, err := r.runtimeService.PodSandboxStatus(context.Background(), podSandboxID, false)
if err != nil {
return nil, err
}
log.L.Infof("[RuntimeService] PodSandboxStatus Response (podSandboxID=%v, status=%v)", podSandBoxID, resp.Status)
if resp.Status != nil {
if err := verifySandboxStatus(resp.Status); err != nil {
return nil, err
}
}
return resp.Status, nil
return resp.GetStatus(), nil
}
// ListPodSandbox returns a list of PodSandboxes.
func (r *RuntimeService) ListPodSandbox(filter *runtimeapi.PodSandboxFilter, opts ...grpc.CallOption) ([]*runtimeapi.PodSandbox, error) {
log.L.Infof("[RuntimeService] ListPodSandbox (filter=%v, timeout=%v)", filter, r.timeout)
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
func (r *RuntimeService) ListPodSandbox(filter *runtimeapi.PodSandboxFilter, _ ...grpc.CallOption) ([]*runtimeapi.PodSandbox, error) {
return r.runtimeService.ListPodSandbox(context.Background(), filter)
}
resp, err := r.runtimeClient.ListPodSandbox(ctx, &runtimeapi.ListPodSandboxRequest{
Filter: filter,
}, opts...)
func (r *RuntimeService) CreateContainer(podSandboxID string, config *runtimeapi.ContainerConfig, sandboxConfig *runtimeapi.PodSandboxConfig, _ ...grpc.CallOption) (string, error) {
return r.runtimeService.CreateContainer(context.Background(), podSandboxID, config, sandboxConfig)
}
func (r *RuntimeService) StartContainer(containerID string, _ ...grpc.CallOption) error {
return r.runtimeService.StartContainer(context.Background(), containerID)
}
func (r *RuntimeService) StopContainer(containerID string, timeout int64, _ ...grpc.CallOption) error {
return r.runtimeService.StopContainer(context.Background(), containerID, timeout)
}
func (r *RuntimeService) RemoveContainer(containerID string, _ ...grpc.CallOption) error {
return r.runtimeService.RemoveContainer(context.Background(), containerID)
}
func (r *RuntimeService) ListContainers(filter *runtimeapi.ContainerFilter, _ ...grpc.CallOption) ([]*runtimeapi.Container, error) {
return r.runtimeService.ListContainers(context.Background(), filter)
}
func (r *RuntimeService) ContainerStatus(containerID string, _ ...grpc.CallOption) (*runtimeapi.ContainerStatus, error) {
resp, err := r.runtimeService.ContainerStatus(context.Background(), containerID, false)
if err != nil {
log.L.WithError(err).Errorf("ListPodSandbox with filter %+v from runtime service failed", filter)
return nil, err
}
log.L.Infof("[RuntimeService] ListPodSandbox Response (filter=%v, items=%v)", filter, resp.Items)
return resp.Items, nil
return resp.GetStatus(), nil
}
// CreateContainer creates a new container in the specified PodSandbox.
func (r *RuntimeService) CreateContainer(podSandBoxID string, config *runtimeapi.ContainerConfig, sandboxConfig *runtimeapi.PodSandboxConfig, opts ...grpc.CallOption) (string, error) {
log.L.Infof("[RuntimeService] CreateContainer (podSandBoxID=%v, timeout=%v)", podSandBoxID, r.timeout)
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
resp, err := r.runtimeClient.CreateContainer(ctx, &runtimeapi.CreateContainerRequest{
PodSandboxId: podSandBoxID,
Config: config,
SandboxConfig: sandboxConfig,
}, opts...)
if err != nil {
log.L.WithError(err).Errorf("CreateContainer in sandbox %q from runtime service failed", podSandBoxID)
return "", err
}
log.L.Infof("[RuntimeService] CreateContainer (podSandBoxID=%v, ContainerId=%v)", podSandBoxID, resp.ContainerId)
if resp.ContainerId == "" {
errorMessage := fmt.Sprintf("ContainerId is not set for container %q", config.GetMetadata())
log.L.Errorf("CreateContainer failed: %s", errorMessage)
return "", errors.New(errorMessage)
}
return resp.ContainerId, nil
func (r *RuntimeService) UpdateContainerResources(containerID string, resources *runtimeapi.LinuxContainerResources, windowsResources *runtimeapi.WindowsContainerResources, _ ...grpc.CallOption) error {
return r.runtimeService.UpdateContainerResources(context.Background(), containerID, &runtimeapi.ContainerResources{
Linux: resources,
Windows: windowsResources,
})
}
// StartContainer starts the container.
func (r *RuntimeService) StartContainer(containerID string, opts ...grpc.CallOption) error {
log.L.Infof("[RuntimeService] StartContainer (containerID=%v, timeout=%v)", containerID, r.timeout)
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
_, err := r.runtimeClient.StartContainer(ctx, &runtimeapi.StartContainerRequest{
ContainerId: containerID,
}, opts...)
if err != nil {
log.L.WithError(err).Errorf("StartContainer %q from runtime service failed", containerID)
return err
}
log.L.Infof("[RuntimeService] StartContainer Response (containerID=%v)", containerID)
return nil
func (r *RuntimeService) ExecSync(containerID string, cmd []string, timeout time.Duration, _ ...grpc.CallOption) ([]byte, []byte, error) {
return r.runtimeService.ExecSync(context.Background(), containerID, cmd, timeout)
}
// StopContainer stops a running container with a grace period (i.e., timeout).
func (r *RuntimeService) StopContainer(containerID string, timeout int64, opts ...grpc.CallOption) error {
log.L.Infof("[RuntimeService] StopContainer (containerID=%v, timeout=%v)", containerID, timeout)
// Use timeout + default timeout (2 minutes) as timeout to leave extra time
// for SIGKILL container and request latency.
t := r.timeout + time.Duration(timeout)*time.Second
ctx, cancel := getContextWithTimeout(t)
defer cancel()
r.logReduction.ClearID(containerID)
_, err := r.runtimeClient.StopContainer(ctx, &runtimeapi.StopContainerRequest{
ContainerId: containerID,
Timeout: timeout,
}, opts...)
if err != nil {
log.L.WithError(err).Errorf("StopContainer %q from runtime service failed", containerID)
return err
}
log.L.Infof("[RuntimeService] StopContainer Response (containerID=%v)", containerID)
return nil
func (r *RuntimeService) Exec(req *runtimeapi.ExecRequest, _ ...grpc.CallOption) (*runtimeapi.ExecResponse, error) {
return r.runtimeService.Exec(context.Background(), req)
}
// RemoveContainer removes the container. If the container is running, the container
// should be forced to removal.
func (r *RuntimeService) RemoveContainer(containerID string, opts ...grpc.CallOption) error {
log.L.Infof("[RuntimeService] RemoveContainer (containerID=%v, timeout=%v)", containerID, r.timeout)
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
r.logReduction.ClearID(containerID)
_, err := r.runtimeClient.RemoveContainer(ctx, &runtimeapi.RemoveContainerRequest{
ContainerId: containerID,
}, opts...)
if err != nil {
log.L.WithError(err).Errorf("RemoveContainer %q from runtime service failed", containerID)
return err
}
log.L.Infof("[RuntimeService] RemoveContainer Response (containerID=%v)", containerID)
return nil
func (r *RuntimeService) Attach(req *runtimeapi.AttachRequest, _ ...grpc.CallOption) (*runtimeapi.AttachResponse, error) {
return r.runtimeService.Attach(context.Background(), req)
}
// ListContainers lists containers by filters.
func (r *RuntimeService) ListContainers(filter *runtimeapi.ContainerFilter, opts ...grpc.CallOption) ([]*runtimeapi.Container, error) {
log.L.Infof("[RuntimeService] ListContainers (filter=%v, timeout=%v)", filter, r.timeout)
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
resp, err := r.runtimeClient.ListContainers(ctx, &runtimeapi.ListContainersRequest{
Filter: filter,
}, opts...)
if err != nil {
log.L.WithError(err).Errorf("ListContainers with filter %+v from runtime service failed", filter)
return nil, err
}
log.L.Infof("[RuntimeService] ListContainers Response (filter=%v, containers=%v)", filter, resp.Containers)
return resp.Containers, nil
func (r *RuntimeService) PortForward(req *runtimeapi.PortForwardRequest, _ ...grpc.CallOption) (*runtimeapi.PortForwardResponse, error) {
return r.runtimeService.PortForward(context.Background(), req)
}
// ContainerStatus returns the container status.
func (r *RuntimeService) ContainerStatus(containerID string, opts ...grpc.CallOption) (*runtimeapi.ContainerStatus, error) {
log.L.Infof("[RuntimeService] ContainerStatus (containerID=%v, timeout=%v)", containerID, r.timeout)
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
resp, err := r.runtimeClient.ContainerStatus(ctx, &runtimeapi.ContainerStatusRequest{
ContainerId: containerID,
}, opts...)
if err != nil {
// Don't spam the log with endless messages about the same failure.
if r.logReduction.ShouldMessageBePrinted(err.Error(), containerID) {
log.L.WithError(err).Errorf("ContainerStatus %q from runtime service failed", containerID)
}
return nil, err
}
r.logReduction.ClearID(containerID)
log.L.Infof("[RuntimeService] ContainerStatus Response (containerID=%v, status=%v)", containerID, resp.Status)
if resp.Status != nil {
if err := verifyContainerStatus(resp.Status); err != nil {
log.L.WithError(err).Errorf("ContainerStatus of %q failed", containerID)
return nil, err
}
}
return resp.Status, nil
func (r *RuntimeService) UpdateRuntimeConfig(runtimeConfig *runtimeapi.RuntimeConfig, _ ...grpc.CallOption) error {
return r.runtimeService.UpdateRuntimeConfig(context.Background(), runtimeConfig)
}
// UpdateContainerResources updates a containers resource config
func (r *RuntimeService) UpdateContainerResources(containerID string, resources *runtimeapi.LinuxContainerResources, windowsResources *runtimeapi.WindowsContainerResources, opts ...grpc.CallOption) error {
log.L.Infof("[RuntimeService] UpdateContainerResources (containerID=%v, timeout=%v)", containerID, r.timeout)
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
_, err := r.runtimeClient.UpdateContainerResources(ctx, &runtimeapi.UpdateContainerResourcesRequest{
ContainerId: containerID,
Linux: resources,
Windows: windowsResources,
}, opts...)
if err != nil {
log.L.WithError(err).Errorf("UpdateContainerResources %q from runtime service failed", containerID)
return err
}
log.L.Infof("[RuntimeService] UpdateContainerResources Response (containerID=%v)", containerID)
return nil
func (r *RuntimeService) Status(_ ...grpc.CallOption) (*runtimeapi.StatusResponse, error) {
return r.runtimeService.Status(context.Background(), true)
}
// ExecSync executes a command in the container, and returns the stdout output.
// If command exits with a non-zero exit code, an error is returned.
func (r *RuntimeService) ExecSync(containerID string, cmd []string, timeout time.Duration, opts ...grpc.CallOption) (stdout []byte, stderr []byte, err error) {
log.L.Infof("[RuntimeService] ExecSync (containerID=%v, timeout=%v)", containerID, timeout)
// Do not set timeout when timeout is 0.
var ctx context.Context
var cancel context.CancelFunc
if timeout != 0 {
// Use timeout + default timeout (2 minutes) as timeout to leave some time for
// the runtime to do cleanup.
ctx, cancel = getContextWithTimeout(r.timeout + timeout)
} else {
ctx, cancel = getContextWithCancel()
}
defer cancel()
timeoutSeconds := int64(timeout.Seconds())
req := &runtimeapi.ExecSyncRequest{
ContainerId: containerID,
Cmd: cmd,
Timeout: timeoutSeconds,
}
resp, err := r.runtimeClient.ExecSync(ctx, req, opts...)
if err != nil {
log.L.WithError(err).Errorf("ExecSync %s '%s' from runtime service failed", containerID, strings.Join(cmd, " "))
return nil, nil, err
}
log.L.Infof("[RuntimeService] ExecSync Response (containerID=%v, ExitCode=%v)", containerID, resp.ExitCode)
err = nil
if resp.ExitCode != 0 {
err = executil.CodeExitError{
Err: fmt.Errorf("command '%s' exited with %d: %s", strings.Join(cmd, " "), resp.ExitCode, resp.Stderr),
Code: int(resp.ExitCode),
}
}
return resp.Stdout, resp.Stderr, err
func (r *RuntimeService) RuntimeConfig(_ *runtimeapi.RuntimeConfigRequest, _ ...grpc.CallOption) (*runtimeapi.RuntimeConfigResponse, error) {
return r.runtimeService.RuntimeConfig(context.Background())
}
// Exec prepares a streaming endpoint to execute a command in the container, and returns the address.
func (r *RuntimeService) Exec(req *runtimeapi.ExecRequest, opts ...grpc.CallOption) (*runtimeapi.ExecResponse, error) {
log.L.Infof("[RuntimeService] Exec (timeout=%v)", r.timeout)
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
resp, err := r.runtimeClient.Exec(ctx, req, opts...)
if err != nil {
log.L.WithError(err).Errorf("Exec %s '%s' from runtime service failed", req.ContainerId, strings.Join(req.Cmd, " "))
return nil, err
}
log.L.Info("[RuntimeService] Exec Response")
if resp.Url == "" {
errorMessage := "URL is not set"
log.L.Errorf("Exec failed: %s", errorMessage)
return nil, errors.New(errorMessage)
}
return resp, nil
func (r *RuntimeService) ContainerStats(containerID string, _ ...grpc.CallOption) (*runtimeapi.ContainerStats, error) {
return r.runtimeService.ContainerStats(context.Background(), containerID)
}
// Attach prepares a streaming endpoint to attach to a running container, and returns the address.
func (r *RuntimeService) Attach(req *runtimeapi.AttachRequest, opts ...grpc.CallOption) (*runtimeapi.AttachResponse, error) {
log.L.Infof("[RuntimeService] Attach (containerId=%v, timeout=%v)", req.ContainerId, r.timeout)
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
resp, err := r.runtimeClient.Attach(ctx, req, opts...)
if err != nil {
log.L.WithError(err).Errorf("Attach %s from runtime service failed", req.ContainerId)
return nil, err
}
log.L.Infof("[RuntimeService] Attach Response (containerId=%v)", req.ContainerId)
if resp.Url == "" {
errorMessage := "URL is not set"
log.L.Errorf("Attach failed: %s", errorMessage)
return nil, errors.New(errorMessage)
}
return resp, nil
func (r *RuntimeService) ListContainerStats(filter *runtimeapi.ContainerStatsFilter, _ ...grpc.CallOption) ([]*runtimeapi.ContainerStats, error) {
return r.runtimeService.ListContainerStats(context.Background(), filter)
}
// PortForward prepares a streaming endpoint to forward ports from a PodSandbox, and returns the address.
func (r *RuntimeService) PortForward(req *runtimeapi.PortForwardRequest, opts ...grpc.CallOption) (*runtimeapi.PortForwardResponse, error) {
log.L.Infof("[RuntimeService] PortForward (podSandboxID=%v, port=%v, timeout=%v)", req.PodSandboxId, req.Port, r.timeout)
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
resp, err := r.runtimeClient.PortForward(ctx, req, opts...)
if err != nil {
log.L.WithError(err).Errorf("PortForward %s from runtime service failed", req.PodSandboxId)
return nil, err
}
log.L.Infof("[RuntimeService] PortForward Response (podSandboxID=%v)", req.PodSandboxId)
if resp.Url == "" {
errorMessage := "URL is not set"
log.L.Errorf("PortForward failed: %s", errorMessage)
return nil, errors.New(errorMessage)
}
return resp, nil
func (r *RuntimeService) ReopenContainerLog(containerID string, _ ...grpc.CallOption) error {
return r.runtimeService.ReopenContainerLog(context.Background(), containerID)
}
// UpdateRuntimeConfig updates the config of a runtime service. The only
// update payload currently supported is the pod CIDR assigned to a node,
// and the runtime service just proxies it down to the network plugin.
func (r *RuntimeService) UpdateRuntimeConfig(runtimeConfig *runtimeapi.RuntimeConfig, opts ...grpc.CallOption) error {
log.L.Infof("[RuntimeService] UpdateRuntimeConfig (runtimeConfig=%v, timeout=%v)", runtimeConfig, r.timeout)
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
// Response doesn't contain anything of interest. This translates to an
// Event notification to the network plugin, which can't fail, so we're
// really looking to surface destination unreachable.
_, err := r.runtimeClient.UpdateRuntimeConfig(ctx, &runtimeapi.UpdateRuntimeConfigRequest{
RuntimeConfig: runtimeConfig,
}, opts...)
if err != nil {
return err
}
log.L.Infof("[RuntimeService] UpdateRuntimeConfig Response (runtimeConfig=%v)", runtimeConfig)
return nil
}
// Status returns the status of the runtime.
func (r *RuntimeService) Status(opts ...grpc.CallOption) (*runtimeapi.StatusResponse, error) {
log.L.Infof("[RuntimeService] Status (timeout=%v)", r.timeout)
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
resp, err := r.runtimeClient.Status(ctx, &runtimeapi.StatusRequest{Verbose: true}, opts...)
if err != nil {
log.L.WithError(err).Errorf("Status from runtime service failed")
return nil, err
}
log.L.Infof("[RuntimeService] Status Response (status=%v)", resp.Status)
if resp.Status == nil || len(resp.Status.Conditions) < 2 {
errorMessage := "RuntimeReady or NetworkReady condition are not set"
log.L.Errorf("Status failed: %s", errorMessage)
return nil, errors.New(errorMessage)
}
return resp, nil
}
// RuntimeConfig returns the CgroupDriver of the runtime.
func (r *RuntimeService) RuntimeConfig(in *runtimeapi.RuntimeConfigRequest, opts ...grpc.CallOption) (*runtimeapi.RuntimeConfigResponse, error) {
log.L.Infof("[RuntimeService] RuntimeConfig (timeout=%v)", r.timeout)
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
runtimeConfig, err := r.runtimeClient.RuntimeConfig(ctx, in)
return runtimeConfig, err
}
// ContainerStats returns the stats of the container.
func (r *RuntimeService) ContainerStats(containerID string, opts ...grpc.CallOption) (*runtimeapi.ContainerStats, error) {
log.L.Infof("[RuntimeService] ContainerStats (containerID=%v, timeout=%v)", containerID, r.timeout)
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
resp, err := r.runtimeClient.ContainerStats(ctx, &runtimeapi.ContainerStatsRequest{
ContainerId: containerID,
}, opts...)
if err != nil {
if r.logReduction.ShouldMessageBePrinted(err.Error(), containerID) {
log.L.WithError(err).Errorf("ContainerStats %q from runtime service failed", containerID)
}
return nil, err
}
r.logReduction.ClearID(containerID)
log.L.Infof("[RuntimeService] ContainerStats Response (containerID=%v, stats=%v)", containerID, resp.GetStats())
return resp.GetStats(), nil
}
// ListContainerStats lists all container stats given the provided filter
func (r *RuntimeService) ListContainerStats(filter *runtimeapi.ContainerStatsFilter, opts ...grpc.CallOption) ([]*runtimeapi.ContainerStats, error) {
log.L.Infof("[RuntimeService] ListContainerStats (filter=%v)", filter)
// Do not set timeout, because writable layer stats collection takes time.
// TODO(random-liu): Should we assume runtime should cache the result, and set timeout here?
ctx, cancel := getContextWithCancel()
defer cancel()
resp, err := r.runtimeClient.ListContainerStats(ctx, &runtimeapi.ListContainerStatsRequest{
Filter: filter,
}, opts...)
if err != nil {
log.L.WithError(err).Errorf("ListContainerStats with filter %+v from runtime service failed", filter)
return nil, err
}
log.L.Infof("[RuntimeService] ListContainerStats Response (filter=%v, stats=%v)", filter, resp.GetStats())
return resp.GetStats(), nil
}
// ReopenContainerLog reopens the container log for the given container ID
func (r *RuntimeService) ReopenContainerLog(containerID string, opts ...grpc.CallOption) error {
log.L.Infof("[RuntimeService] ReopenContainerLog (containerID=%v, timeout=%v)", containerID, r.timeout)
ctx, cancel := getContextWithTimeout(r.timeout)
defer cancel()
_, err := r.runtimeClient.ReopenContainerLog(ctx, &runtimeapi.ReopenContainerLogRequest{
ContainerId: containerID,
}, opts...)
if err != nil {
log.L.WithError(err).Errorf("ReopenContainerLog %q from runtime service failed", containerID)
return err
}
log.L.Infof("[RuntimeService] ReopenContainerLog Response (containerID=%v)", containerID)
return nil
}
// GetContainerEvents returns a GRPC client to stream container events
// GetContainerEvents uses the extra raw grpc client connection kept alongside
// the upstream runtime service. The upstream RuntimeService interface exposes
// GetContainerEvents via a channel/callback helper, but containerd's
// integration tests still assert on the stream-shaped RPC directly and need
// multiple listeners on the same CRI endpoint.
func (r *RuntimeService) GetContainerEvents(ctx context.Context, request *runtimeapi.GetEventsRequest, opts ...grpc.CallOption) (runtimeapi.RuntimeService_GetContainerEventsClient, error) {
log.L.Infof("[RuntimeService] GetContainerEvents (timeout=%v)", r.timeout)
client, err := r.runtimeClient.GetContainerEvents(ctx, request, opts...)
if err != nil {
log.L.WithError(err).Errorf("GetContainerEvents from runtime service failed")
return nil, err
}
return client, nil
return r.runtimeClient.GetContainerEvents(ctx, request, opts...)
}
+84
View File
@@ -0,0 +1,84 @@
//go:build !windows
/*
Copyright The containerd 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 remote
import (
"context"
"fmt"
"io"
"net"
"os"
"testing"
"time"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
)
type fakeRuntimeService struct {
runtimeapi.UnimplementedRuntimeServiceServer
}
func (f *fakeRuntimeService) GetContainerEvents(_ *runtimeapi.GetEventsRequest, stream grpc.ServerStreamingServer[runtimeapi.ContainerEventResponse]) error {
if err := stream.Send(&runtimeapi.ContainerEventResponse{
ContainerEventType: runtimeapi.ContainerEventType_CONTAINER_CREATED_EVENT,
}); err != nil {
return err
}
return io.EOF
}
func TestNewRuntimeClientConnGetContainerEventsUnix(t *testing.T) {
socketPath := fmt.Sprintf("/tmp/containerd-cri-events-%d.sock", time.Now().UnixNano())
t.Cleanup(func() {
require.NoError(t, os.RemoveAll(socketPath))
})
listener, err := net.Listen("unix", socketPath)
require.NoError(t, err)
t.Cleanup(func() {
err := listener.Close()
if err != nil {
require.ErrorContains(t, err, "use of closed network connection")
}
})
server := grpc.NewServer()
t.Cleanup(server.Stop)
runtimeapi.RegisterRuntimeServiceServer(server, &fakeRuntimeService{})
go func() {
_ = server.Serve(listener)
}()
conn, err := newRuntimeClientConn("unix://" + socketPath)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, conn.Close())
})
client := runtimeapi.NewRuntimeServiceClient(conn)
stream, err := client.GetContainerEvents(context.Background(), &runtimeapi.GetEventsRequest{})
require.NoError(t, err)
resp, err := stream.Recv()
require.NoError(t, err)
require.Equal(t, runtimeapi.ContainerEventType_CONTAINER_CREATED_EVENT, resp.GetContainerEventType())
}
@@ -1,71 +0,0 @@
//go:build !unix && !windows
/*
Copyright The containerd 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.
*/
/*
Copyright 2017 The Kubernetes 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 util
import (
"context"
"fmt"
"net"
"time"
)
// CreateListener creates a listener on the specified endpoint.
func CreateListener(endpoint string) (net.Listener, error) {
return nil, fmt.Errorf("CreateListener is unsupported in this build")
}
// GetAddressAndDialer returns the address parsed from the given endpoint and a context dialer.
func GetAddressAndDialer(endpoint string) (string, func(ctx context.Context, addr string) (net.Conn, error), error) {
return "", nil, fmt.Errorf("GetAddressAndDialer is unsupported in this build")
}
// LockAndCheckSubPath empty implementation
func LockAndCheckSubPath(volumePath, subPath string) ([]uintptr, error) {
return []uintptr{}, nil
}
// UnlockPath empty implementation
func UnlockPath(fileHandles []uintptr) {
}
// LocalEndpoint empty implementation
func LocalEndpoint(path, file string) (string, error) {
return "", fmt.Errorf("LocalEndpoint is unsupported in this build")
}
// GetBootTime empty implementation
func GetBootTime() (time.Time, error) {
return time.Time{}, fmt.Errorf("GetBootTime is unsupported in this build")
}
-171
View File
@@ -1,171 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2017 The Kubernetes 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 util
import (
"context"
"errors"
"fmt"
"net"
"net/url"
"strings"
"syscall"
"time"
"github.com/Microsoft/go-winio"
)
const (
tcpProtocol = "tcp"
npipeProtocol = "npipe"
)
// CreateListener creates a listener on the specified endpoint.
func CreateListener(endpoint string) (net.Listener, error) {
protocol, addr, err := parseEndpoint(endpoint)
if err != nil {
return nil, err
}
switch protocol {
case tcpProtocol:
return net.Listen(tcpProtocol, addr)
case npipeProtocol:
return winio.ListenPipe(addr, nil)
default:
return nil, fmt.Errorf("only support tcp and npipe endpoint")
}
}
// GetAddressAndDialer returns the address parsed from the given endpoint and a context dialer.
func GetAddressAndDialer(endpoint string) (string, func(ctx context.Context, addr string) (net.Conn, error), error) {
protocol, addr, err := parseEndpoint(endpoint)
if err != nil {
return "", nil, err
}
// Use passthrough as the scheme so it allows us to use our custom dialer:
//
// "grpc.Dial uses "passthrough" as the default name resolver for backward compatibility while grpc.NewClient
// uses "dns" as its default name resolver. This subtle difference is important to legacy systems that also
// specified a custom dialer and expected it to receive the target string directly."
// https://github.com/grpc/grpc-go/blob/master/Documentation/anti-patterns.md#the-wrong-way-grpcdial
addr = fmt.Sprintf("passthrough:///%s", addr)
if protocol == tcpProtocol {
return addr, tcpDial, nil
}
if protocol == npipeProtocol {
return addr, npipeDial, nil
}
return "", nil, errors.New("only support tcp and npipe endpoint")
}
func tcpDial(ctx context.Context, addr string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, tcpProtocol, addr)
}
func npipeDial(ctx context.Context, addr string) (net.Conn, error) {
return winio.DialPipeContext(ctx, addr)
}
func parseEndpoint(endpoint string) (string, string, error) {
// url.Parse doesn't recognize \, so replace with / first.
endpoint = strings.ReplaceAll(endpoint, "\\", "/")
u, err := url.Parse(endpoint)
if err != nil {
return "", "", err
}
switch u.Scheme {
case "tcp":
return "tcp", u.Host, nil
case "npipe":
if strings.HasPrefix(u.Path, "//./pipe") {
return "npipe", u.Path, nil
}
// fallback host if not provided.
host := u.Host
if host == "" {
host = "."
}
return "npipe", fmt.Sprintf("//%s%s", host, u.Path), nil
case "":
return "", "", fmt.Errorf("using %q as endpoint is deprecated, please consider using full url format", endpoint)
}
return u.Scheme, "", fmt.Errorf("protocol %q not supported", u.Scheme)
}
var tickCount = syscall.NewLazyDLL("kernel32.dll").NewProc("GetTickCount64")
// GetBootTime returns the time at which the machine was started, truncated to the nearest second
func GetBootTime() (time.Time, error) {
currentTime := time.Now()
output, _, err := tickCount.Call()
if errno, ok := err.(syscall.Errno); !ok || errno != 0 {
return time.Time{}, err
}
return currentTime.Add(-time.Duration(output) * time.Millisecond).Truncate(time.Second), nil
}
// IsUnixDomainSocket returns whether a given file is a AF_UNIX socket file
func IsUnixDomainSocket(filePath string) (bool, error) {
// Due to the absence of golang support for os.ModeSocket in Windows (https://github.com/golang/go/issues/33357)
// we need to dial the file and check if we receive an error to determine if a file is Unix Domain Socket file.
// Note that querrying for the Reparse Points (https://docs.microsoft.com/en-us/windows/win32/fileio/reparse-points)
// for the file (using FSCTL_GET_REPARSE_POINT) and checking for reparse tag: reparseTagSocket
// does NOT work in 1809 if the socket file is created within a bind mounted directory by a container
// and the FSCTL is issued in the host by the kubelet.
c, err := net.Dial("unix", filePath)
if err == nil {
c.Close()
return true, nil
}
return false, nil
}
// NormalizePath converts FS paths returned by certain go frameworks (like fsnotify)
// to native Windows paths that can be passed to Windows specific code
func NormalizePath(path string) string {
path = strings.ReplaceAll(path, "/", "\\")
if strings.HasPrefix(path, "\\") {
path = "c:" + path
}
return path
}
@@ -240,8 +240,9 @@ func TestCreateContainer(t *testing.T) {
if err != nil {
t.Skip("skipping test, error: ", err)
}
client, err := RawRuntimeClient()
client, conn, err := RawRuntimeClient()
require.NoError(t, err, "failed to get raw grpc runtime service client")
t.Cleanup(func() { _ = conn.Close() })
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
+5 -4
View File
@@ -35,7 +35,7 @@ import (
"github.com/stretchr/testify/require"
criapiv1 "k8s.io/cri-api/pkg/apis/runtime/v1"
"github.com/containerd/containerd/v2/internal/cri/types"
podsandboxtypes "github.com/containerd/containerd/v2/internal/cri/server/podsandbox/types"
"github.com/containerd/containerd/v2/internal/failpoint"
)
@@ -302,11 +302,12 @@ func TestRunPodSandboxAndTeardownCNISlow(t *testing.T) {
}
// sbserverSandboxInfo gets sandbox info.
func sbserverSandboxInfo(id string) (*criapiv1.PodSandboxStatus, *types.SandboxInfo, error) {
client, err := RawRuntimeClient()
func sbserverSandboxInfo(id string) (*criapiv1.PodSandboxStatus, *podsandboxtypes.SandboxInfo, error) {
client, conn, err := RawRuntimeClient()
if err != nil {
return nil, nil, fmt.Errorf("failed to get raw runtime client: %w", err)
}
defer conn.Close()
resp, err := client.PodSandboxStatus(context.Background(), &criapiv1.PodSandboxStatusRequest{
PodSandboxId: id,
Verbose: true,
@@ -315,7 +316,7 @@ func sbserverSandboxInfo(id string) (*criapiv1.PodSandboxStatus, *types.SandboxI
return nil, nil, fmt.Errorf("failed to get sandbox status: %w", err)
}
status := resp.GetStatus()
var info types.SandboxInfo
var info podsandboxtypes.SandboxInfo
if err := json.Unmarshal([]byte(resp.GetInfo()["info"]), &info); err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal sandbox info: %w", err)
}
+2 -1
View File
@@ -231,8 +231,9 @@ func TestVolumeOwnership(t *testing.T) {
}
func getContainerBindVolumes(t *testing.T, containerID string) (map[string]string, error) {
client, err := RawRuntimeClient()
client, conn, err := RawRuntimeClient()
require.NoError(t, err, "failed to get raw grpc runtime service client")
defer conn.Close()
request := &v1.ContainerStatusRequest{
ContainerId: containerID,
Verbose: true,
+1 -1
View File
@@ -33,8 +33,8 @@ limitations under the License.
package bandwidth
import (
resource "github.com/containerd/containerd/v2/internal/cri/resourcequantity"
"github.com/containerd/errdefs"
resource "k8s.io/apimachinery/pkg/api/resource"
)
// FakeShaper provides an implementation of the bandwidth.Shaper.
+1 -1
View File
@@ -32,7 +32,7 @@ limitations under the License.
package bandwidth
import resource "github.com/containerd/containerd/v2/internal/cri/resourcequantity"
import resource "k8s.io/apimachinery/pkg/api/resource"
// Shaper is designed so that the shaper structs created
// satisfy the Shaper interface.
+4 -4
View File
@@ -42,11 +42,11 @@ import (
"net"
"strings"
executil "github.com/containerd/containerd/v2/internal/cri/executil"
resource "github.com/containerd/containerd/v2/internal/cri/resourcequantity"
"github.com/containerd/containerd/v2/internal/cri/setutils"
"github.com/containerd/containerd/v2/internal/lazyregexp"
"github.com/containerd/log"
resource "k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/util/sets"
executil "k8s.io/utils/exec"
)
var (
@@ -89,7 +89,7 @@ func (t *tcShaper) nextClassID() (int, error) {
}
scanner := bufio.NewScanner(bytes.NewBuffer(data))
classes := setutils.Set[string]{}
classes := sets.Set[string]{}
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// skip empty lines
+1 -1
View File
@@ -35,8 +35,8 @@ limitations under the License.
package bandwidth
import (
resource "github.com/containerd/containerd/v2/internal/cri/resourcequantity"
"github.com/containerd/errdefs"
resource "k8s.io/apimachinery/pkg/api/resource"
)
type unsupportedShaper struct {
+1 -1
View File
@@ -35,7 +35,7 @@ package bandwidth
import (
"fmt"
resource "github.com/containerd/containerd/v2/internal/cri/resourcequantity"
resource "k8s.io/apimachinery/pkg/api/resource"
)
var minRsrc = resource.MustParse("1k")
-7
View File
@@ -1,7 +0,0 @@
# Clock
This package provides an interface for time-based operations. It allows
mocking time for testing.
This is a copy of k8s.io/utils/clock. We have to copy it to avoid importing
internal k8s utilities
-194
View File
@@ -1,194 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2014 The Kubernetes 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 clock
import "time"
// PassiveClock allows for injecting fake or real clocks into code
// that needs to read the current time but does not support scheduling
// activity in the future.
type PassiveClock interface {
Now() time.Time
Since(time.Time) time.Duration
}
// Clock allows for injecting fake or real clocks into code that
// needs to do arbitrary things based on time.
type Clock interface {
PassiveClock
// After returns the channel of a new Timer.
// This method does not allow to free/GC the backing timer before it fires. Use
// NewTimer instead.
After(d time.Duration) <-chan time.Time
// NewTimer returns a new Timer.
NewTimer(d time.Duration) Timer
// Sleep sleeps for the provided duration d.
// Consider making the sleep interruptible by using 'select' on a context channel and a timer channel.
Sleep(d time.Duration)
// Tick returns the channel of a new Ticker.
// This method does not allow to free/GC the backing ticker. Use
// NewTicker from WithTicker instead.
Tick(d time.Duration) <-chan time.Time
}
// WithTicker allows for injecting fake or real clocks into code that
// needs to do arbitrary things based on time.
type WithTicker interface {
Clock
// NewTicker returns a new Ticker.
NewTicker(time.Duration) Ticker
}
// WithDelayedExecution allows for injecting fake or real clocks into
// code that needs to make use of AfterFunc functionality.
type WithDelayedExecution interface {
Clock
// AfterFunc executes f in its own goroutine after waiting
// for d duration and returns a Timer whose channel can be
// closed by calling Stop() on the Timer.
AfterFunc(d time.Duration, f func()) Timer
}
// WithTickerAndDelayedExecution allows for injecting fake or real clocks
// into code that needs Ticker and AfterFunc functionality
type WithTickerAndDelayedExecution interface {
WithTicker
// AfterFunc executes f in its own goroutine after waiting
// for d duration and returns a Timer whose channel can be
// closed by calling Stop() on the Timer.
AfterFunc(d time.Duration, f func()) Timer
}
// Ticker defines the Ticker interface.
type Ticker interface {
C() <-chan time.Time
Stop()
}
var _ = WithTicker(RealClock{})
// RealClock really calls time.Now()
type RealClock struct{}
// Now returns the current time.
func (RealClock) Now() time.Time {
return time.Now()
}
// Since returns time since the specified timestamp.
func (RealClock) Since(ts time.Time) time.Duration {
return time.Since(ts)
}
// After is the same as time.After(d).
// This method does not allow to free/GC the backing timer before it fires. Use
// NewTimer instead.
func (RealClock) After(d time.Duration) <-chan time.Time {
return time.After(d)
}
// NewTimer is the same as time.NewTimer(d)
func (RealClock) NewTimer(d time.Duration) Timer {
return &realTimer{
timer: time.NewTimer(d),
}
}
// AfterFunc is the same as time.AfterFunc(d, f).
func (RealClock) AfterFunc(d time.Duration, f func()) Timer {
return &realTimer{
timer: time.AfterFunc(d, f),
}
}
// Tick is the same as time.Tick(d)
// This method does not allow to free/GC the backing ticker. Use
// NewTicker instead.
func (RealClock) Tick(d time.Duration) <-chan time.Time {
return time.Tick(d)
}
// NewTicker returns a new Ticker.
func (RealClock) NewTicker(d time.Duration) Ticker {
return &realTicker{
ticker: time.NewTicker(d),
}
}
// Sleep is the same as time.Sleep(d)
// Consider making the sleep interruptible by using 'select' on a context channel and a timer channel.
func (RealClock) Sleep(d time.Duration) {
time.Sleep(d)
}
// Timer allows for injecting fake or real timers into code that
// needs to do arbitrary things based on time.
type Timer interface {
C() <-chan time.Time
Stop() bool
Reset(d time.Duration) bool
}
var _ = Timer(&realTimer{})
// realTimer is backed by an actual time.Timer.
type realTimer struct {
timer *time.Timer
}
// C returns the underlying timer's channel.
func (r *realTimer) C() <-chan time.Time {
return r.timer.C
}
// Stop calls Stop() on the underlying timer.
func (r *realTimer) Stop() bool {
return r.timer.Stop()
}
// Reset calls Reset() on the underlying timer.
func (r *realTimer) Reset(d time.Duration) bool {
return r.timer.Reset(d)
}
type realTicker struct {
ticker *time.Ticker
}
func (r *realTicker) C() <-chan time.Time {
return r.ticker.C
}
func (r *realTicker) Stop() {
r.ticker.Stop()
}
-1
View File
@@ -26,7 +26,6 @@ import (
k8snet "k8s.io/apimachinery/pkg/util/net"
k8scert "k8s.io/client-go/util/cert"
streaming "k8s.io/cri-streaming/pkg/streaming"
)
-4
View File
@@ -1,4 +0,0 @@
# Executil
This package provides an interface for `os/exec`. It makes it easier to mock
and replace in tests
-34
View File
@@ -1,34 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2017 The Kubernetes 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 exec provides an injectable interface and implementations for running commands.
package executil // cloned from "k8s.io/utils/exec"
-288
View File
@@ -1,288 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2017 The Kubernetes 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 executil
import (
"context"
"errors"
"io"
"io/fs"
osexec "os/exec"
"syscall"
"time"
)
// ErrExecutableNotFound is returned if the executable is not found.
var ErrExecutableNotFound = osexec.ErrNotFound
// Interface is an interface that presents a subset of the os/exec API. Use this
// when you want to inject fakeable/mockable exec behavior.
type Interface interface {
// Command returns a Cmd instance which can be used to run a single command.
// This follows the pattern of package os/exec.
Command(cmd string, args ...string) Cmd
// CommandContext returns a Cmd instance which can be used to run a single command.
//
// The provided context is used to kill the process if the context becomes done
// before the command completes on its own. For example, a timeout can be set in
// the context.
CommandContext(ctx context.Context, cmd string, args ...string) Cmd
// LookPath wraps os/exec.LookPath
LookPath(file string) (string, error)
}
// Cmd is an interface that presents an API that is very similar to Cmd from os/exec.
// As more functionality is needed, this can grow. Since Cmd is a struct, we will have
// to replace fields with get/set method pairs.
type Cmd interface {
// Run runs the command to the completion.
Run() error
// CombinedOutput runs the command and returns its combined standard output
// and standard error. This follows the pattern of package os/exec.
CombinedOutput() ([]byte, error)
// Output runs the command and returns standard output, but not standard err
Output() ([]byte, error)
SetDir(dir string)
SetStdin(in io.Reader)
SetStdout(out io.Writer)
SetStderr(out io.Writer)
SetEnv(env []string)
// StdoutPipe and StderrPipe for getting the process' Stdout and Stderr as
// Readers
StdoutPipe() (io.ReadCloser, error)
StderrPipe() (io.ReadCloser, error)
// Start and Wait are for running a process non-blocking
Start() error
Wait() error
// Stops the command by sending SIGTERM. It is not guaranteed the
// process will stop before this function returns. If the process is not
// responding, an internal timer function will send a SIGKILL to force
// terminate after 10 seconds.
Stop()
}
// ExitError is an interface that presents an API similar to os.ProcessState, which is
// what ExitError from os/exec is. This is designed to make testing a bit easier and
// probably loses some of the cross-platform properties of the underlying library.
type ExitError interface {
String() string
Error() string
Exited() bool
ExitStatus() int
}
// Implements Interface in terms of really exec()ing.
type executor struct{}
// New returns a new Interface which will os/exec to run commands.
func New() Interface {
return &executor{}
}
// Command is part of the Interface interface.
func (executor *executor) Command(cmd string, args ...string) Cmd {
return (*cmdWrapper)(maskErrDotCmd(osexec.Command(cmd, args...)))
}
// CommandContext is part of the Interface interface.
func (executor *executor) CommandContext(ctx context.Context, cmd string, args ...string) Cmd {
return (*cmdWrapper)(maskErrDotCmd(osexec.CommandContext(ctx, cmd, args...)))
}
// LookPath is part of the Interface interface
func (executor *executor) LookPath(file string) (string, error) {
path, err := osexec.LookPath(file)
return path, handleError(maskErrDot(err))
}
// Wraps exec.Cmd so we can capture errors.
type cmdWrapper osexec.Cmd
var _ Cmd = &cmdWrapper{}
func (cmd *cmdWrapper) SetDir(dir string) {
cmd.Dir = dir
}
func (cmd *cmdWrapper) SetStdin(in io.Reader) {
cmd.Stdin = in
}
func (cmd *cmdWrapper) SetStdout(out io.Writer) {
cmd.Stdout = out
}
func (cmd *cmdWrapper) SetStderr(out io.Writer) {
cmd.Stderr = out
}
func (cmd *cmdWrapper) SetEnv(env []string) {
cmd.Env = env
}
func (cmd *cmdWrapper) StdoutPipe() (io.ReadCloser, error) {
r, err := (*osexec.Cmd)(cmd).StdoutPipe()
return r, handleError(err)
}
func (cmd *cmdWrapper) StderrPipe() (io.ReadCloser, error) {
r, err := (*osexec.Cmd)(cmd).StderrPipe()
return r, handleError(err)
}
func (cmd *cmdWrapper) Start() error {
err := (*osexec.Cmd)(cmd).Start()
return handleError(err)
}
func (cmd *cmdWrapper) Wait() error {
err := (*osexec.Cmd)(cmd).Wait()
return handleError(err)
}
// Run is part of the Cmd interface.
func (cmd *cmdWrapper) Run() error {
err := (*osexec.Cmd)(cmd).Run()
return handleError(err)
}
// CombinedOutput is part of the Cmd interface.
func (cmd *cmdWrapper) CombinedOutput() ([]byte, error) {
out, err := (*osexec.Cmd)(cmd).CombinedOutput()
return out, handleError(err)
}
func (cmd *cmdWrapper) Output() ([]byte, error) {
out, err := (*osexec.Cmd)(cmd).Output()
return out, handleError(err)
}
// Stop is part of the Cmd interface.
func (cmd *cmdWrapper) Stop() {
c := (*osexec.Cmd)(cmd)
if c.Process == nil {
return
}
c.Process.Signal(syscall.SIGTERM)
time.AfterFunc(10*time.Second, func() {
if !c.ProcessState.Exited() {
c.Process.Signal(syscall.SIGKILL)
}
})
}
func handleError(err error) error {
if err == nil {
return nil
}
switch e := err.(type) {
case *osexec.ExitError:
return &ExitErrorWrapper{e}
case *fs.PathError:
return ErrExecutableNotFound
case *osexec.Error:
if e.Err == osexec.ErrNotFound {
return ErrExecutableNotFound
}
}
return err
}
// ExitErrorWrapper is an implementation of ExitError in terms of os/exec ExitError.
// Note: standard exec.ExitError is type *os.ProcessState, which already implements Exited().
type ExitErrorWrapper struct {
*osexec.ExitError
}
var _ ExitError = &ExitErrorWrapper{}
// ExitStatus is part of the ExitError interface.
func (eew ExitErrorWrapper) ExitStatus() int {
ws, ok := eew.Sys().(syscall.WaitStatus)
if !ok {
panic("can't call ExitStatus() on a non-WaitStatus exitErrorWrapper")
}
return ws.ExitStatus()
}
// CodeExitError is an implementation of ExitError consisting of an error object
// and an exit code (the upper bits of os.exec.ExitStatus).
type CodeExitError struct {
Err error
Code int
}
var _ ExitError = CodeExitError{}
func (e CodeExitError) Error() string {
return e.Err.Error()
}
func (e CodeExitError) String() string {
return e.Err.Error()
}
// Exited is to check if the process has finished
func (e CodeExitError) Exited() bool {
return true
}
// ExitStatus is for checking the error code
func (e CodeExitError) ExitStatus() int {
return e.Code
}
// maskErrDotCmd reverts the behavior of osexec.Cmd to what it was before go1.19
// specifically set the Err field to nil (LookPath returns a new error when the file
// is resolved to the current directory.
func maskErrDotCmd(cmd *osexec.Cmd) *osexec.Cmd {
cmd.Err = maskErrDot(cmd.Err)
return cmd
}
func maskErrDot(err error) error {
if err != nil && errors.Is(err, osexec.ErrDot) {
return nil
}
return err
}
-281
View File
@@ -1,281 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2017 The Kubernetes 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 executil
import (
"context"
"errors"
"io"
osexec "os/exec"
goruntime "runtime"
"testing"
"time"
)
func TestExecutorNoArgs(t *testing.T) {
ex := New()
cmd := ex.Command("true")
out, err := cmd.CombinedOutput()
if err != nil {
t.Errorf("expected success, got %v", err)
}
if len(out) != 0 {
t.Errorf("expected no output, got %q", string(out))
}
cmd = ex.Command("false")
out, err = cmd.CombinedOutput()
if err == nil {
t.Errorf("expected failure, got nil error")
}
if len(out) != 0 {
t.Errorf("expected no output, got %q", string(out))
}
ee, ok := err.(ExitError)
if !ok {
t.Errorf("expected an ExitError, got %+v", err)
}
if ee.Exited() {
if code := ee.ExitStatus(); code != 1 {
t.Errorf("expected exit status 1, got %d", code)
}
}
cmd = ex.Command("/does/not/exist")
_, err = cmd.CombinedOutput()
if err == nil {
t.Errorf("expected failure, got nil error")
}
if ee, ok := err.(ExitError); ok {
t.Errorf("expected non-ExitError, got %+v", ee)
}
}
func TestExecutorWithArgs(t *testing.T) {
if goruntime.GOOS == "windows" {
t.Skip("Skipped on Windows.")
}
ex := New()
cmd := ex.Command("echo", "stdout")
out, err := cmd.CombinedOutput()
if err != nil {
t.Errorf("expected success, got %+v", err)
}
if string(out) != "stdout\n" {
t.Errorf("unexpected output: %q", string(out))
}
cmd = ex.Command("/bin/sh", "-c", "echo stderr > /dev/stderr")
out, err = cmd.CombinedOutput()
if err != nil {
t.Errorf("expected success, got %+v", err)
}
if string(out) != "stderr\n" {
t.Errorf("unexpected output: %q", string(out))
}
}
func TestLookPath(t *testing.T) {
ex := New()
shExpected, _ := osexec.LookPath("sh")
sh, _ := ex.LookPath("sh")
if sh != shExpected {
t.Errorf("unexpected result for LookPath: got %s, expected %s", sh, shExpected)
}
}
func TestExecutableNotFound(t *testing.T) {
exec := New()
cmd := exec.Command("fake_executable_name")
_, err := cmd.CombinedOutput()
if err != ErrExecutableNotFound {
t.Errorf("cmd.CombinedOutput(): Expected error ErrExecutableNotFound but got %v", err)
}
cmd = exec.Command("fake_executable_name")
_, err = cmd.Output()
if err != ErrExecutableNotFound {
t.Errorf("cmd.Output(): Expected error ErrExecutableNotFound but got %v", err)
}
cmd = exec.Command("fake_executable_name")
err = cmd.Run()
if err != ErrExecutableNotFound {
t.Errorf("cmd.Run(): Expected error ErrExecutableNotFound but got %v", err)
}
}
func TestStopBeforeStart(t *testing.T) {
cmd := New().Command("echo", "hello")
// no panic calling Stop before calling Run
cmd.Stop()
cmd.Run()
// no panic calling Stop after command is done
cmd.Stop()
}
func TestTimeout(t *testing.T) {
exec := New()
ctx, cancel := context.WithTimeout(context.Background(), time.Nanosecond)
defer cancel()
err := exec.CommandContext(ctx, "sleep", "2").Run()
if !errors.Is(ctx.Err(), context.DeadlineExceeded) {
t.Errorf("expected %v but got %v", context.DeadlineExceeded, err)
}
}
func TestSetEnv(t *testing.T) {
if goruntime.GOOS == "windows" {
t.Skip("Skipped on Windows.")
}
ex := New()
out, err := ex.Command("/bin/sh", "-c", "echo $FOOBAR").CombinedOutput()
if err != nil {
t.Errorf("expected success, got %+v", err)
}
if string(out) != "\n" {
t.Errorf("unexpected output: %q", string(out))
}
cmd := ex.Command("/bin/sh", "-c", "echo $FOOBAR")
cmd.SetEnv([]string{"FOOBAR=baz"})
out, err = cmd.CombinedOutput()
if err != nil {
t.Errorf("expected success, got %+v", err)
}
if string(out) != "baz\n" {
t.Errorf("unexpected output: %q", string(out))
}
}
func TestStdIOPipes(t *testing.T) {
if goruntime.GOOS == "windows" {
t.Skip("Skipped on Windows.")
}
cmd := New().Command("/bin/sh", "-c", "echo 'OUT'>&1; echo 'ERR'>&2")
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
t.Fatalf("expected StdoutPipe() not to error, got: %v", err)
}
stderrPipe, err := cmd.StderrPipe()
if err != nil {
t.Fatalf("expected StderrPipe() not to error, got: %v", err)
}
stdout := make(chan string)
stderr := make(chan string)
go func() {
stdout <- readAll(t, stdoutPipe, "StdOut")
}()
go func() {
stderr <- readAll(t, stderrPipe, "StdErr")
}()
if err := cmd.Start(); err != nil {
t.Errorf("expected Start() not to error, got: %v", err)
}
if e, a := "OUT\n", <-stdout; e != a {
t.Errorf("expected StdOut to be '%s', got: '%v'", e, a)
}
if e, a := "ERR\n", <-stderr; e != a {
t.Errorf("expected StdErr to be '%s', got: '%v'", e, a)
}
if err := cmd.Wait(); err != nil {
t.Errorf("expected Wait() not to error, got: %v", err)
}
}
func readAll(t *testing.T, r io.Reader, n string) string {
t.Helper()
b, err := io.ReadAll(r)
if err != nil {
t.Fatalf("unexpected error when reading from %s: %v", n, err)
}
return string(b)
}
func TestExecutorGo119LookPath(t *testing.T) {
if goruntime.GOOS == "windows" {
t.Skip("Skipped on Windows.")
}
t.Setenv("PATH", "./testdata")
ex := New()
path, err := ex.LookPath("hello.sh")
if err != nil {
t.Errorf("expected success, got %v", err)
}
if path != "testdata/hello.sh" {
t.Errorf("expected relative path to hello script, got %v", path)
}
cmd := ex.Command("hello.sh")
out, err := cmd.CombinedOutput()
if err != nil {
t.Errorf("expected success, got %v", err)
} else {
if len(out) != 5 {
t.Errorf("expected 'hello' output, got %q", string(out))
}
}
cmd = ex.CommandContext(context.Background(), "hello.sh")
out, err = cmd.CombinedOutput()
if err != nil {
t.Errorf("expected success, got %v", err)
} else {
if len(out) != 5 {
t.Errorf("expected 'hello' output, got %q", string(out))
}
}
}
-17
View File
@@ -1,17 +0,0 @@
#!/bin/bash
# Copyright The containerd 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.
echo -n "hello"
+26
View File
@@ -14,6 +14,22 @@
limitations under the License.
*/
/*
Copyright 2016 The Kubernetes 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 labels
const (
@@ -37,4 +53,14 @@ const (
ContainerMetadataExtension = criContainerdPrefix + ".container.metadata"
// SandboxMetadataExtension is an extension name that identify metadata of sandbox in CreateContainerRequest
SandboxMetadataExtension = criContainerdPrefix + ".sandbox.metadata"
// KubernetesPodNameLabel is the pod name label used by Kubernetes.
KubernetesPodNameLabel = "io.kubernetes.pod.name"
// KubernetesPodNamespaceLabel is the pod namespace label used by Kubernetes.
KubernetesPodNamespaceLabel = "io.kubernetes.pod.namespace"
// KubernetesPodUIDLabel is the pod UID label used by Kubernetes.
KubernetesPodUIDLabel = "io.kubernetes.pod.uid"
// KubernetesContainerNameLabel is the container name label used by Kubernetes.
KubernetesContainerNameLabel = "io.kubernetes.container.name"
// PodInfraContainerName is the Kubernetes container name used for infra containers.
PodInfraContainerName = "POD"
)
-352
View File
@@ -1,352 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2014 The Kubernetes 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 resourcequantity
import (
"math/big"
"strconv"
inf "gopkg.in/inf.v0"
)
// Scale is used for getting and setting the base-10 scaled value.
// Base-2 scales are omitted for mathematical simplicity.
// See Quantity.ScaledValue for more details.
type Scale int32
// infScale adapts a Scale value to an inf.Scale value.
func (s Scale) infScale() inf.Scale {
return inf.Scale(-s) // inf.Scale is upside-down
}
const (
Nano Scale = -9
Micro Scale = -6
Milli Scale = -3
Kilo Scale = 3
Mega Scale = 6
Giga Scale = 9
Tera Scale = 12
Peta Scale = 15
Exa Scale = 18
)
var (
Zero = int64Amount{}
// Used by quantity strings - treat as read only
zeroBytes = []byte("0")
)
// int64Amount represents a fixed precision numerator and arbitrary scale exponent. It is faster
// than operations on inf.Dec for values that can be represented as int64.
type int64Amount struct {
value int64
scale Scale
}
// Sign returns 0 if the value is zero, -1 if it is less than 0, or 1 if it is greater than 0.
func (a int64Amount) Sign() int {
switch {
case a.value == 0:
return 0
case a.value > 0:
return 1
default:
return -1
}
}
// AsInt64 returns the current amount as an int64 at scale 0, or false if the value cannot be
// represented in an int64 OR would result in a loss of precision. This method is intended as
// an optimization to avoid calling AsDec.
func (a int64Amount) AsInt64() (int64, bool) {
if a.scale == 0 {
return a.value, true
}
if a.scale < 0 {
// TODO: attempt to reduce factors, although it is assumed that factors are reduced prior
// to the int64Amount being created.
return 0, false
}
return positiveScaleInt64(a.value, a.scale)
}
// AsScaledInt64 returns an int64 representing the value of this amount at the specified scale,
// rounding up, or false if that would result in overflow. (1e20).AsScaledInt64(1) would result
// in overflow because 1e19 is not representable as an int64. Note that setting a scale larger
// than the current value may result in loss of precision - i.e. (1e-6).AsScaledInt64(0) would
// return 1, because 0.000001 is rounded up to 1.
func (a int64Amount) AsScaledInt64(scale Scale) (result int64, ok bool) {
if a.scale < scale {
result, _ = negativeScaleInt64(a.value, scale-a.scale)
return result, true
}
return positiveScaleInt64(a.value, a.scale-scale)
}
// AsDec returns an inf.Dec representation of this value.
func (a int64Amount) AsDec() *inf.Dec {
var base inf.Dec
base.SetUnscaled(a.value)
base.SetScale(inf.Scale(-a.scale))
return &base
}
// Cmp returns 0 if a and b are equal, 1 if a is greater than b, or -1 if a is less than b.
func (a int64Amount) Cmp(b int64Amount) int {
switch {
case a.scale == b.scale:
// compare only the unscaled portion
case a.scale > b.scale:
result, remainder, exact := divideByScaleInt64(b.value, a.scale-b.scale)
if !exact {
return a.AsDec().Cmp(b.AsDec())
}
if result == a.value {
switch {
case remainder == 0:
return 0
case remainder > 0:
return -1
default:
return 1
}
}
b.value = result
default:
result, remainder, exact := divideByScaleInt64(a.value, b.scale-a.scale)
if !exact {
return a.AsDec().Cmp(b.AsDec())
}
if result == b.value {
switch {
case remainder == 0:
return 0
case remainder > 0:
return 1
default:
return -1
}
}
a.value = result
}
switch {
case a.value == b.value:
return 0
case a.value < b.value:
return -1
default:
return 1
}
}
// Add adds two int64Amounts together, matching scales. It will return false and not mutate
// a if overflow or underflow would result.
func (a *int64Amount) Add(b int64Amount) bool {
switch {
case b.value == 0:
return true
case a.value == 0:
a.value = b.value
a.scale = b.scale
return true
case a.scale == b.scale:
c, ok := int64Add(a.value, b.value)
if !ok {
return false
}
a.value = c
case a.scale > b.scale:
c, ok := positiveScaleInt64(a.value, a.scale-b.scale)
if !ok {
return false
}
c, ok = int64Add(c, b.value)
if !ok {
return false
}
a.scale = b.scale
a.value = c
default:
c, ok := positiveScaleInt64(b.value, b.scale-a.scale)
if !ok {
return false
}
c, ok = int64Add(a.value, c)
if !ok {
return false
}
a.value = c
}
return true
}
// Sub removes the value of b from the current amount, or returns false if underflow would result.
func (a *int64Amount) Sub(b int64Amount) bool {
return a.Add(int64Amount{value: -b.value, scale: b.scale})
}
// Mul multiplies the provided b to the current amount, or
// returns false if overflow or underflow would result.
func (a *int64Amount) Mul(b int64) bool {
switch {
case a.value == 0:
return true
case b == 0:
a.value = 0
a.scale = 0
return true
case a.scale == 0:
c, ok := int64Multiply(a.value, b)
if !ok {
return false
}
a.value = c
case a.scale > 0:
c, ok := int64Multiply(a.value, b)
if !ok {
return false
}
if _, ok = positiveScaleInt64(c, a.scale); !ok {
return false
}
a.value = c
default:
c, ok := int64Multiply(a.value, b)
if !ok {
return false
}
if _, ok = negativeScaleInt64(c, -a.scale); !ok {
return false
}
a.value = c
}
return true
}
// AsScale adjusts this amount to set a minimum scale, rounding up, and returns true iff no precision
// was lost. (1.1e5).AsScale(5) would return 1.1e5, but (1.1e5).AsScale(6) would return 1e6.
func (a int64Amount) AsScale(scale Scale) (int64Amount, bool) {
if a.scale >= scale {
return a, true
}
result, exact := negativeScaleInt64(a.value, scale-a.scale)
return int64Amount{value: result, scale: scale}, exact
}
// AsCanonicalBytes accepts a buffer to write the base-10 string value of this field to, and returns
// either that buffer or a larger buffer and the current exponent of the value. The value is adjusted
// until the exponent is a multiple of 3 - i.e. 1.1e5 would return "110", 3.
func (a int64Amount) AsCanonicalBytes(out []byte) (result []byte, exponent int32) {
mantissa := a.value
exponent = int32(a.scale)
amount, times := removeInt64Factors(mantissa, 10)
exponent += times
// make sure exponent is a multiple of 3
var ok bool
switch exponent % 3 {
case 1, -2:
amount, ok = int64MultiplyScale10(amount)
if !ok {
return infDecAmount{a.AsDec()}.AsCanonicalBytes(out)
}
exponent = exponent - 1
case 2, -1:
amount, ok = int64MultiplyScale100(amount)
if !ok {
return infDecAmount{a.AsDec()}.AsCanonicalBytes(out)
}
exponent = exponent - 2
}
return strconv.AppendInt(out, amount, 10), exponent
}
// AsCanonicalBase1024Bytes accepts a buffer to write the base-1024 string value of this field to, and returns
// either that buffer or a larger buffer and the current exponent of the value. 2048 is 2 * 1024 ^ 1 and would
// return []byte("2048"), 1.
func (a int64Amount) AsCanonicalBase1024Bytes(out []byte) (result []byte, exponent int32) {
value, ok := a.AsScaledInt64(0)
if !ok {
return infDecAmount{a.AsDec()}.AsCanonicalBase1024Bytes(out)
}
amount, exponent := removeInt64Factors(value, 1024)
return strconv.AppendInt(out, amount, 10), exponent
}
// infDecAmount implements common operations over an inf.Dec that are specific to the quantity
// representation.
type infDecAmount struct {
*inf.Dec
}
// AsScale adjusts this amount to set a minimum scale, rounding up, and returns true iff no precision
// was lost. (1.1e5).AsScale(5) would return 1.1e5, but (1.1e5).AsScale(6) would return 1e6.
func (a infDecAmount) AsScale(scale Scale) (infDecAmount, bool) {
tmp := &inf.Dec{}
tmp.Round(a.Dec, scale.infScale(), inf.RoundUp)
return infDecAmount{tmp}, tmp.Cmp(a.Dec) == 0
}
// AsCanonicalBytes accepts a buffer to write the base-10 string value of this field to, and returns
// either that buffer or a larger buffer and the current exponent of the value. The value is adjusted
// until the exponent is a multiple of 3 - i.e. 1.1e5 would return "110", 3.
func (a infDecAmount) AsCanonicalBytes(out []byte) (result []byte, exponent int32) {
mantissa := a.Dec.UnscaledBig()
exponent = int32(-a.Dec.Scale())
amount := big.NewInt(0).Set(mantissa)
// move all factors of 10 into the exponent for easy reasoning
amount, times := removeBigIntFactors(amount, bigTen)
exponent += times
// make sure exponent is a multiple of 3
for exponent%3 != 0 {
amount.Mul(amount, bigTen)
exponent--
}
return append(out, amount.String()...), exponent
}
// AsCanonicalBase1024Bytes accepts a buffer to write the base-1024 string value of this field to, and returns
// either that buffer or a larger buffer and the current exponent of the value. 2048 is 2 * 1024 ^ 1 and would
// return []byte("2048"), 1.
func (a infDecAmount) AsCanonicalBase1024Bytes(out []byte) (result []byte, exponent int32) {
tmp := &inf.Dec{}
tmp.Round(a.Dec, 0, inf.RoundUp)
amount, exponent := removeBigIntFactors(tmp.UnscaledBig(), big1024)
return append(out, amount.String()...), exponent
}
@@ -1,221 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2014 The Kubernetes 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 resourcequantity
import (
"testing"
)
func TestInt64AmountAsInt64(t *testing.T) {
for _, test := range []struct {
value int64
scale Scale
result int64
ok bool
}{
{100, 0, 100, true},
{100, 1, 1000, true},
{100, -5, 0, false},
{100, 100, 0, false},
} {
r, ok := int64Amount{value: test.value, scale: test.scale}.AsInt64()
if r != test.result {
t.Errorf("%v: unexpected result: %d", test, r)
}
if ok != test.ok {
t.Errorf("%v: unexpected ok: %t", test, ok)
}
}
}
func TestInt64AmountAdd(t *testing.T) {
for _, test := range []struct {
a, b, c int64Amount
ok bool
}{
{int64Amount{value: 100, scale: 1}, int64Amount{value: 10, scale: 2}, int64Amount{value: 200, scale: 1}, true},
{int64Amount{value: 100, scale: 1}, int64Amount{value: 1, scale: 2}, int64Amount{value: 110, scale: 1}, true},
{int64Amount{value: 100, scale: 1}, int64Amount{value: 1, scale: 100}, int64Amount{value: 1, scale: 100}, false},
{int64Amount{value: -5, scale: 2}, int64Amount{value: 50, scale: 1}, int64Amount{value: 0, scale: 1}, true},
{int64Amount{value: -5, scale: 2}, int64Amount{value: 5, scale: 2}, int64Amount{value: 0, scale: 2}, true},
{int64Amount{value: mostPositive, scale: -1}, int64Amount{value: 1, scale: -1}, int64Amount{value: 0, scale: -1}, false},
{int64Amount{value: mostPositive, scale: -1}, int64Amount{value: 0, scale: -1}, int64Amount{value: mostPositive, scale: -1}, true},
{int64Amount{value: mostPositive / 10, scale: 1}, int64Amount{value: 10, scale: 0}, int64Amount{value: mostPositive, scale: -1}, false},
} {
c := test.a
ok := c.Add(test.b)
if ok != test.ok {
t.Errorf("%v: unexpected ok: %t", test, ok)
}
if ok {
if c != test.c {
t.Errorf("%v: unexpected result: %d", test, c)
}
} else {
if c != test.a {
t.Errorf("%v: overflow addition mutated source: %d", test, c)
}
}
// addition is commutative
c = test.b
if ok := c.Add(test.a); ok != test.ok {
t.Errorf("%v: unexpected ok: %t", test, ok)
}
if ok {
if c != test.c {
t.Errorf("%v: unexpected result: %d", test, c)
}
} else {
if c != test.b {
t.Errorf("%v: overflow addition mutated source: %d", test, c)
}
}
}
}
func TestInt64AmountMul(t *testing.T) {
for _, test := range []struct {
a int64Amount
b int64
c int64Amount
ok bool
}{
{int64Amount{value: 100, scale: 1}, 1000, int64Amount{value: 100000, scale: 1}, true},
{int64Amount{value: 100, scale: -1}, 1000, int64Amount{value: 100000, scale: -1}, true},
{int64Amount{value: 1, scale: 100}, 10, int64Amount{value: 1, scale: 100}, false},
{int64Amount{value: 1, scale: -100}, 10, int64Amount{value: 1, scale: -100}, false},
{int64Amount{value: -5, scale: 2}, 500, int64Amount{value: -2500, scale: 2}, true},
{int64Amount{value: -5, scale: -2}, 500, int64Amount{value: -2500, scale: -2}, true},
{int64Amount{value: 0, scale: 1}, 0, int64Amount{value: 0, scale: 1}, true},
{int64Amount{value: mostPositive, scale: -1}, 10, int64Amount{value: mostPositive, scale: -1}, false},
{int64Amount{value: mostPositive, scale: -1}, 0, int64Amount{value: 0, scale: 0}, true},
{int64Amount{value: mostPositive, scale: 0}, 1, int64Amount{value: mostPositive, scale: 0}, true},
{int64Amount{value: mostPositive / 10, scale: 1}, 10, int64Amount{value: mostPositive / 10, scale: 1}, false},
{int64Amount{value: mostPositive, scale: 0}, -1, int64Amount{value: -mostPositive, scale: 0}, true},
{int64Amount{value: mostNegative, scale: 0}, 1, int64Amount{value: mostNegative, scale: 0}, true},
{int64Amount{value: mostNegative, scale: 1}, 0, int64Amount{value: 0, scale: 0}, true},
{int64Amount{value: mostNegative, scale: 1}, 1, int64Amount{value: mostNegative, scale: 1}, false},
} {
c := test.a
ok := c.Mul(test.b)
if ok && !test.ok {
t.Errorf("unextected success: %v", c)
} else if !ok && test.ok {
t.Errorf("unexpeted failure: %v", c)
} else if ok {
if c != test.c {
t.Errorf("%v: unexpected result: %d", test, c)
}
} else {
if c != test.a {
t.Errorf("%v: overflow multiplication mutated source: %d", test, c)
}
}
}
}
func TestInt64AsCanonicalString(t *testing.T) {
for _, test := range []struct {
value int64
scale Scale
result string
exponent int32
}{
{100, 0, "100", 0},
{100, 1, "1", 3},
{100, -1, "10", 0},
{10800, -10, "1080", -9},
} {
r, exp := int64Amount{value: test.value, scale: test.scale}.AsCanonicalBytes(nil)
if string(r) != test.result {
t.Errorf("%v: unexpected result: %s", test, r)
}
if exp != test.exponent {
t.Errorf("%v: unexpected exponent: %d", test, exp)
}
}
}
func TestAmountSign(t *testing.T) {
table := []struct {
i int64Amount
expect int
}{
{int64Amount{value: -50, scale: 1}, -1},
{int64Amount{value: 0, scale: 1}, 0},
{int64Amount{value: 300, scale: 1}, 1},
{int64Amount{value: -50, scale: -8}, -1},
{int64Amount{value: 50, scale: -8}, 1},
{int64Amount{value: 0, scale: -8}, 0},
{int64Amount{value: -50, scale: 0}, -1},
{int64Amount{value: 50, scale: 0}, 1},
{int64Amount{value: 0, scale: 0}, 0},
}
for _, testCase := range table {
if result := testCase.i.Sign(); result != testCase.expect {
t.Errorf("i: %v, Expected: %v, Actual: %v", testCase.i, testCase.expect, result)
}
}
}
func TestInt64AmountAsScaledInt64(t *testing.T) {
for _, test := range []struct {
name string
i int64Amount
scaled Scale
result int64
ok bool
}{
{"test when i.scale < scaled ", int64Amount{value: 100, scale: 0}, 5, 1, true},
{"test when i.scale = scaled", int64Amount{value: 100, scale: 1}, 1, 100, true},
{"test when i.scale > scaled and result doesn't overflow", int64Amount{value: 100, scale: 5}, 2, 100000, true},
{"test when i.scale > scaled and result overflows", int64Amount{value: 876, scale: 30}, 4, 0, false},
{"test when i.scale < 0 and fraction exists", int64Amount{value: 93, scale: -1}, 0, 10, true},
{"test when i.scale < 0 and fraction doesn't exist", int64Amount{value: 100, scale: -1}, 0, 10, true},
{"test when i.value < 0 and fraction exists", int64Amount{value: -1932, scale: 2}, 4, -20, true},
{"test when i.value < 0 and fraction doesn't exists", int64Amount{value: -1900, scale: 2}, 4, -19, true},
} {
t.Run(test.name, func(t *testing.T) {
r, ok := test.i.AsScaledInt64(test.scaled)
if r != test.result {
t.Errorf("%v: expected result: %d, got result: %d", test.name, test.result, r)
}
if ok != test.ok {
t.Errorf("%v: expected ok: %t, got ok: %t", test.name, test.ok, ok)
}
})
}
}
-34
View File
@@ -1,34 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2022 The Kubernetes 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 resourcequantity has tools for converting resource quantities to and from strings.
package resourcequantity // cloned from "k8s.io/apimachinery/pkg/api/resource"
-326
View File
@@ -1,326 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2014 The Kubernetes 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 resourcequantity
import (
"math/big"
inf "gopkg.in/inf.v0"
)
const (
// maxInt64Factors is the highest value that will be checked when removing factors of 10 from an int64.
// It is also the maximum decimal digits that can be represented with an int64.
maxInt64Factors = 18
)
var (
// Commonly needed big.Int values-- treat as read only!
bigTen = big.NewInt(10)
bigZero = big.NewInt(0)
bigOne = big.NewInt(1)
bigThousand = big.NewInt(1000)
big1024 = big.NewInt(1024)
// Commonly needed inf.Dec values-- treat as read only!
decZero = inf.NewDec(0, 0)
decOne = inf.NewDec(1, 0)
// Largest (in magnitude) number allowed.
maxAllowed = infDecAmount{inf.NewDec((1<<63)-1, 0)} // == max int64
// The maximum value we can represent milli-units for.
// Compare with the return value of Quantity.Value() to
// see if it's safe to use Quantity.MilliValue().
MaxMilliValue = int64(((1 << 63) - 1) / 1000)
)
const mostNegative = -(mostPositive + 1)
const mostPositive = 1<<63 - 1
// int64Add returns a+b, or false if that would overflow int64.
func int64Add(a, b int64) (int64, bool) {
c := a + b
switch {
case a > 0 && b > 0:
if c < 0 {
return 0, false
}
case a < 0 && b < 0:
if c > 0 {
return 0, false
}
if a == mostNegative && b == mostNegative {
return 0, false
}
}
return c, true
}
// int64Multiply returns a*b, or false if that would overflow or underflow int64.
func int64Multiply(a, b int64) (int64, bool) {
if a == 0 || b == 0 || a == 1 || b == 1 {
return a * b, true
}
if a == mostNegative || b == mostNegative {
return 0, false
}
c := a * b
return c, c/b == a
}
// int64MultiplyScale returns a*b, assuming b is greater than one, or false if that would overflow or underflow int64.
// Use when b is known to be greater than one.
func int64MultiplyScale(a int64, b int64) (int64, bool) {
if a == 0 || a == 1 {
return a * b, true
}
if a == mostNegative && b != 1 {
return 0, false
}
c := a * b
return c, c/b == a
}
// int64MultiplyScale10 multiplies a by 10, or returns false if that would overflow. This method is faster than
// int64Multiply(a, 10) because the compiler can optimize constant factor multiplication.
func int64MultiplyScale10(a int64) (int64, bool) {
if a == 0 || a == 1 {
return a * 10, true
}
if a == mostNegative {
return 0, false
}
c := a * 10
return c, c/10 == a
}
// int64MultiplyScale100 multiplies a by 100, or returns false if that would overflow. This method is faster than
// int64Multiply(a, 100) because the compiler can optimize constant factor multiplication.
func int64MultiplyScale100(a int64) (int64, bool) {
if a == 0 || a == 1 {
return a * 100, true
}
if a == mostNegative {
return 0, false
}
c := a * 100
return c, c/100 == a
}
// int64MultiplyScale1000 multiplies a by 1000, or returns false if that would overflow. This method is faster than
// int64Multiply(a, 1000) because the compiler can optimize constant factor multiplication.
func int64MultiplyScale1000(a int64) (int64, bool) {
if a == 0 || a == 1 {
return a * 1000, true
}
if a == mostNegative {
return 0, false
}
c := a * 1000
return c, c/1000 == a
}
// positiveScaleInt64 multiplies base by 10^scale, returning false if the
// value overflows. Passing a negative scale is undefined.
func positiveScaleInt64(base int64, scale Scale) (int64, bool) {
switch scale {
case 0:
return base, true
case 1:
return int64MultiplyScale10(base)
case 2:
return int64MultiplyScale100(base)
case 3:
return int64MultiplyScale1000(base)
case 6:
return int64MultiplyScale(base, 1000000)
case 9:
return int64MultiplyScale(base, 1000000000)
default:
value := base
var ok bool
for range scale {
if value, ok = int64MultiplyScale(value, 10); !ok {
return 0, false
}
}
return value, true
}
}
// negativeScaleInt64 reduces base by the provided scale, rounding up, until the
// value is zero or the scale is reached. Passing a negative scale is undefined.
// The value returned, if not exact, is rounded away from zero.
func negativeScaleInt64(base int64, scale Scale) (result int64, exact bool) {
if scale == 0 {
return base, true
}
value := base
var fraction bool
for range scale {
if !fraction && value%10 != 0 {
fraction = true
}
value = value / 10
if value == 0 {
if fraction {
if base > 0 {
return 1, false
}
return -1, false
}
return 0, true
}
}
if fraction {
if base > 0 {
value++
} else {
value--
}
}
return value, !fraction
}
func pow10Int64(b int64) int64 {
switch b {
case 0:
return 1
case 1:
return 10
case 2:
return 100
case 3:
return 1000
case 4:
return 10000
case 5:
return 100000
case 6:
return 1000000
case 7:
return 10000000
case 8:
return 100000000
case 9:
return 1000000000
case 10:
return 10000000000
case 11:
return 100000000000
case 12:
return 1000000000000
case 13:
return 10000000000000
case 14:
return 100000000000000
case 15:
return 1000000000000000
case 16:
return 10000000000000000
case 17:
return 100000000000000000
case 18:
return 1000000000000000000
default:
return 0
}
}
// negativeScaleInt64 returns the result of dividing base by scale * 10 and the remainder, or
// false if no such division is possible. Dividing by negative scales is undefined.
func divideByScaleInt64(base int64, scale Scale) (result, remainder int64, exact bool) {
if scale == 0 {
return base, 0, true
}
// the max scale representable in base 10 in an int64 is 18 decimal places
if scale >= 18 {
return 0, base, false
}
divisor := pow10Int64(int64(scale))
return base / divisor, base % divisor, true
}
// removeInt64Factors divides in a loop; the return values have the property that
// value == result * base ^ scale
func removeInt64Factors(value int64, base int64) (result int64, times int32) {
times = 0
result = value
negative := result < 0
if negative {
result = -result
}
switch base {
// allow the compiler to optimize the common cases
case 10:
for result >= 10 && result%10 == 0 {
times++
result = result / 10
}
// allow the compiler to optimize the common cases
case 1024:
for result >= 1024 && result%1024 == 0 {
times++
result = result / 1024
}
default:
for result >= base && result%base == 0 {
times++
result = result / base
}
}
if negative {
result = -result
}
return result, times
}
// removeBigIntFactors divides in a loop; the return values have the property that
// d == result * factor ^ times
// d may be modified in place.
// If d == 0, then the return values will be (0, 0)
func removeBigIntFactors(d, factor *big.Int) (result *big.Int, times int32) {
q := big.NewInt(0)
m := big.NewInt(0)
for d.Cmp(bigZero) != 0 {
q.DivMod(d, factor, m)
if m.Cmp(bigZero) != 0 {
break
}
times++
d, q = q, d
}
return d, times
}
-227
View File
@@ -1,227 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2014 The Kubernetes 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 resourcequantity
import (
"testing"
)
func TestDetectOverflowAdd(t *testing.T) {
for _, test := range []struct {
a, b int64
c int64
ok bool
}{
{0, 0, 0, true},
{-1, 1, 0, true},
{0, 1, 1, true},
{2, 2, 4, true},
{2, -2, 0, true},
{-2, -2, -4, true},
{mostNegative, -1, 0, false},
{mostNegative, 1, mostNegative + 1, true},
{mostPositive, -1, mostPositive - 1, true},
{mostPositive, 1, 0, false},
{mostNegative, mostPositive, -1, true},
{mostPositive, mostNegative, -1, true},
{mostPositive, mostPositive, 0, false},
{mostNegative, mostNegative, 0, false},
{-mostPositive, mostNegative, 0, false},
{mostNegative, -mostPositive, 0, false},
{-mostPositive, -mostPositive, 0, false},
} {
c, ok := int64Add(test.a, test.b)
if c != test.c {
t.Errorf("%v: unexpected result: %d", test, c)
}
if ok != test.ok {
t.Errorf("%v: unexpected overflow: %t", test, ok)
}
// addition is commutative
d, ok2 := int64Add(test.b, test.a)
if c != d || ok != ok2 {
t.Errorf("%v: not commutative: %d %t", test, d, ok2)
}
}
}
func TestDetectOverflowMultiply(t *testing.T) {
for _, test := range []struct {
a, b int64
c int64
ok bool
}{
{0, 0, 0, true},
{-1, 1, -1, true},
{-1, -1, 1, true},
{1, 1, 1, true},
{0, 1, 0, true},
{1, 0, 0, true},
{2, 2, 4, true},
{2, -2, -4, true},
{-2, -2, 4, true},
{mostNegative, -1, 0, false},
{mostNegative, 1, mostNegative, true},
{mostPositive, -1, -mostPositive, true},
{mostPositive, 1, mostPositive, true},
{mostNegative, mostPositive, 0, false},
{mostPositive, mostNegative, 0, false},
{mostPositive, mostPositive, 1, false},
{mostNegative, mostNegative, 0, false},
{-mostPositive, mostNegative, 0, false},
{mostNegative, -mostPositive, 0, false},
{-mostPositive, -mostPositive, 1, false},
} {
c, ok := int64Multiply(test.a, test.b)
if c != test.c {
t.Errorf("%v: unexpected result: %d", test, c)
}
if ok != test.ok {
t.Errorf("%v: unexpected overflow: %t", test, ok)
}
// multiplication is commutative
d, ok2 := int64Multiply(test.b, test.a)
if c != d || ok != ok2 {
t.Errorf("%v: not commutative: %d %t", test, d, ok2)
}
}
}
func TestDetectOverflowScale(t *testing.T) {
for _, a := range []int64{0, -1, 1, 10, -10, mostPositive, mostNegative, -mostPositive} {
for _, b := range []int64{1, 2, 10, 100, 1000, mostPositive} {
expect, expectOk := int64Multiply(a, b)
c, ok := int64MultiplyScale(a, b)
if c != expect {
t.Errorf("%d*%d: unexpected result: %d", a, b, c)
}
if ok != expectOk {
t.Errorf("%d*%d: unexpected overflow: %t", a, b, ok)
}
}
for _, test := range []struct {
base int64
fn func(a int64) (int64, bool)
}{
{10, int64MultiplyScale10},
{100, int64MultiplyScale100},
{1000, int64MultiplyScale1000},
} {
expect, expectOk := int64Multiply(a, test.base)
c, ok := test.fn(a)
if c != expect {
t.Errorf("%d*%d: unexpected result: %d", a, test.base, c)
}
if ok != expectOk {
t.Errorf("%d*%d: unexpected overflow: %t", a, test.base, ok)
}
}
}
}
func TestRemoveInt64Factors(t *testing.T) {
for _, test := range []struct {
value int64
max int64
result int64
scale int32
}{
{100, 10, 1, 2},
{100, 10, 1, 2},
{100, 100, 1, 1},
{1, 10, 1, 0},
} {
r, s := removeInt64Factors(test.value, test.max)
if r != test.result {
t.Errorf("%v: unexpected result: %d", test, r)
}
if s != test.scale {
t.Errorf("%v: unexpected scale: %d", test, s)
}
}
}
func TestNegativeScaleInt64(t *testing.T) {
for _, test := range []struct {
base int64
scale Scale
result int64
exact bool
}{
{1234567, 0, 1234567, true},
{1234567, 1, 123457, false},
{1234567, 2, 12346, false},
{1234567, 3, 1235, false},
{1234567, 4, 124, false},
{-1234567, 0, -1234567, true},
{-1234567, 1, -123457, false},
{-1234567, 2, -12346, false},
{-1234567, 3, -1235, false},
{-1234567, 4, -124, false},
{1000, 0, 1000, true},
{1000, 1, 100, true},
{1000, 2, 10, true},
{1000, 3, 1, true},
{1000, 4, 1, false},
{-1000, 0, -1000, true},
{-1000, 1, -100, true},
{-1000, 2, -10, true},
{-1000, 3, -1, true},
{-1000, 4, -1, false},
{0, 0, 0, true},
{0, 1, 0, true},
{0, 2, 0, true},
// negative scale is undefined behavior
{1000, -1, 1000, true},
} {
result, exact := negativeScaleInt64(test.base, test.scale)
if result != test.result {
t.Errorf("%v: unexpected result: %d", test, result)
}
if exact != test.exact {
t.Errorf("%v: unexpected exact: %t", test, exact)
}
}
}
-851
View File
@@ -1,851 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2014 The Kubernetes 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 resourcequantity
import (
"bytes"
"errors"
"fmt"
math "math"
"math/big"
"strconv"
"strings"
inf "gopkg.in/inf.v0"
)
// Quantity is a fixed-point representation of a number.
// It provides convenient marshaling/unmarshaling in JSON and YAML,
// in addition to String() and AsInt64() accessors.
//
// The serialization format is:
//
// ```
// <quantity> ::= <signedNumber><suffix>
//
// (Note that <suffix> may be empty, from the "" case in <decimalSI>.)
//
// <digit> ::= 0 | 1 | ... | 9
// <digits> ::= <digit> | <digit><digits>
// <number> ::= <digits> | <digits>.<digits> | <digits>. | .<digits>
// <sign> ::= "+" | "-"
// <signedNumber> ::= <number> | <sign><number>
// <suffix> ::= <binarySI> | <decimalExponent> | <decimalSI>
// <binarySI> ::= Ki | Mi | Gi | Ti | Pi | Ei
//
// (International System of units; See: http://physics.nist.gov/cuu/Units/binary.html)
//
// <decimalSI> ::= m | "" | k | M | G | T | P | E
//
// (Note that 1024 = 1Ki but 1000 = 1k; I didn't choose the capitalization.)
//
// <decimalExponent> ::= "e" <signedNumber> | "E" <signedNumber>
// ```
//
// No matter which of the three exponent forms is used, no quantity may represent
// a number greater than 2^63-1 in magnitude, nor may it have more than 3 decimal
// places. Numbers larger or more precise will be capped or rounded up.
// (E.g.: 0.1m will rounded up to 1m.)
// This may be extended in the future if we require larger or smaller quantities.
//
// When a Quantity is parsed from a string, it will remember the type of suffix
// it had, and will use the same type again when it is serialized.
//
// Before serializing, Quantity will be put in "canonical form".
// This means that Exponent/suffix will be adjusted up or down (with a
// corresponding increase or decrease in Mantissa) such that:
//
// - No precision is lost
// - No fractional digits will be emitted
// - The exponent (or suffix) is as large as possible.
//
// The sign will be omitted unless the number is negative.
//
// Examples:
//
// - 1.5 will be serialized as "1500m"
// - 1.5Gi will be serialized as "1536Mi"
//
// Note that the quantity will NEVER be internally represented by a
// floating point number. That is the whole point of this exercise.
//
// Non-canonical values will still parse as long as they are well formed,
// but will be re-emitted in their canonical form. (So always use canonical
// form, or don't diff.)
//
// This format is intended to make it difficult to use these numbers without
// writing some sort of special handling code in the hopes that will
// cause implementors to also use a fixed point implementation.
//
// +protobuf=true
// +protobuf.embed=string
// +protobuf.options.marshal=false
// +protobuf.options.(gogoproto.goproto_stringer)=false
// +k8s:deepcopy-gen=true
type Quantity struct {
// i is the quantity in int64 scaled form, if d.Dec == nil
i int64Amount
// d is the quantity in inf.Dec form if d.Dec != nil
d infDecAmount
// s is the generated value of this quantity to avoid recalculation
s string
// Change Format at will. See the comment for Canonicalize for
// more details.
Format
}
// CanonicalValue allows a quantity amount to be converted to a string.
type CanonicalValue interface {
// AsCanonicalBytes returns a byte array representing the string representation
// of the value mantissa and an int32 representing its exponent in base-10. Callers may
// pass a byte slice to the method to avoid allocations.
AsCanonicalBytes(out []byte) ([]byte, int32)
// AsCanonicalBase1024Bytes returns a byte array representing the string representation
// of the value mantissa and an int32 representing its exponent in base-1024. Callers
// may pass a byte slice to the method to avoid allocations.
AsCanonicalBase1024Bytes(out []byte) ([]byte, int32)
}
// Format lists the three possible formattings of a quantity.
type Format string
const (
DecimalExponent = Format("DecimalExponent") // e.g., 12e6
BinarySI = Format("BinarySI") // e.g., 12Mi (12 * 2^20)
DecimalSI = Format("DecimalSI") // e.g., 12M (12 * 10^6)
)
// MustParse turns the given string into a quantity or panics; for tests
// or other cases where you know the string is valid.
func MustParse(str string) Quantity {
q, err := ParseQuantity(str)
if err != nil {
panic(fmt.Errorf("cannot parse '%v': %v", str, err))
}
return q
}
const (
// splitREString is used to separate a number from its suffix; as such,
// this is overly permissive, but that's OK-- it will be checked later.
splitREString = "^([+-]?[0-9.]+)([eEinumkKMGTP]*[-+]?[0-9]*)$"
)
var (
// Errors that could happen while parsing a string.
ErrFormatWrong = errors.New("quantities must match the regular expression '" + splitREString + "'")
ErrNumeric = errors.New("unable to parse numeric part of quantity")
ErrSuffix = errors.New("unable to parse quantity's suffix")
)
// parseQuantityString is a fast scanner for quantity values.
func parseQuantityString(str string) (positive bool, value, num, denom, suffix string, err error) {
positive = true
pos := 0
end := len(str)
// handle leading sign
if pos < end {
switch str[0] {
case '-':
positive = false
pos++
case '+':
pos++
}
}
// strip leading zeros
Zeroes:
for i := pos; ; i++ {
if i >= end {
num = "0"
value = num
return
}
switch str[i] {
case '0':
pos++
default:
break Zeroes
}
}
// extract the numerator
Num:
for i := pos; ; i++ {
if i >= end {
num = str[pos:end]
value = str[0:end]
return
}
switch str[i] {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
default:
num = str[pos:i]
pos = i
break Num
}
}
// if we stripped all numerator positions, always return 0
if len(num) == 0 {
num = "0"
}
// handle a denominator
if pos < end && str[pos] == '.' {
pos++
Denom:
for i := pos; ; i++ {
if i >= end {
denom = str[pos:end]
value = str[0:end]
return
}
switch str[i] {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
default:
denom = str[pos:i]
pos = i
break Denom
}
}
// TODO: we currently allow 1.G, but we may not want to in the future.
// if len(denom) == 0 {
// err = ErrFormatWrong
// return
// }
}
value = str[0:pos]
// grab the elements of the suffix
suffixStart := pos
for i := pos; ; i++ {
if i >= end {
suffix = str[suffixStart:end]
return
}
if !strings.ContainsAny(str[i:i+1], "eEinumkKMGTP") {
pos = i
break
}
}
if pos < end {
switch str[pos] {
case '-', '+':
pos++
}
}
Suffix:
for i := pos; ; i++ {
if i >= end {
suffix = str[suffixStart:end]
return
}
switch str[i] {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
default:
break Suffix
}
}
// we encountered a non decimal in the Suffix loop, but the last character
// was not a valid exponent
err = ErrFormatWrong
return
}
// ParseQuantity turns str into a Quantity, or returns an error.
func ParseQuantity(str string) (Quantity, error) {
if len(str) == 0 {
return Quantity{}, ErrFormatWrong
}
if str == "0" {
return Quantity{Format: DecimalSI, s: str}, nil
}
positive, value, num, denom, suf, err := parseQuantityString(str)
if err != nil {
return Quantity{}, err
}
base, exponent, format, ok := quantitySuffixer.interpret(suffix(suf))
if !ok {
return Quantity{}, ErrSuffix
}
precision := int32(0)
scale := int32(0)
mantissa := int64(1)
switch format {
case DecimalExponent, DecimalSI:
scale = exponent
precision = maxInt64Factors - int32(len(num)+len(denom))
case BinarySI:
scale = 0
switch {
case exponent >= 0 && len(denom) == 0:
// only handle positive binary numbers with the fast path
mantissa = mantissa << uint64(exponent)
// 1Mi (2^20) has ~6 digits of decimal precision, so exponent*3/10 -1 is roughly the precision
precision = 15 - int32(len(num)) - int32(float32(exponent)*3/10) - 1
default:
precision = -1
}
}
if precision >= 0 {
// if we have a denominator, shift the entire value to the left by the number of places in the
// denominator
scale -= int32(len(denom))
if scale >= int32(Nano) {
shifted := num + denom
var value int64
value, err := strconv.ParseInt(shifted, 10, 64)
if err != nil {
return Quantity{}, ErrNumeric
}
if result, ok := int64Multiply(value, mantissa); ok {
if !positive {
result = -result
}
// if the number is in canonical form, reuse the string
switch format {
case BinarySI:
if exponent%10 == 0 && (value&0x07 != 0) {
return Quantity{i: int64Amount{value: result, scale: Scale(scale)}, Format: format, s: str}, nil
}
default:
if scale%3 == 0 && !strings.HasSuffix(shifted, "000") && shifted[0] != '0' {
return Quantity{i: int64Amount{value: result, scale: Scale(scale)}, Format: format, s: str}, nil
}
}
return Quantity{i: int64Amount{value: result, scale: Scale(scale)}, Format: format}, nil
}
}
}
amount := new(inf.Dec)
if _, ok := amount.SetString(value); !ok {
return Quantity{}, ErrNumeric
}
// So that no one but us has to think about suffixes, remove it.
//nolint:staticcheck // QF1003: could use tagged switch on base (staticcheck)
if base == 10 {
amount.SetScale(amount.Scale() + Scale(exponent).infScale())
} else if base == 2 {
// numericSuffix = 2 ** exponent
numericSuffix := big.NewInt(1).Lsh(bigOne, uint(exponent))
ub := amount.UnscaledBig()
amount.SetUnscaledBig(ub.Mul(ub, numericSuffix))
}
// Cap at min/max bounds.
sign := amount.Sign()
if sign == -1 {
amount.Neg(amount)
}
// This rounds non-zero values up to the minimum representable value, under the theory that
// if you want some resources, you should get some resources, even if you asked for way too small
// of an amount. Arguably, this should be inf.RoundHalfUp (normal rounding), but that would have
// the side effect of rounding values < .5n to zero.
if v, ok := amount.Unscaled(); v != int64(0) || !ok {
amount.Round(amount, Nano.infScale(), inf.RoundUp)
}
// The max is just a simple cap.
// TODO: this prevents accumulating quantities greater than int64, for instance quota across a cluster
if format == BinarySI && amount.Cmp(maxAllowed.Dec) > 0 {
amount.Set(maxAllowed.Dec)
}
if format == BinarySI && amount.Cmp(decOne) < 0 && amount.Cmp(decZero) > 0 {
// This avoids rounding and hopefully confusion, too.
format = DecimalSI
}
if sign == -1 {
amount.Neg(amount)
}
return Quantity{d: infDecAmount{amount}, Format: format}, nil
}
// DeepCopy returns a deep-copy of the Quantity value. Note that the method
// receiver is a value, so we can mutate it in-place and return it.
func (q Quantity) DeepCopy() Quantity {
if q.d.Dec != nil {
tmp := &inf.Dec{}
q.d.Dec = tmp.Set(q.d.Dec)
}
return q
}
// CanonicalizeBytes returns the canonical form of q and its suffix (see comment on Quantity).
//
// Note about BinarySI:
// - If q.Format is set to BinarySI and q.Amount represents a non-zero value between
// -1 and +1, it will be emitted as if q.Format were DecimalSI.
// - Otherwise, if q.Format is set to BinarySI, fractional parts of q.Amount will be
// rounded up. (1.1i becomes 2i.)
func (q *Quantity) CanonicalizeBytes(out []byte) (result, suffix []byte) {
if q.IsZero() {
return zeroBytes, nil
}
var rounded CanonicalValue
format := q.Format
switch format {
case DecimalExponent, DecimalSI:
case BinarySI:
if q.CmpInt64(-1024) > 0 && q.CmpInt64(1024) < 0 {
// This avoids rounding and hopefully confusion, too.
format = DecimalSI
} else {
var exact bool
if rounded, exact = q.AsScale(0); !exact {
// Don't lose precision-- show as DecimalSI
format = DecimalSI
}
}
default:
format = DecimalExponent
}
// TODO: If BinarySI formatting is requested but would cause rounding, upgrade to
// one of the other formats.
switch format {
case DecimalExponent, DecimalSI:
number, exponent := q.AsCanonicalBytes(out)
suffix, _ := quantitySuffixer.constructBytes(10, exponent, format)
return number, suffix
default:
// format must be BinarySI
number, exponent := rounded.AsCanonicalBase1024Bytes(out)
suffix, _ := quantitySuffixer.constructBytes(2, exponent*10, format)
return number, suffix
}
}
// AsApproximateFloat64 returns a float64 representation of the quantity which
// may lose precision. If precision matter more than performance, see
// AsFloat64Slow. If the value of the quantity is outside the range of a
// float64 +Inf/-Inf will be returned.
func (q *Quantity) AsApproximateFloat64() float64 {
var base float64
var exponent int
if q.d.Dec != nil {
base, _ = big.NewFloat(0).SetInt(q.d.Dec.UnscaledBig()).Float64()
exponent = int(-q.d.Dec.Scale())
} else {
base = float64(q.i.value)
exponent = int(q.i.scale)
}
if exponent == 0 {
return base
}
return base * math.Pow10(exponent)
}
// AsFloat64Slow returns a float64 representation of the quantity. This is
// more precise than AsApproximateFloat64 but significantly slower. If the
// value of the quantity is outside the range of a float64 +Inf/-Inf will be
// returned.
func (q *Quantity) AsFloat64Slow() float64 {
infDec := q.AsDec()
var absScale int64
if infDec.Scale() < 0 {
absScale = int64(-infDec.Scale())
} else {
absScale = int64(infDec.Scale())
}
pow10AbsScale := big.NewInt(10)
pow10AbsScale = pow10AbsScale.Exp(pow10AbsScale, big.NewInt(absScale), nil)
var resultBigFloat *big.Float
if infDec.Scale() < 0 {
resultBigInt := new(big.Int).Mul(infDec.UnscaledBig(), pow10AbsScale)
resultBigFloat = new(big.Float).SetInt(resultBigInt)
} else {
pow10AbsScaleFloat := new(big.Float).SetInt(pow10AbsScale)
resultBigFloat = new(big.Float).SetInt(infDec.UnscaledBig())
resultBigFloat = resultBigFloat.Quo(resultBigFloat, pow10AbsScaleFloat)
}
result, _ := resultBigFloat.Float64()
return result
}
// AsInt64 returns a representation of the current value as an int64 if a fast conversion
// is possible. If false is returned, callers must use the inf.Dec form of this quantity.
func (q *Quantity) AsInt64() (int64, bool) {
if q.d.Dec != nil {
return 0, false
}
return q.i.AsInt64()
}
// ToDec promotes the quantity in place to use an inf.Dec representation and returns itself.
func (q *Quantity) ToDec() *Quantity {
if q.d.Dec == nil {
q.d.Dec = q.i.AsDec()
q.i = int64Amount{}
}
return q
}
// AsDec returns the quantity as represented by a scaled inf.Dec.
func (q *Quantity) AsDec() *inf.Dec {
if q.d.Dec != nil {
return q.d.Dec
}
q.d.Dec = q.i.AsDec()
q.i = int64Amount{}
return q.d.Dec
}
// AsCanonicalBytes returns the canonical byte representation of this quantity as a mantissa
// and base 10 exponent. The out byte slice may be passed to the method to avoid an extra
// allocation.
func (q *Quantity) AsCanonicalBytes(out []byte) (result []byte, exponent int32) {
if q.d.Dec != nil {
return q.d.AsCanonicalBytes(out)
}
return q.i.AsCanonicalBytes(out)
}
// IsZero returns true if the quantity is equal to zero.
func (q *Quantity) IsZero() bool {
if q.d.Dec != nil {
return q.d.Dec.Sign() == 0
}
return q.i.value == 0
}
// Sign returns 0 if the quantity is zero, -1 if the quantity is less than zero, or 1 if the
// quantity is greater than zero.
func (q *Quantity) Sign() int {
if q.d.Dec != nil {
return q.d.Dec.Sign()
}
return q.i.Sign()
}
// AsScale returns the current value, rounded up to the provided scale, and returns
// false if the scale resulted in a loss of precision.
func (q *Quantity) AsScale(scale Scale) (CanonicalValue, bool) {
if q.d.Dec != nil {
return q.d.AsScale(scale)
}
return q.i.AsScale(scale)
}
// RoundUp updates the quantity to the provided scale, ensuring that the value is at
// least 1. False is returned if the rounding operation resulted in a loss of precision.
// Negative numbers are rounded away from zero (-9 scale 1 rounds to -10).
func (q *Quantity) RoundUp(scale Scale) bool {
if q.d.Dec != nil {
q.s = ""
d, exact := q.d.AsScale(scale)
q.d = d
return exact
}
// avoid clearing the string value if we have already calculated it
if q.i.scale >= scale {
return true
}
q.s = ""
i, exact := q.i.AsScale(scale)
q.i = i
return exact
}
// Add adds the provide y quantity to the current value. If the current value is zero,
// the format of the quantity will be updated to the format of y.
func (q *Quantity) Add(y Quantity) {
q.s = ""
if q.d.Dec == nil && y.d.Dec == nil {
if q.i.value == 0 {
q.Format = y.Format
}
if q.i.Add(y.i) {
return
}
} else if q.IsZero() {
q.Format = y.Format
}
q.ToDec().d.Dec.Add(q.d.Dec, y.AsDec())
}
// Sub subtracts the provided quantity from the current value in place. If the current
// value is zero, the format of the quantity will be updated to the format of y.
func (q *Quantity) Sub(y Quantity) {
q.s = ""
if q.IsZero() {
q.Format = y.Format
}
if q.d.Dec == nil && y.d.Dec == nil && q.i.Sub(y.i) {
return
}
q.ToDec().d.Dec.Sub(q.d.Dec, y.AsDec())
}
// Mul multiplies the provided y to the current value.
// It will return false if the result is inexact. Otherwise, it will return true.
func (q *Quantity) Mul(y int64) bool {
q.s = ""
if q.d.Dec == nil && q.i.Mul(y) {
return true
}
return q.ToDec().d.Dec.Mul(q.d.Dec, inf.NewDec(y, inf.Scale(0))).UnscaledBig().IsInt64()
}
// Cmp returns 0 if the quantity is equal to y, -1 if the quantity is less than y, or 1 if the
// quantity is greater than y.
func (q *Quantity) Cmp(y Quantity) int {
if q.d.Dec == nil && y.d.Dec == nil {
return q.i.Cmp(y.i)
}
return q.AsDec().Cmp(y.AsDec())
}
// CmpInt64 returns 0 if the quantity is equal to y, -1 if the quantity is less than y, or 1 if the
// quantity is greater than y.
func (q *Quantity) CmpInt64(y int64) int {
if q.d.Dec != nil {
return q.d.Dec.Cmp(inf.NewDec(y, inf.Scale(0)))
}
return q.i.Cmp(int64Amount{value: y})
}
// Neg sets quantity to be the negative value of itself.
func (q *Quantity) Neg() {
q.s = ""
if q.d.Dec == nil {
q.i.value = -q.i.value
return
}
q.d.Dec.Neg(q.d.Dec)
}
// Equal checks equality of two Quantities. This is useful for testing with
// cmp.Equal.
func (q Quantity) Equal(v Quantity) bool {
return q.Cmp(v) == 0
}
// int64QuantityExpectedBytes is the expected width in bytes of the canonical string representation
// of most Quantity values.
const int64QuantityExpectedBytes = 18
// String formats the Quantity as a string, caching the result if not calculated.
// String is an expensive operation and caching this result significantly reduces the cost of
// normal parse / marshal operations on Quantity.
func (q *Quantity) String() string {
if q == nil {
return "<nil>"
}
if len(q.s) == 0 {
result := make([]byte, 0, int64QuantityExpectedBytes)
number, suffix := q.CanonicalizeBytes(result)
number = append(number, suffix...)
q.s = string(number)
}
return q.s
}
// MarshalJSON implements the json.Marshaller interface.
func (q Quantity) MarshalJSON() ([]byte, error) {
if len(q.s) > 0 {
out := make([]byte, len(q.s)+2)
out[0], out[len(out)-1] = '"', '"'
copy(out[1:], q.s)
return out, nil
}
result := make([]byte, int64QuantityExpectedBytes)
result[0] = '"'
number, suffix := q.CanonicalizeBytes(result[1:1])
// if the same slice was returned to us that we passed in, avoid another allocation by copying number into
// the source slice and returning that
if len(number) > 0 && &number[0] == &result[1] && (len(number)+len(suffix)+2) <= int64QuantityExpectedBytes {
number = append(number, suffix...)
number = append(number, '"')
return result[:1+len(number)], nil
}
// if CanonicalizeBytes needed more space than our slice provided, we may need to allocate again so use
// append
result = result[:1]
result = append(result, number...)
result = append(result, suffix...)
result = append(result, '"')
return result, nil
}
// ToUnstructured implements the value.UnstructuredConverter interface.
func (q Quantity) ToUnstructured() any {
return q.String()
}
// UnmarshalJSON implements the json.Unmarshaller interface.
// TODO: Remove support for leading/trailing whitespace
func (q *Quantity) UnmarshalJSON(value []byte) error {
l := len(value)
if l == 4 && bytes.Equal(value, []byte("null")) {
q.d.Dec = nil
q.i = int64Amount{}
return nil
}
if l >= 2 && value[0] == '"' && value[l-1] == '"' {
value = value[1 : l-1]
}
parsed, err := ParseQuantity(strings.TrimSpace(string(value)))
if err != nil {
return err
}
// This copy is safe because parsed will not be referred to again.
*q = parsed
return nil
}
// NewDecimalQuantity returns a new Quantity representing the given
// value in the given format.
func NewDecimalQuantity(b inf.Dec, format Format) *Quantity {
return &Quantity{
d: infDecAmount{&b},
Format: format,
}
}
// NewQuantity returns a new Quantity representing the given
// value in the given format.
func NewQuantity(value int64, format Format) *Quantity {
return &Quantity{
i: int64Amount{value: value},
Format: format,
}
}
// NewMilliQuantity returns a new Quantity representing the given
// value * 1/1000 in the given format. Note that BinarySI formatting
// will round fractional values, and will be changed to DecimalSI for
// values x where (-1 < x < 1) && (x != 0).
func NewMilliQuantity(value int64, format Format) *Quantity {
return &Quantity{
i: int64Amount{value: value, scale: -3},
Format: format,
}
}
// NewScaledQuantity returns a new Quantity representing the given
// value * 10^scale in DecimalSI format.
func NewScaledQuantity(value int64, scale Scale) *Quantity {
return &Quantity{
i: int64Amount{value: value, scale: scale},
Format: DecimalSI,
}
}
// Value returns the unscaled value of q rounded up to the nearest integer away from 0.
func (q *Quantity) Value() int64 {
return q.ScaledValue(0)
}
// MilliValue returns the value of ceil(q * 1000); this could overflow an int64;
// if that's a concern, call Value() first to verify the number is small enough.
func (q *Quantity) MilliValue() int64 {
return q.ScaledValue(Milli)
}
// ScaledValue returns the value of ceil(q / 10^scale).
// For example, NewQuantity(1, DecimalSI).ScaledValue(Milli) returns 1000.
// This could overflow an int64.
// To detect overflow, call Value() first and verify the expected magnitude.
func (q *Quantity) ScaledValue(scale Scale) int64 {
if q.d.Dec == nil {
i, _ := q.i.AsScaledInt64(scale)
return i
}
dec := q.d.Dec
return scaledValue(dec.UnscaledBig(), int(dec.Scale()), int(scale.infScale()))
}
// Set sets q's value to be value.
func (q *Quantity) Set(value int64) {
q.SetScaled(value, 0)
}
// SetMilli sets q's value to be value * 1/1000.
func (q *Quantity) SetMilli(value int64) {
q.SetScaled(value, Milli)
}
// SetScaled sets q's value to be value * 10^scale
func (q *Quantity) SetScaled(value int64, scale Scale) {
q.s = ""
q.d.Dec = nil
q.i = int64Amount{value: value, scale: scale}
}
// QuantityValue makes it possible to use a Quantity as value for a command
// line parameter.
//
// +protobuf=true
// +protobuf.embed=string
// +protobuf.options.marshal=false
// +protobuf.options.(gogoproto.goproto_stringer)=false
// +k8s:deepcopy-gen=true
type QuantityValue struct {
Quantity
}
// Set implements pflag.Value.Set and Go flag.Value.Set.
func (q *QuantityValue) Set(s string) error {
quantity, err := ParseQuantity(s)
if err != nil {
return err
}
q.Quantity = quantity
return nil
}
// Type implements pflag.Value.Type.
func (q QuantityValue) Type() string {
return "quantity"
}
@@ -1,75 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2014 The Kubernetes 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 resourcequantity_test
import (
"fmt"
resource "github.com/containerd/containerd/v2/internal/cri/resourcequantity"
)
func ExampleFormat() {
memorySize := resource.NewQuantity(5*1024*1024*1024, resource.BinarySI)
fmt.Printf("memorySize = %v\n", memorySize)
diskSize := resource.NewQuantity(5*1000*1000*1000, resource.DecimalSI)
fmt.Printf("diskSize = %v\n", diskSize)
cores := resource.NewMilliQuantity(5300, resource.DecimalSI)
fmt.Printf("cores = %v\n", cores)
// Output:
// memorySize = 5Gi
// diskSize = 5G
// cores = 5300m
}
func ExampleMustParse() {
memorySize := resource.MustParse("5Gi")
fmt.Printf("memorySize = %v (%v)\n", memorySize.Value(), memorySize.Format)
diskSize := resource.MustParse("5G")
fmt.Printf("diskSize = %v (%v)\n", diskSize.Value(), diskSize.Format)
cores := resource.MustParse("5300m")
fmt.Printf("milliCores = %v (%v)\n", cores.MilliValue(), cores.Format)
cores2 := resource.MustParse("5.4")
fmt.Printf("milliCores = %v (%v)\n", cores2.MilliValue(), cores2.Format)
// Output:
// memorySize = 5368709120 (BinarySI)
// diskSize = 5000000000 (DecimalSI)
// milliCores = 5300 (DecimalSI)
// milliCores = 5400 (DecimalSI)
}
File diff suppressed because it is too large. Load diff
-111
View File
@@ -1,111 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2015 The Kubernetes 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 resourcequantity
import (
"math"
"math/big"
"sync"
)
var (
// A sync pool to reduce allocation.
intPool sync.Pool
maxInt64 = big.NewInt(math.MaxInt64)
)
func init() {
intPool.New = func() any {
return &big.Int{}
}
}
// scaledValue scales given unscaled value from scale to new Scale and returns
// an int64. It ALWAYS rounds up the result when scale down. The final result might
// overflow.
//
// scale, newScale represents the scale of the unscaled decimal.
// The mathematical value of the decimal is unscaled * 10**(-scale).
func scaledValue(unscaled *big.Int, scale, newScale int) int64 {
dif := scale - newScale
if dif == 0 {
return unscaled.Int64()
}
// Handle scale up
// This is an easy case, we do not need to care about rounding and overflow.
// If any intermediate operation causes overflow, the result will overflow.
if dif < 0 {
return unscaled.Int64() * int64(math.Pow10(-dif))
}
// Handle scale down
// We have to be careful about the intermediate operations.
// fast path when unscaled < max.Int64 and exp(10,dif) < max.Int64
const log10MaxInt64 = 19
if unscaled.Cmp(maxInt64) < 0 && dif < log10MaxInt64 {
divide := int64(math.Pow10(dif))
result := unscaled.Int64() / divide
mod := unscaled.Int64() % divide
if mod != 0 {
return result + 1
}
return result
}
// We should only convert back to int64 when getting the result.
divisor := intPool.Get().(*big.Int)
exp := intPool.Get().(*big.Int)
result := intPool.Get().(*big.Int)
defer func() {
intPool.Put(divisor)
intPool.Put(exp)
intPool.Put(result)
}()
// divisor = 10^(dif)
// TODO: create loop up table if exp costs too much.
divisor.Exp(bigTen, exp.SetInt64(int64(dif)), nil)
// reuse exp
remainder := exp
// result = unscaled / divisor
// remainder = unscaled % divisor
result.DivMod(unscaled, divisor, remainder)
if remainder.Sign() != 0 {
return result.Int64() + 1
}
return result.Int64()
}
@@ -1,101 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2015 The Kubernetes 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 resourcequantity
import (
"math"
"math/big"
"testing"
)
func TestScaledValueInternal(t *testing.T) {
tests := []struct {
unscaled *big.Int
scale int
newScale int
want int64
}{
// remain scale
{big.NewInt(1000), 0, 0, 1000},
// scale down
{big.NewInt(1000), 0, -3, 1},
{big.NewInt(1000), 3, 0, 1},
{big.NewInt(0), 3, 0, 0},
// always round up
{big.NewInt(999), 3, 0, 1},
{big.NewInt(500), 3, 0, 1},
{big.NewInt(499), 3, 0, 1},
{big.NewInt(1), 3, 0, 1},
// large scaled value does not lose precision
{big.NewInt(0).Sub(maxInt64, bigOne), 1, 0, (math.MaxInt64-1)/10 + 1},
// large intermediate result.
{big.NewInt(1).Exp(big.NewInt(10), big.NewInt(100), nil), 100, 0, 1},
// scale up
{big.NewInt(0), 0, 3, 0},
{big.NewInt(1), 0, 3, 1000},
{big.NewInt(1), -3, 0, 1000},
{big.NewInt(1000), -3, 2, 100000000},
{big.NewInt(0).Div(big.NewInt(math.MaxInt64), bigThousand), 0, 3,
(math.MaxInt64 / 1000) * 1000},
}
for i, tt := range tests {
old := (&big.Int{}).Set(tt.unscaled)
got := scaledValue(tt.unscaled, tt.scale, tt.newScale)
if got != tt.want {
t.Errorf("#%d: got = %v, want %v", i, got, tt.want)
}
if tt.unscaled.Cmp(old) != 0 {
t.Errorf("#%d: unscaled = %v, want %v", i, tt.unscaled, old)
}
}
}
func BenchmarkScaledValueSmall(b *testing.B) {
s := big.NewInt(1000)
for i := 0; i < b.N; i++ {
scaledValue(s, 3, 0)
}
}
func BenchmarkScaledValueLarge(b *testing.B) {
s := big.NewInt(math.MaxInt64)
s.Mul(s, big.NewInt(1000))
for i := 0; i < b.N; i++ {
scaledValue(s, 10, 0)
}
}
-214
View File
@@ -1,214 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2014 The Kubernetes 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 resourcequantity
import (
"strconv"
)
type suffix string
// suffixer can interpret and construct suffixes.
type suffixer interface {
interpret(suffix) (base, exponent int32, fmt Format, ok bool)
construct(base, exponent int32, fmt Format) (s suffix, ok bool)
constructBytes(base, exponent int32, fmt Format) (s []byte, ok bool)
}
// quantitySuffixer handles suffixes for all three formats that quantity
// can handle.
var quantitySuffixer = newSuffixer()
type bePair struct {
base, exponent int32
}
type listSuffixer struct {
suffixToBE map[suffix]bePair
beToSuffix map[bePair]suffix
beToSuffixBytes map[bePair][]byte
}
func (ls *listSuffixer) addSuffix(s suffix, pair bePair) {
if ls.suffixToBE == nil {
ls.suffixToBE = map[suffix]bePair{}
}
if ls.beToSuffix == nil {
ls.beToSuffix = map[bePair]suffix{}
}
if ls.beToSuffixBytes == nil {
ls.beToSuffixBytes = map[bePair][]byte{}
}
ls.suffixToBE[s] = pair
ls.beToSuffix[pair] = s
ls.beToSuffixBytes[pair] = []byte(s)
}
func (ls *listSuffixer) lookup(s suffix) (base, exponent int32, ok bool) {
pair, ok := ls.suffixToBE[s]
if !ok {
return 0, 0, false
}
return pair.base, pair.exponent, true
}
func (ls *listSuffixer) construct(base, exponent int32) (s suffix, ok bool) {
s, ok = ls.beToSuffix[bePair{base, exponent}]
return
}
func (ls *listSuffixer) constructBytes(base, exponent int32) (s []byte, ok bool) {
s, ok = ls.beToSuffixBytes[bePair{base, exponent}]
return
}
type suffixHandler struct {
decSuffixes listSuffixer
binSuffixes listSuffixer
}
type fastLookup struct {
*suffixHandler
}
func (l fastLookup) interpret(s suffix) (base, exponent int32, format Format, ok bool) {
switch s {
case "":
return 10, 0, DecimalSI, true
case "n":
return 10, -9, DecimalSI, true
case "u":
return 10, -6, DecimalSI, true
case "m":
return 10, -3, DecimalSI, true
case "k":
return 10, 3, DecimalSI, true
case "M":
return 10, 6, DecimalSI, true
case "G":
return 10, 9, DecimalSI, true
}
return l.suffixHandler.interpret(s)
}
func newSuffixer() suffixer {
sh := &suffixHandler{}
// IMPORTANT: if you change this section you must change fastLookup
sh.binSuffixes.addSuffix("Ki", bePair{2, 10})
sh.binSuffixes.addSuffix("Mi", bePair{2, 20})
sh.binSuffixes.addSuffix("Gi", bePair{2, 30})
sh.binSuffixes.addSuffix("Ti", bePair{2, 40})
sh.binSuffixes.addSuffix("Pi", bePair{2, 50})
sh.binSuffixes.addSuffix("Ei", bePair{2, 60})
// Don't emit an error when trying to produce
// a suffix for 2^0.
sh.decSuffixes.addSuffix("", bePair{2, 0})
sh.decSuffixes.addSuffix("n", bePair{10, -9})
sh.decSuffixes.addSuffix("u", bePair{10, -6})
sh.decSuffixes.addSuffix("m", bePair{10, -3})
sh.decSuffixes.addSuffix("", bePair{10, 0})
sh.decSuffixes.addSuffix("k", bePair{10, 3})
sh.decSuffixes.addSuffix("M", bePair{10, 6})
sh.decSuffixes.addSuffix("G", bePair{10, 9})
sh.decSuffixes.addSuffix("T", bePair{10, 12})
sh.decSuffixes.addSuffix("P", bePair{10, 15})
sh.decSuffixes.addSuffix("E", bePair{10, 18})
return fastLookup{sh}
}
func (sh *suffixHandler) construct(base, exponent int32, fmt Format) (s suffix, ok bool) {
switch fmt {
case DecimalSI:
return sh.decSuffixes.construct(base, exponent)
case BinarySI:
return sh.binSuffixes.construct(base, exponent)
case DecimalExponent:
if base != 10 {
return "", false
}
if exponent == 0 {
return "", true
}
return suffix("e" + strconv.FormatInt(int64(exponent), 10)), true
}
return "", false
}
func (sh *suffixHandler) constructBytes(base, exponent int32, format Format) (s []byte, ok bool) {
switch format {
case DecimalSI:
return sh.decSuffixes.constructBytes(base, exponent)
case BinarySI:
return sh.binSuffixes.constructBytes(base, exponent)
case DecimalExponent:
if base != 10 {
return nil, false
}
if exponent == 0 {
return nil, true
}
result := make([]byte, 8)
result[0] = 'e'
number := strconv.AppendInt(result[1:1], int64(exponent), 10)
if &result[1] == &number[0] {
return result[:1+len(number)], true
}
result = append(result[:1], number...)
return result, true
}
return nil, false
}
func (sh *suffixHandler) interpret(suffix suffix) (base, exponent int32, fmt Format, ok bool) {
// Try lookup tables first
if b, e, ok := sh.decSuffixes.lookup(suffix); ok {
return b, e, DecimalSI, true
}
if b, e, ok := sh.binSuffixes.lookup(suffix); ok {
return b, e, BinarySI, true
}
if len(suffix) > 1 && (suffix[0] == 'E' || suffix[0] == 'e') {
parsed, err := strconv.ParseInt(string(suffix[1:]), 10, 32)
if err != nil {
return 0, 0, DecimalExponent, false
}
return 10, int32(parsed), DecimalExponent, true
}
return 0, 0, DecimalExponent, false
}
@@ -39,10 +39,10 @@ import (
"github.com/containerd/containerd/v2/core/images"
"github.com/containerd/containerd/v2/core/mount"
"github.com/containerd/containerd/v2/internal/cri/annotations"
crilabels "github.com/containerd/containerd/v2/internal/cri/labels"
containerstore "github.com/containerd/containerd/v2/internal/cri/store/container"
imagestore "github.com/containerd/containerd/v2/internal/cri/store/image"
"github.com/containerd/containerd/v2/internal/cri/store/sandbox"
critypes "github.com/containerd/containerd/v2/internal/cri/types"
"github.com/containerd/containerd/v2/pkg/archive"
"github.com/containerd/containerd/v2/pkg/protobuf/proto"
ptypes "github.com/containerd/containerd/v2/pkg/protobuf/types"
@@ -261,11 +261,11 @@ func (c *criService) CRImportCheckpoint(
sandboxUID := sandboxConfig.GetMetadata().GetUid()
if sandboxUID != "" {
if _, ok := originalLabels[critypes.KubernetesPodUIDLabel]; ok {
originalLabels[critypes.KubernetesPodUIDLabel] = sandboxUID
if _, ok := originalLabels[crilabels.KubernetesPodUIDLabel]; ok {
originalLabels[crilabels.KubernetesPodUIDLabel] = sandboxUID
}
if _, ok := originalAnnotations[critypes.KubernetesPodUIDLabel]; ok {
originalAnnotations[critypes.KubernetesPodUIDLabel] = sandboxUID
if _, ok := originalAnnotations[crilabels.KubernetesPodUIDLabel]; ok {
originalAnnotations[crilabels.KubernetesPodUIDLabel] = sandboxUID
}
}
@@ -273,11 +273,11 @@ func (c *criService) CRImportCheckpoint(
fixupLabels := []string{
// Update the container name. It has already been update in metadata.Name.
// It also needs to be updated in the container labels.
critypes.KubernetesContainerNameLabel,
crilabels.KubernetesContainerNameLabel,
// Update pod name in the labels.
critypes.KubernetesPodNameLabel,
crilabels.KubernetesPodNameLabel,
// Also update namespace.
critypes.KubernetesPodNamespaceLabel,
crilabels.KubernetesPodNamespaceLabel,
}
for _, annotation := range fixupLabels {
+1 -1
View File
@@ -28,8 +28,8 @@ import (
eventtypes "github.com/containerd/containerd/api/events"
"github.com/containerd/containerd/v2/core/events"
"github.com/containerd/containerd/v2/internal/cri/clock"
"github.com/containerd/containerd/v2/internal/cri/constants"
"k8s.io/utils/clock"
)
const (
+1 -1
View File
@@ -21,11 +21,11 @@ import (
"time"
eventtypes "github.com/containerd/containerd/api/events"
testingclock "github.com/containerd/containerd/v2/internal/cri/clock/testing"
"github.com/containerd/containerd/v2/pkg/protobuf/prototestutil"
"github.com/containerd/typeurl/v2"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
testingclock "k8s.io/utils/clock/testing"
)
type noopEventHandler struct {
@@ -27,7 +27,6 @@ import (
"github.com/containerd/containerd/v2/core/containers"
"github.com/containerd/containerd/v2/core/sandbox"
"github.com/containerd/containerd/v2/internal/cri/server/podsandbox/types"
critypes "github.com/containerd/containerd/v2/internal/cri/types"
"github.com/containerd/errdefs"
)
@@ -60,7 +59,7 @@ func (c *Controller) Status(ctx context.Context, sandboxID string, verbose bool)
// toCRISandboxInfo converts internal container object information to CRI sandbox status response info map.
func toCRISandboxInfo(ctx context.Context, sb *types.PodSandbox) (map[string]string, error) {
si := &critypes.SandboxInfo{
si := &types.SandboxInfo{
Pid: sb.Status.Get().Pid,
Config: sb.Metadata.Config,
CNIResult: sb.Metadata.CNIResult,
@@ -21,7 +21,7 @@ import (
"github.com/opencontainers/runtime-spec/specs-go"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
"github.com/containerd/containerd/v2/internal/cri/store/sandbox"
sandboxstore "github.com/containerd/containerd/v2/internal/cri/store/sandbox"
)
// SandboxInfo is extra information for sandbox.
@@ -39,7 +39,7 @@ type SandboxInfo struct {
// Note: RuntimeSpec may not be populated if the sandbox has not been fully created.
RuntimeSpec *specs.Spec `json:"runtimeSpec"`
CNIResult *cni.Result `json:"cniResult"`
Metadata *sandbox.Metadata `json:"sandboxMetadata"`
Metadata *sandboxstore.Metadata `json:"sandboxMetadata"`
Overhead *runtime.ContainerResources `json:"overhead,omitempty"`
Resources *runtime.ContainerResources `json:"resources,omitempty"`
}
+3 -3
View File
@@ -24,8 +24,8 @@ import (
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
podsandboxtypes "github.com/containerd/containerd/v2/internal/cri/server/podsandbox/types"
sandboxstore "github.com/containerd/containerd/v2/internal/cri/store/sandbox"
"github.com/containerd/containerd/v2/internal/cri/types"
"github.com/containerd/errdefs"
)
@@ -115,7 +115,7 @@ func (c *criService) getIPs(sandbox sandboxstore.Sandbox) (string, []string, err
// setUpdatedResources sets updated pod sandbox resources in the sandbox info.
func setUpdatedResources(ctx context.Context, sandbox sandboxstore.Sandbox, info map[string]string) error {
var sbInfo types.SandboxInfo
var sbInfo podsandboxtypes.SandboxInfo
if info == nil {
return nil
}
@@ -178,7 +178,7 @@ func toCRISandboxStatus(meta sandboxstore.Metadata, status string, createdAt tim
// but if controller.Status() returns a NotFound error,
// we should fallback to get SandboxInfo from cached sandbox itself.
func toDeletedCRISandboxInfo(sandbox sandboxstore.Sandbox) (map[string]string, error) {
si := &types.SandboxInfo{
si := &podsandboxtypes.SandboxInfo{
Pid: sandbox.Status.Get().Pid,
Config: sandbox.Config,
CNIResult: sandbox.CNIResult,
+2 -2
View File
@@ -25,8 +25,8 @@ import (
"github.com/stretchr/testify/assert"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
podsandboxtypes "github.com/containerd/containerd/v2/internal/cri/server/podsandbox/types"
sandboxstore "github.com/containerd/containerd/v2/internal/cri/store/sandbox"
"github.com/containerd/containerd/v2/internal/cri/types"
)
func TestPodSandboxStatus(t *testing.T) {
@@ -204,7 +204,7 @@ func TestSetUpdatedResources(t *testing.T) {
return
}
var info types.SandboxInfo
var info podsandboxtypes.SandboxInfo
err = json.Unmarshal([]byte(test.info["info"]), &info)
assert.NoError(t, err)
+1 -1
View File
@@ -32,6 +32,7 @@ import (
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/runtime-spec/specs-go/features"
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
streaming "k8s.io/cri-streaming/pkg/streaming"
apitypes "github.com/containerd/containerd/api/types"
@@ -56,7 +57,6 @@ import (
"github.com/containerd/containerd/v2/pkg/oci"
osinterface "github.com/containerd/containerd/v2/pkg/os"
"github.com/containerd/containerd/v2/plugins"
streaming "k8s.io/cri-streaming/pkg/streaming"
)
var kernelSupportsRRO bool
+3 -4
View File
@@ -22,12 +22,11 @@ import (
"io"
"math"
remotecommand "k8s.io/cri-streaming/pkg/streaming/remotecommand"
"k8s.io/streaming/pkg/runtime"
executil "github.com/containerd/containerd/v2/internal/cri/executil"
ctrdutil "github.com/containerd/containerd/v2/internal/cri/util"
streaming "k8s.io/cri-streaming/pkg/streaming"
remotecommand "k8s.io/cri-streaming/pkg/streaming/remotecommand"
"k8s.io/streaming/pkg/runtime"
executil "k8s.io/utils/exec"
)
type streamRuntime struct {
-35
View File
@@ -1,35 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2022 The Kubernetes 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 setutils has generic set and specified sets. Generic set will
// replace specified ones over time. And specific ones are deprecated.
package setutils // cloned from "k8s.io/apimachinery/pkg/util/sets"
-37
View File
@@ -1,37 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2022 The Kubernetes 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 setutils
// Empty is public since it is used by some internal API objects for conversions between external
// string arrays and internal sets, and conversion logic requires public types today.
type Empty struct{}
-245
View File
@@ -1,245 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2022 The Kubernetes 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 setutils
import (
"cmp"
"slices"
"sort"
)
// Set is a set of the same type elements, implemented via map[comparable]struct{} for minimal memory consumption.
type Set[T comparable] map[T]Empty
// New creates a Set from a list of values.
// NOTE: type param must be explicitly instantiated if given items are empty.
func New[T comparable](items ...T) Set[T] {
ss := make(Set[T], len(items))
ss.Insert(items...)
return ss
}
// KeySet creates a Set from a keys of a map[comparable](? extends interface{}).
// If the value passed in is not actually a map, this will panic.
func KeySet[T comparable, V any](theMap map[T]V) Set[T] {
ret := make(Set[T], len(theMap))
for keyValue := range theMap {
ret.Insert(keyValue)
}
return ret
}
// Insert adds items to the set.
func (s Set[T]) Insert(items ...T) Set[T] {
for _, item := range items {
s[item] = Empty{}
}
return s
}
func Insert[T comparable](set Set[T], items ...T) Set[T] {
return set.Insert(items...)
}
// Delete removes all items from the set.
func (s Set[T]) Delete(items ...T) Set[T] {
for _, item := range items {
delete(s, item)
}
return s
}
// Clear empties the set.
// It is preferable to replace the set with a newly constructed set,
// but not all callers can do that (when there are other references to the map).
func (s Set[T]) Clear() Set[T] {
clear(s)
return s
}
// Has returns true if and only if item is contained in the set.
func (s Set[T]) Has(item T) bool {
_, contained := s[item]
return contained
}
// HasAll returns true if and only if all items are contained in the set.
func (s Set[T]) HasAll(items ...T) bool {
for _, item := range items {
if !s.Has(item) {
return false
}
}
return true
}
// HasAny returns true if any items are contained in the set.
func (s Set[T]) HasAny(items ...T) bool {
return slices.ContainsFunc(items, s.Has)
}
// Clone returns a new set which is a copy of the current set.
func (s Set[T]) Clone() Set[T] {
result := make(Set[T], len(s))
for key := range s {
result.Insert(key)
}
return result
}
// Difference returns a set of objects that are not in s2.
// For example:
// s1 = {a1, a2, a3}
// s2 = {a1, a2, a4, a5}
// s1.Difference(s2) = {a3}
// s2.Difference(s1) = {a4, a5}
func (s Set[T]) Difference(s2 Set[T]) Set[T] {
result := New[T]()
for key := range s {
if !s2.Has(key) {
result.Insert(key)
}
}
return result
}
// SymmetricDifference returns a set of elements which are in either of the sets, but not in their intersection.
// For example:
// s1 = {a1, a2, a3}
// s2 = {a1, a2, a4, a5}
// s1.SymmetricDifference(s2) = {a3, a4, a5}
// s2.SymmetricDifference(s1) = {a3, a4, a5}
func (s Set[T]) SymmetricDifference(s2 Set[T]) Set[T] {
return s.Difference(s2).Union(s2.Difference(s))
}
// Union returns a new set which includes items in either s1 or s2.
// For example:
// s1 = {a1, a2}
// s2 = {a3, a4}
// s1.Union(s2) = {a1, a2, a3, a4}
// s2.Union(s1) = {a1, a2, a3, a4}
func (s Set[T]) Union(s2 Set[T]) Set[T] {
result := s.Clone()
for key := range s2 {
result.Insert(key)
}
return result
}
// Intersection returns a new set which includes the item in BOTH s1 and s2
// For example:
// s1 = {a1, a2}
// s2 = {a2, a3}
// s1.Intersection(s2) = {a2}
func (s Set[T]) Intersection(s2 Set[T]) Set[T] {
var walk, other Set[T]
result := New[T]()
if s.Len() < s2.Len() {
walk = s
other = s2
} else {
walk = s2
other = s
}
for key := range walk {
if other.Has(key) {
result.Insert(key)
}
}
return result
}
// IsSuperset returns true if and only if s1 is a superset of s2.
func (s Set[T]) IsSuperset(s2 Set[T]) bool {
for item := range s2 {
if !s.Has(item) {
return false
}
}
return true
}
// Equal returns true if and only if s1 is equal (as a set) to s2.
// Two sets are equal if their membership is identical.
// (In practice, this means same elements, order doesn't matter)
func (s Set[T]) Equal(s2 Set[T]) bool {
return len(s) == len(s2) && s.IsSuperset(s2)
}
type sortableSliceOfGeneric[T cmp.Ordered] []T
func (g sortableSliceOfGeneric[T]) Len() int { return len(g) }
func (g sortableSliceOfGeneric[T]) Less(i, j int) bool { return less[T](g[i], g[j]) }
func (g sortableSliceOfGeneric[T]) Swap(i, j int) { g[i], g[j] = g[j], g[i] }
// List returns the contents as a sorted T slice.
//
// This is a separate function and not a method because not all types supported
// by Generic are ordered and only those can be sorted.
func List[T cmp.Ordered](s Set[T]) []T {
res := make(sortableSliceOfGeneric[T], 0, len(s))
for key := range s {
res = append(res, key)
}
sort.Sort(res)
return res
}
// UnsortedList returns the slice with contents in random order.
func (s Set[T]) UnsortedList() []T {
res := make([]T, 0, len(s))
for key := range s {
res = append(res, key)
}
return res
}
// PopAny returns a single element from the set.
func (s Set[T]) PopAny() (T, bool) {
for key := range s {
s.Delete(key)
return key, true
}
var zeroValue T
return zeroValue, false
}
// Len returns the size of the set.
func (s Set[T]) Len() int {
return len(s)
}
func less[T cmp.Ordered](lhs, rhs T) bool {
return lhs < rhs
}
+5 -5
View File
@@ -26,7 +26,6 @@ import (
"github.com/containerd/containerd/v2/core/images"
"github.com/containerd/containerd/v2/core/images/usage"
"github.com/containerd/containerd/v2/internal/cri/labels"
"github.com/containerd/containerd/v2/internal/cri/setutils"
"github.com/containerd/containerd/v2/internal/cri/util"
"github.com/containerd/errdefs"
"github.com/containerd/platforms"
@@ -36,6 +35,7 @@ import (
"github.com/opencontainers/go-digest/digestset"
imageidentity "github.com/opencontainers/image-spec/identity"
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
"k8s.io/apimachinery/pkg/util/sets"
)
// Image contains all resources associated with the image. All fields
@@ -90,7 +90,7 @@ func NewStore(img Getter, provider content.InfoReaderProvider, platform platform
store: &store{
images: make(map[string]Image),
digestSet: digestset.NewSet(),
pinnedRefs: make(map[string]setutils.Set[string]),
pinnedRefs: make(map[string]sets.Set[string]),
},
}
}
@@ -215,7 +215,7 @@ type store struct {
lock sync.RWMutex
images map[string]Image
digestSet *digestset.Set
pinnedRefs map[string]setutils.Set[string]
pinnedRefs map[string]sets.Set[string]
}
func (s *store) list() []Image {
@@ -242,7 +242,7 @@ func (s *store) add(img Image) error {
if img.Pinned {
if refs := s.pinnedRefs[img.ID]; refs == nil {
s.pinnedRefs[img.ID] = setutils.New(img.References...)
s.pinnedRefs[img.ID] = sets.New(img.References...)
} else {
refs.Insert(img.References...)
}
@@ -288,7 +288,7 @@ func (s *store) pin(id, ref string) error {
}
if refs := s.pinnedRefs[digest.String()]; refs == nil {
s.pinnedRefs[digest.String()] = setutils.New(ref)
s.pinnedRefs[digest.String()] = sets.New(ref)
} else {
refs.Insert(ref)
}
+3 -3
View File
@@ -21,11 +21,11 @@ import (
"strings"
"testing"
"github.com/containerd/containerd/v2/internal/cri/setutils"
"github.com/containerd/errdefs"
"github.com/opencontainers/go-digest/digestset"
assertlib "github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/util/sets"
)
func TestInternalStore(t *testing.T) {
@@ -61,7 +61,7 @@ func TestInternalStore(t *testing.T) {
s := &store{
images: make(map[string]Image),
digestSet: digestset.NewSet(),
pinnedRefs: make(map[string]setutils.Set[string]),
pinnedRefs: make(map[string]sets.Set[string]),
}
t.Logf("should be able to add image")
@@ -144,7 +144,7 @@ func TestInternalStorePinnedImage(t *testing.T) {
s := &store{
images: make(map[string]Image),
digestSet: digestset.NewSet(),
pinnedRefs: make(map[string]setutils.Set[string]),
pinnedRefs: make(map[string]sets.Set[string]),
}
ref1 := "containerd.io/ref-1"
-68
View File
@@ -1,68 +0,0 @@
/*
Copyright The containerd 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.
*/
/*
Copyright 2016 The Kubernetes 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 types
// Label keys for labels used in this package.
const (
KubernetesPodNameLabel = "io.kubernetes.pod.name"
KubernetesPodNamespaceLabel = "io.kubernetes.pod.namespace"
KubernetesPodUIDLabel = "io.kubernetes.pod.uid"
KubernetesContainerNameLabel = "io.kubernetes.container.name"
)
// Label value constants
const (
// PodInfraContainerName is the KubernetesPodNameLabel value for infra
// containers.
PodInfraContainerName = "POD"
)
// GetContainerName returns the value of the KubernetesContainerNameLabel.
func GetContainerName(labels map[string]string) string {
return labels[KubernetesContainerNameLabel]
}
// GetPodName returns the value of the KubernetesPodNameLabel.
func GetPodName(labels map[string]string) string {
return labels[KubernetesPodNameLabel]
}
// GetPodUID returns the value of the KubernetesPodUIDLabel.
func GetPodUID(labels map[string]string) string {
return labels[KubernetesPodUIDLabel]
}
// GetPodNamespace returns the value of the KubernetesPodNamespaceLabel.
func GetPodNamespace(labels map[string]string) string {
return labels[KubernetesPodNamespaceLabel]
}
+2 -2
View File
@@ -19,7 +19,7 @@ package util
import (
"strings"
"github.com/containerd/containerd/v2/internal/cri/setutils"
"k8s.io/apimachinery/pkg/util/sets"
)
// InStringSlice checks whether a string is inside a string slice.
@@ -48,5 +48,5 @@ func SubtractStringSlice(ss []string, str string) []string {
// MergeStringSlices merges 2 string slices into one and remove duplicated elements.
func MergeStringSlices(a []string, b []string) []string {
return setutils.New(a...).Insert(b...).UnsortedList()
return sets.New(a...).Insert(b...).UnsortedList()
}
+11 -3
View File
@@ -220,7 +220,7 @@ func extractSamples(f *dto.MetricFamily, o *DecodeOptions) (model.Vector, error)
return extractSummary(o, f), nil
case dto.MetricType_UNTYPED:
return extractUntyped(o, f), nil
case dto.MetricType_HISTOGRAM:
case dto.MetricType_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM:
return extractHistogram(o, f), nil
}
return nil, fmt.Errorf("expfmt.extractSamples: unknown metric family type %v", f.GetType())
@@ -403,9 +403,13 @@ func extractHistogram(o *DecodeOptions, f *dto.MetricFamily) model.Vector {
infSeen = true
}
v := q.GetCumulativeCountFloat()
if v <= 0 {
v = float64(q.GetCumulativeCount())
}
samples = append(samples, &model.Sample{
Metric: model.Metric(lset),
Value: model.SampleValue(q.GetCumulativeCount()),
Value: model.SampleValue(v),
Timestamp: timestamp,
})
}
@@ -428,9 +432,13 @@ func extractHistogram(o *DecodeOptions, f *dto.MetricFamily) model.Vector {
}
lset[model.MetricNameLabel] = model.LabelValue(f.GetName() + "_count")
v := m.Histogram.GetSampleCountFloat()
if v <= 0 {
v = float64(m.Histogram.GetSampleCount())
}
count := &model.Sample{
Metric: model.Metric(lset),
Value: model.SampleValue(m.Histogram.GetSampleCount()),
Value: model.SampleValue(v),
Timestamp: timestamp,
}
samples = append(samples, count)
+1
View File
@@ -45,6 +45,7 @@ const (
// The Content-Type values for the different wire protocols. Do not do direct
// comparisons to these constants, instead use the comparison functions.
//
// Deprecated: Use expfmt.NewFormat(expfmt.TypeUnknown) instead.
FmtUnknown Format = `<unknown>`
// Deprecated: Use expfmt.NewFormat(expfmt.TypeTextPlain) instead.
-1
View File
@@ -13,7 +13,6 @@
// Build only when actually fuzzing
//go:build gofuzz
// +build gofuzz
package expfmt
+38 -21
View File
@@ -160,38 +160,38 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...E
n, err = w.WriteString("# HELP ")
written += n
if err != nil {
return
return written, err
}
n, err = writeName(w, compliantName)
written += n
if err != nil {
return
return written, err
}
err = w.WriteByte(' ')
written++
if err != nil {
return
return written, err
}
n, err = writeEscapedString(w, *in.Help, true)
written += n
if err != nil {
return
return written, err
}
err = w.WriteByte('\n')
written++
if err != nil {
return
return written, err
}
}
n, err = w.WriteString("# TYPE ")
written += n
if err != nil {
return
return written, err
}
n, err = writeName(w, compliantName)
written += n
if err != nil {
return
return written, err
}
switch metricType {
case dto.MetricType_COUNTER:
@@ -208,39 +208,41 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...E
n, err = w.WriteString(" unknown\n")
case dto.MetricType_HISTOGRAM:
n, err = w.WriteString(" histogram\n")
case dto.MetricType_GAUGE_HISTOGRAM:
n, err = w.WriteString(" gaugehistogram\n")
default:
return written, fmt.Errorf("unknown metric type %s", metricType.String())
}
written += n
if err != nil {
return
return written, err
}
if toOM.withUnit && in.Unit != nil {
n, err = w.WriteString("# UNIT ")
written += n
if err != nil {
return
return written, err
}
n, err = writeName(w, compliantName)
written += n
if err != nil {
return
return written, err
}
err = w.WriteByte(' ')
written++
if err != nil {
return
return written, err
}
n, err = writeEscapedString(w, *in.Unit, true)
written += n
if err != nil {
return
return written, err
}
err = w.WriteByte('\n')
written++
if err != nil {
return
return written, err
}
}
@@ -304,7 +306,7 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...E
)
written += n
if err != nil {
return
return written, err
}
}
n, err = writeOpenMetricsSample(
@@ -314,7 +316,7 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...E
)
written += n
if err != nil {
return
return written, err
}
n, err = writeOpenMetricsSample(
w, compliantName, "_count", metric, "", 0,
@@ -325,7 +327,7 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...E
createdTsBytesWritten, err = writeOpenMetricsCreated(w, compliantName, "", metric, "", 0, metric.Summary.GetCreatedTimestamp())
n += createdTsBytesWritten
}
case dto.MetricType_HISTOGRAM:
case dto.MetricType_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM:
if metric.Histogram == nil {
return written, fmt.Errorf(
"expected histogram in metric %s %s", compliantName, metric,
@@ -333,6 +335,12 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...E
}
infSeen := false
for _, b := range metric.Histogram.Bucket {
if b.GetCumulativeCountFloat() > 0 {
return written, fmt.Errorf(
"OpenMetrics v1.0 does not support float histogram %s %s",
compliantName, metric,
)
}
n, err = writeOpenMetricsSample(
w, compliantName, "_bucket", metric,
model.BucketLabel, b.GetUpperBound(),
@@ -341,7 +349,7 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...E
)
written += n
if err != nil {
return
return written, err
}
if math.IsInf(b.GetUpperBound(), +1) {
infSeen = true
@@ -354,9 +362,12 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...E
0, metric.Histogram.GetSampleCount(), true,
nil,
)
// We do not check for a float sample count here
// because we will check for it below (and error
// out if needed).
written += n
if err != nil {
return
return written, err
}
}
n, err = writeOpenMetricsSample(
@@ -366,7 +377,13 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...E
)
written += n
if err != nil {
return
return written, err
}
if metric.Histogram.GetSampleCountFloat() > 0 {
return written, fmt.Errorf(
"OpenMetrics v1.0 does not support float histogram %s %s",
compliantName, metric,
)
}
n, err = writeOpenMetricsSample(
w, compliantName, "_count", metric, "", 0,
@@ -384,10 +401,10 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...E
}
written += n
if err != nil {
return
return written, err
}
}
return
return written, err
}
// FinalizeOpenMetrics writes the final `# EOF\n` line required by OpenMetrics.
+35 -23
View File
@@ -108,38 +108,38 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (written int, err e
n, err = w.WriteString("# HELP ")
written += n
if err != nil {
return
return written, err
}
n, err = writeName(w, name)
written += n
if err != nil {
return
return written, err
}
err = w.WriteByte(' ')
written++
if err != nil {
return
return written, err
}
n, err = writeEscapedString(w, *in.Help, false)
written += n
if err != nil {
return
return written, err
}
err = w.WriteByte('\n')
written++
if err != nil {
return
return written, err
}
}
n, err = w.WriteString("# TYPE ")
written += n
if err != nil {
return
return written, err
}
n, err = writeName(w, name)
written += n
if err != nil {
return
return written, err
}
metricType := in.GetType()
switch metricType {
@@ -151,14 +151,17 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (written int, err e
n, err = w.WriteString(" summary\n")
case dto.MetricType_UNTYPED:
n, err = w.WriteString(" untyped\n")
case dto.MetricType_HISTOGRAM:
case dto.MetricType_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM:
// The classic Prometheus text format has no notion of a gauge
// histogram. We render a gauge histogram in the same way as a
// regular histogram.
n, err = w.WriteString(" histogram\n")
default:
return written, fmt.Errorf("unknown metric type %s", metricType.String())
}
written += n
if err != nil {
return
return written, err
}
// Finally the samples, one line for each.
@@ -208,7 +211,7 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (written int, err e
)
written += n
if err != nil {
return
return written, err
}
}
n, err = writeSample(
@@ -217,13 +220,13 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (written int, err e
)
written += n
if err != nil {
return
return written, err
}
n, err = writeSample(
w, name, "_count", metric, "", 0,
float64(metric.Summary.GetSampleCount()),
)
case dto.MetricType_HISTOGRAM:
case dto.MetricType_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM:
if metric.Histogram == nil {
return written, fmt.Errorf(
"expected histogram in metric %s %s", name, metric,
@@ -231,28 +234,36 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (written int, err e
}
infSeen := false
for _, b := range metric.Histogram.Bucket {
v := b.GetCumulativeCountFloat()
if v == 0 {
v = float64(b.GetCumulativeCount())
}
n, err = writeSample(
w, name, "_bucket", metric,
model.BucketLabel, b.GetUpperBound(),
float64(b.GetCumulativeCount()),
v,
)
written += n
if err != nil {
return
return written, err
}
if math.IsInf(b.GetUpperBound(), +1) {
infSeen = true
}
}
if !infSeen {
v := metric.Histogram.GetSampleCountFloat()
if v == 0 {
v = float64(metric.Histogram.GetSampleCount())
}
n, err = writeSample(
w, name, "_bucket", metric,
model.BucketLabel, math.Inf(+1),
float64(metric.Histogram.GetSampleCount()),
v,
)
written += n
if err != nil {
return
return written, err
}
}
n, err = writeSample(
@@ -261,12 +272,13 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (written int, err e
)
written += n
if err != nil {
return
return written, err
}
n, err = writeSample(
w, name, "_count", metric, "", 0,
float64(metric.Histogram.GetSampleCount()),
)
v := metric.Histogram.GetSampleCountFloat()
if v == 0 {
v = float64(metric.Histogram.GetSampleCount())
}
n, err = writeSample(w, name, "_count", metric, "", 0, v)
default:
return written, fmt.Errorf(
"unexpected type in metric %s %s", name, metric,
@@ -274,10 +286,10 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (written int, err e
}
written += n
if err != nil {
return
return written, err
}
}
return
return written, err
}
// writeSample writes a single sample in text format to w, given the metric
+83 -19
View File
@@ -48,8 +48,10 @@ func (e ParseError) Error() string {
return fmt.Sprintf("text format parsing error in line %d: %s", e.Line, e.Msg)
}
// TextParser is used to parse the simple and flat text-based exchange format. Its
// zero value is ready to use.
// TextParser is used to parse the simple and flat text-based exchange format.
//
// TextParser instances must be created with NewTextParser, the zero value of
// TextParser is invalid.
type TextParser struct {
metricFamiliesByName map[string]*dto.MetricFamily
buf *bufio.Reader // Where the parsed input is read through.
@@ -129,9 +131,44 @@ func (p *TextParser) TextToMetricFamilies(in io.Reader) (map[string]*dto.MetricF
if p.err != nil && errors.Is(p.err, io.EOF) {
p.parseError("unexpected end of input stream")
}
for _, histogramMetric := range p.histograms {
normalizeHistogram(histogramMetric.GetHistogram())
}
return p.metricFamiliesByName, p.err
}
// normalizeHistogram makes sure that all the buckets and the count in each
// histogram is either completely float or completely integer.
func normalizeHistogram(histogram *dto.Histogram) {
if histogram == nil {
return
}
anyFloats := false
if histogram.GetSampleCountFloat() != 0 {
anyFloats = true
} else {
for _, b := range histogram.GetBucket() {
if b.GetCumulativeCountFloat() != 0 {
anyFloats = true
break
}
}
}
if !anyFloats {
return
}
if histogram.GetSampleCountFloat() == 0 {
histogram.SampleCountFloat = proto.Float64(float64(histogram.GetSampleCount()))
histogram.SampleCount = nil
}
for _, b := range histogram.GetBucket() {
if b.GetCumulativeCountFloat() == 0 {
b.CumulativeCountFloat = proto.Float64(float64(b.GetCumulativeCount()))
b.CumulativeCount = nil
}
}
}
func (p *TextParser) reset(in io.Reader) {
p.metricFamiliesByName = map[string]*dto.MetricFamily{}
p.currentLabelPairs = nil
@@ -281,7 +318,9 @@ func (p *TextParser) readingLabels() stateFn {
// Summaries/histograms are special. We have to reset the
// currentLabels map, currentQuantile and currentBucket before starting to
// read labels.
if p.currentMF.GetType() == dto.MetricType_SUMMARY || p.currentMF.GetType() == dto.MetricType_HISTOGRAM {
if p.currentMF.GetType() == dto.MetricType_SUMMARY ||
p.currentMF.GetType() == dto.MetricType_HISTOGRAM ||
p.currentMF.GetType() == dto.MetricType_GAUGE_HISTOGRAM {
p.currentLabels = map[string]string{}
p.currentLabels[string(model.MetricNameLabel)] = p.currentMF.GetName()
p.currentQuantile = math.NaN()
@@ -374,7 +413,9 @@ func (p *TextParser) startLabelName() stateFn {
// Special summary/histogram treatment. Don't add 'quantile' and 'le'
// labels to 'real' labels.
if (p.currentMF.GetType() != dto.MetricType_SUMMARY || p.currentLabelPair.GetName() != model.QuantileLabel) &&
(p.currentMF.GetType() != dto.MetricType_HISTOGRAM || p.currentLabelPair.GetName() != model.BucketLabel) {
((p.currentMF.GetType() != dto.MetricType_HISTOGRAM &&
p.currentMF.GetType() != dto.MetricType_GAUGE_HISTOGRAM) ||
p.currentLabelPair.GetName() != model.BucketLabel) {
p.currentLabelPairs = append(p.currentLabelPairs, p.currentLabelPair)
}
// Check for duplicate label names.
@@ -425,7 +466,7 @@ func (p *TextParser) startLabelValue() stateFn {
}
}
// Similar special treatment of histograms.
if p.currentMF.GetType() == dto.MetricType_HISTOGRAM {
if p.currentMF.GetType() == dto.MetricType_HISTOGRAM || p.currentMF.GetType() == dto.MetricType_GAUGE_HISTOGRAM {
if p.currentLabelPair.GetName() == model.BucketLabel {
if p.currentBucket, p.err = parseFloat(p.currentLabelPair.GetValue()); p.err != nil {
// Create a more helpful error message.
@@ -476,7 +517,7 @@ func (p *TextParser) readingValue() stateFn {
p.summaries[signature] = p.currentMetric
p.currentMF.Metric = append(p.currentMF.Metric, p.currentMetric)
}
case dto.MetricType_HISTOGRAM:
case dto.MetricType_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM:
signature := model.LabelsToSignature(p.currentLabels)
if histogram := p.histograms[signature]; histogram != nil {
p.currentMetric = histogram
@@ -522,24 +563,38 @@ func (p *TextParser) readingValue() stateFn {
},
)
}
case dto.MetricType_HISTOGRAM:
case dto.MetricType_HISTOGRAM, dto.MetricType_GAUGE_HISTOGRAM:
// *sigh*
if p.currentMetric.Histogram == nil {
p.currentMetric.Histogram = &dto.Histogram{}
}
switch {
case p.currentIsHistogramCount:
p.currentMetric.Histogram.SampleCount = proto.Uint64(uint64(value))
if uintValue := uint64(value); value == float64(uintValue) {
p.currentMetric.Histogram.SampleCount = proto.Uint64(uintValue)
} else {
if value < 0 {
p.parseError(fmt.Sprintf("negative count for histogram %q", p.currentMF.GetName()))
return nil
}
p.currentMetric.Histogram.SampleCountFloat = proto.Float64(value)
}
case p.currentIsHistogramSum:
p.currentMetric.Histogram.SampleSum = proto.Float64(value)
case !math.IsNaN(p.currentBucket):
p.currentMetric.Histogram.Bucket = append(
p.currentMetric.Histogram.Bucket,
&dto.Bucket{
UpperBound: proto.Float64(p.currentBucket),
CumulativeCount: proto.Uint64(uint64(value)),
},
)
b := &dto.Bucket{
UpperBound: proto.Float64(p.currentBucket),
}
if uintValue := uint64(value); value == float64(uintValue) {
b.CumulativeCount = proto.Uint64(uintValue)
} else {
if value < 0 {
p.parseError(fmt.Sprintf("negative bucket population for histogram %q", p.currentMF.GetName()))
return nil
}
b.CumulativeCountFloat = proto.Float64(value)
}
p.currentMetric.Histogram.Bucket = append(p.currentMetric.Histogram.Bucket, b)
}
default:
p.err = fmt.Errorf("unexpected type for metric name %q", p.currentMF.GetName())
@@ -602,10 +657,18 @@ func (p *TextParser) readingType() stateFn {
if p.readTokenUntilNewline(false); p.err != nil {
return nil // Unexpected end of input.
}
metricType, ok := dto.MetricType_value[strings.ToUpper(p.currentToken.String())]
typ := strings.ToUpper(p.currentToken.String()) // Tolerate any combination of upper and lower case.
metricType, ok := dto.MetricType_value[typ] // Tolerate "gauge_histogram" (not originally part of the text format).
if !ok {
p.parseError(fmt.Sprintf("unknown metric type %q", p.currentToken.String()))
return nil
// We also want to tolerate "gaugehistogram" to mark a gauge
// histogram, because that string is used in OpenMetrics. Note,
// however, that gauge histograms do not officially exist in the
// classic text format.
if typ != "GAUGEHISTOGRAM" {
p.parseError(fmt.Sprintf("unknown metric type %q", p.currentToken.String()))
return nil
}
metricType = int32(dto.MetricType_GAUGE_HISTOGRAM)
}
p.currentMF.Type = dto.MetricType(metricType).Enum()
return p.startOfLine
@@ -855,7 +918,8 @@ func (p *TextParser) setOrCreateCurrentMF() {
}
histogramName := histogramMetricName(name)
if p.currentMF = p.metricFamiliesByName[histogramName]; p.currentMF != nil {
if p.currentMF.GetType() == dto.MetricType_HISTOGRAM {
if p.currentMF.GetType() == dto.MetricType_HISTOGRAM ||
p.currentMF.GetType() == dto.MetricType_GAUGE_HISTOGRAM {
if isCount(name) {
p.currentIsHistogramCount = true
}
+17 -11
View File
@@ -1,7 +1,9 @@
version: "2"
linters:
enable:
- errorlint
- forbidigo
- gocritic
- godot
- misspell
- revive
@@ -11,6 +13,20 @@ linters:
forbid:
- pattern: ^fmt\.Print.*$
msg: Do not commit print statements.
gocritic:
enable-all: true
disabled-checks:
- commentFormatting
- commentedOutCode
- deferInLoop
- filepathJoin
- hugeParam
- importShadow
- paramTypeCombine
- rangeValCopy
- tooManyResultsChecker
- unnamedResult
- whyNoLint
godot:
exclude:
# Ignore "See: URL".
@@ -19,16 +35,12 @@ linters:
misspell:
locale: US
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
warn-unused: true
formatters:
enable:
- gofmt
@@ -37,9 +49,3 @@ formatters:
goimports:
local-prefixes:
- github.com/prometheus/procfs
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright 2018 The Prometheus Authors
# Copyright The Prometheus 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
+1 -1
View File
@@ -139,7 +139,7 @@ common-deps:
update-go-deps:
@echo ">> updating Go dependencies"
@for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \
$(GO) get -d $$m; \
$(GO) get $$m; \
done
$(GO) mod tidy
+5 -4
View File
@@ -1,4 +1,4 @@
// Copyright 2019 The Prometheus Authors
// Copyright The Prometheus 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
@@ -73,15 +73,16 @@ func parseARPEntries(data []byte) ([]ARPEntry, error) {
columns := strings.Fields(line)
width := len(columns)
if width == expectedHeaderWidth || width == 0 {
switch width {
case expectedHeaderWidth, 0:
continue
} else if width == expectedDataWidth {
case expectedDataWidth:
entry, err := parseARPEntry(columns)
if err != nil {
return []ARPEntry{}, fmt.Errorf("%w: Failed to parse ARP entry: %v: %w", ErrFileParse, entry, err)
}
entries = append(entries, entry)
} else {
default:
return []ARPEntry{}, fmt.Errorf("%w: %d columns found, but expected %d: %w", ErrFileParse, width, expectedDataWidth, err)
}
+4 -6
View File
@@ -1,4 +1,4 @@
// Copyright 2017 The Prometheus Authors
// Copyright The Prometheus 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
@@ -64,14 +64,12 @@ func parseBuddyInfo(r io.Reader) ([]BuddyInfo, error) {
if bucketCount == -1 {
bucketCount = arraySize
} else {
if bucketCount != arraySize {
return nil, fmt.Errorf("%w: mismatch in number of buddyinfo buckets, previous count %d, new count %d", ErrFileParse, bucketCount, arraySize)
}
} else if bucketCount != arraySize {
return nil, fmt.Errorf("%w: mismatch in number of buddyinfo buckets, previous count %d, new count %d", ErrFileParse, bucketCount, arraySize)
}
sizes := make([]float64, arraySize)
for i := 0; i < arraySize; i++ {
for i := range arraySize {
sizes[i], err = strconv.ParseFloat(parts[i+4], 64)
if err != nil {
return nil, fmt.Errorf("%w: Invalid valid in buddyinfo: %f: %w", ErrFileParse, sizes[i], err)
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2021 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2019 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2020 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2022 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2020 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2020 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2020 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2020 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2020 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2020 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2019 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2014 Prometheus Team
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2018 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2018 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2018 The Prometheus Authors
// Copyright The Prometheus 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
+5 -4
View File
@@ -1,4 +1,4 @@
// Copyright 2019 The Prometheus Authors
// Copyright The Prometheus 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
@@ -388,20 +388,21 @@ func parseFscacheinfo(r io.Reader) (*Fscacheinfo, error) {
}
}
case "CacheOp:":
if strings.Split(fields[1], "=")[0] == "alo" {
switch strings.Split(fields[1], "=")[0] {
case "alo":
err := setFSCacheFields(fields[1:], &m.CacheopAllocationsinProgress, &m.CacheopLookupObjectInProgress,
&m.CacheopLookupCompleteInPorgress, &m.CacheopGrabObjectInProgress)
if err != nil {
return &m, err
}
} else if strings.Split(fields[1], "=")[0] == "inv" {
case "inv":
err := setFSCacheFields(fields[1:], &m.CacheopInvalidations, &m.CacheopUpdateObjectInProgress,
&m.CacheopDropObjectInProgress, &m.CacheopPutObjectInProgress, &m.CacheopAttributeChangeInProgress,
&m.CacheopSyncCacheInProgress)
if err != nil {
return &m, err
}
} else {
default:
err := setFSCacheFields(fields[1:], &m.CacheopReadOrAllocPageInProgress, &m.CacheopReadOrAllocPagesInProgress,
&m.CacheopAllocatePageInProgress, &m.CacheopAllocatePagesInProgress, &m.CacheopWritePagesInProgress,
&m.CacheopUncachePagesInProgress, &m.CacheopDissociatePagesInProgress)
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2019 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2018 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2019 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2018 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2019 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2019 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2018 The Prometheus Authors
// Copyright The Prometheus 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
+45
View File
@@ -0,0 +1,45 @@
// Copyright The Prometheus 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.
//go:build !windows
// +build !windows
package procfs
import (
"os"
"strconv"
"strings"
)
// KernelHung contains information about to the kernel's hung_task_detect_count number.
type KernelHung struct {
// Indicates the total number of tasks that have been detected as hung since the system boot.
// This file shows up if `CONFIG_DETECT_HUNG_TASK` is enabled.
HungTaskDetectCount *uint64
}
// KernelHung returns values from /proc/sys/kernel/hung_task_detect_count.
func (fs FS) KernelHung() (KernelHung, error) {
data, err := os.ReadFile(fs.proc.Path("sys", "kernel", "hung_task_detect_count"))
if err != nil {
return KernelHung{}, err
}
val, err := strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64)
if err != nil {
return KernelHung{}, err
}
return KernelHung{
HungTaskDetectCount: &val,
}, nil
}
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2020 The Prometheus Authors
// Copyright The Prometheus 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
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2019 The Prometheus Authors
// Copyright The Prometheus 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
+96 -22
View File
@@ -1,4 +1,4 @@
// Copyright 2018 The Prometheus Authors
// Copyright The Prometheus 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
@@ -27,13 +27,34 @@ var (
recoveryLinePctRE = regexp.MustCompile(`= (.+)%`)
recoveryLineFinishRE = regexp.MustCompile(`finish=(.+)min`)
recoveryLineSpeedRE = regexp.MustCompile(`speed=(.+)[A-Z]`)
componentDeviceRE = regexp.MustCompile(`(.*)\[\d+\]`)
componentDeviceRE = regexp.MustCompile(`(.*)\[(\d+)\](\([SF]+\))?`)
personalitiesPrefix = "Personalities : "
)
type MDStatComponent struct {
// Name of the component device.
Name string
// DescriptorIndex number of component device, e.g. the order in the superblock.
DescriptorIndex int32
// Flags per Linux drivers/md/md.[ch] as of v6.12-rc1
// Subset that are exposed in mdstat
WriteMostly bool
Journal bool
Faulty bool // "Faulty" is what kernel source uses for "(F)"
Spare bool
Replacement bool
// Some additional flags that are NOT exposed in procfs today; they may
// be available via sysfs.
// In_sync, Bitmap_sync, Blocked, WriteErrorSeen, FaultRecorded,
// BlockedBadBlocks, WantReplacement, Candidate, ...
}
// MDStat holds info parsed from /proc/mdstat.
type MDStat struct {
// Name of the device.
Name string
// raid type of the device.
Type string
// activity-state of the device.
ActivityState string
// Number of active disks.
@@ -58,8 +79,8 @@ type MDStat struct {
BlocksSyncedFinishTime float64
// current sync speed (in Kilobytes/sec)
BlocksSyncedSpeed float64
// Name of md component devices
Devices []string
// component devices
Devices []MDStatComponent
}
// MDStat parses an mdstat-file (/proc/mdstat) and returns a slice of
@@ -80,28 +101,52 @@ func (fs FS) MDStat() ([]MDStat, error) {
// parseMDStat parses data from mdstat file (/proc/mdstat) and returns a slice of
// structs containing the relevant info.
func parseMDStat(mdStatData []byte) ([]MDStat, error) {
// TODO:
// - parse global hotspares from the "unused devices" line.
mdStats := []MDStat{}
lines := strings.Split(string(mdStatData), "\n")
knownRaidTypes := make(map[string]bool)
for i, line := range lines {
if strings.TrimSpace(line) == "" || line[0] == ' ' ||
strings.HasPrefix(line, "Personalities") ||
strings.HasPrefix(line, "unused") {
continue
}
// Personalities : [linear] [multipath] [raid0] [raid1] [raid6] [raid5] [raid4] [raid10]
if len(knownRaidTypes) == 0 && strings.HasPrefix(line, personalitiesPrefix) {
personalities := strings.Fields(line[len(personalitiesPrefix):])
for _, word := range personalities {
word := word[1 : len(word)-1]
knownRaidTypes[word] = true
}
continue
}
deviceFields := strings.Fields(line)
if len(deviceFields) < 3 {
return nil, fmt.Errorf("%w: Expected 3+ lines, got %q", ErrFileParse, line)
}
mdName := deviceFields[0] // mdx
state := deviceFields[2] // active or inactive
state := deviceFields[2] // active, inactive, broken
mdType := "unknown" // raid1, raid5, etc.
var deviceStartIndex int
if len(deviceFields) > 3 { // mdType may be in the 3rd or 4th field
if isRaidType(deviceFields[3], knownRaidTypes) {
mdType = deviceFields[3]
deviceStartIndex = 4
} else if len(deviceFields) > 4 && isRaidType(deviceFields[4], knownRaidTypes) {
// if the 3rd field is (...), the 4th field is the mdType
mdType = deviceFields[4]
deviceStartIndex = 5
}
}
if len(lines) <= i+3 {
return nil, fmt.Errorf("%w: Too few lines for md device: %q", ErrFileParse, mdName)
}
// Failed disks have the suffix (F) & Spare disks have the suffix (S).
// Failed (Faulty) disks have the suffix (F) & Spare disks have the suffix (S).
fail := int64(strings.Count(line, "(F)"))
spare := int64(strings.Count(line, "(S)"))
active, total, down, size, err := evalStatusLine(lines[i], lines[i+1])
@@ -129,13 +174,14 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) {
// Append recovery and resyncing state info.
if recovering || resyncing || checking || reshaping {
if recovering {
switch {
case recovering:
state = "recovering"
} else if reshaping {
case reshaping:
state = "reshaping"
} else if checking {
case checking:
state = "checking"
} else {
default:
state = "resyncing"
}
@@ -151,8 +197,14 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) {
}
}
devices, err := evalComponentDevices(deviceFields[deviceStartIndex:])
if err != nil {
return nil, fmt.Errorf("error parsing components in md device %q: %w", mdName, err)
}
mdStats = append(mdStats, MDStat{
Name: mdName,
Type: mdType,
ActivityState: state,
DisksActive: active,
DisksFailed: fail,
@@ -165,14 +217,24 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) {
BlocksSyncedPct: pct,
BlocksSyncedFinishTime: finish,
BlocksSyncedSpeed: speed,
Devices: evalComponentDevices(deviceFields),
Devices: devices,
})
}
return mdStats, nil
}
// check if a string's format is like the mdType
// Rule 1: mdType should not be like (...)
// Rule 2: mdType should not be like sda[0]
// .
func isRaidType(mdType string, knownRaidTypes map[string]bool) bool {
_, ok := knownRaidTypes[mdType]
return !strings.ContainsAny(mdType, "([") && ok
}
func evalStatusLine(deviceLine, statusLine string) (active, total, down, size int64, err error) {
// e.g. 523968 blocks super 1.2 [4/4] [UUUU]
statusFields := strings.Fields(statusLine)
if len(statusFields) < 1 {
return 0, 0, 0, 0, fmt.Errorf("%w: Unexpected statusline %q: %w", ErrFileParse, statusLine, err)
@@ -263,17 +325,29 @@ func evalRecoveryLine(recoveryLine string) (blocksSynced int64, blocksToBeSynced
return blocksSynced, blocksToBeSynced, pct, finish, speed, nil
}
func evalComponentDevices(deviceFields []string) []string {
mdComponentDevices := make([]string, 0)
if len(deviceFields) > 3 {
for _, field := range deviceFields[4:] {
match := componentDeviceRE.FindStringSubmatch(field)
if match == nil {
continue
}
mdComponentDevices = append(mdComponentDevices, match[1])
func evalComponentDevices(deviceFields []string) ([]MDStatComponent, error) {
mdComponentDevices := make([]MDStatComponent, 0)
for _, field := range deviceFields {
match := componentDeviceRE.FindStringSubmatch(field)
if match == nil {
continue
}
descriptorIndex, err := strconv.ParseInt(match[2], 10, 32)
if err != nil {
return mdComponentDevices, fmt.Errorf("error parsing int from device %q: %w", match[2], err)
}
mdComponentDevices = append(mdComponentDevices, MDStatComponent{
Name: match[1],
DescriptorIndex: int32(descriptorIndex),
// match may contain one or more of these
// https://github.com/torvalds/linux/blob/7ec462100ef9142344ddbf86f2c3008b97acddbe/drivers/md/md.c#L8376-L8392
Faulty: strings.Contains(match[3], "(F)"),
Spare: strings.Contains(match[3], "(S)"),
Journal: strings.Contains(match[3], "(J)"),
Replacement: strings.Contains(match[3], "(R)"),
WriteMostly: strings.Contains(match[3], "(W)"),
})
}
return mdComponentDevices
return mdComponentDevices, nil
}
Loaded 100 of 161 files, more files were not shown because too many files have changed in this diff. Show more