Add config drive as a source for OpenStack instance metadata

This adds the config drive as an additional source for instance metadata
when using OpenStack.
This commit is contained in:
ederst 2022-06-21 10:08:19 +02:00
parent ae03c6e1a0
commit 4056da0cce
6 changed files with 681 additions and 13 deletions

View File

@ -20,17 +20,42 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"os"
"path"
"strings" "strings"
"k8s.io/klog/v2" "k8s.io/klog/v2"
"k8s.io/kops/protokube/pkg/gossip" "k8s.io/kops/protokube/pkg/gossip"
gossipos "k8s.io/kops/protokube/pkg/gossip/openstack" gossipos "k8s.io/kops/protokube/pkg/gossip/openstack"
"k8s.io/kops/upup/pkg/fi/cloudup/openstack" "k8s.io/kops/upup/pkg/fi/cloudup/openstack"
"k8s.io/mount-utils"
utilexec "k8s.io/utils/exec"
) )
const MetadataLatest string = "http://169.254.169.254/openstack/latest/meta_data.json" const (
// MetadataLatestPath is the path to the metadata on the config drive
MetadataLatestPath string = "openstack/latest/meta_data.json"
// MetadataID is the identifier for the metadata service
MetadataID string = "metadataService"
// MetadataLastestServiceURL points to the latest metadata of the metadata service
MetadataLatestServiceURL string = "http://169.254.169.254/" + MetadataLatestPath
// ConfigDriveID is the identifier for the config drive containing metadata
ConfigDriveID string = "configDrive"
// ConfigDriveLabel identifies the config drive by label on the OS
ConfigDriveLabel string = "config-2"
// DefaultMetadataSearchOrder defines the default order in which the metadata services are queried
DefaultMetadataSearchOrder string = ConfigDriveID + ", " + MetadataID
DiskByLabelPath string = "/dev/disk/by-label/"
)
type Metadata struct { type Metadata struct {
// Matches openstack.TagClusterName // Matches openstack.TagClusterName
@ -59,31 +84,146 @@ type OpenStackCloudProvider struct {
storageZone string storageZone string
} }
type MetadataService struct {
serviceURL string
configDrivePath string
mounter *mount.SafeFormatAndMount
mountTarget string
searchOrder string
}
var _ CloudProvider = &OpenStackCloudProvider{} var _ CloudProvider = &OpenStackCloudProvider{}
func getLocalMetadata() (*InstanceMetadata, error) { // getFromConfigDrive tries to get metadata by mounting a config drive and returns it as InstanceMetadata
var meta InstanceMetadata // It will return an error if there is no disk labelled as ConfigDriveLabel or other errors while mounting the disk, or reading the file occur.
func (mds MetadataService) getFromConfigDrive() (*InstanceMetadata, error) {
dev := path.Join(DiskByLabelPath, ConfigDriveLabel)
if _, err := os.Stat(dev); os.IsNotExist(err) {
out, err := mds.mounter.Exec.Command(
"blkid", "-l",
"-t", fmt.Sprintf("LABEL=%s", ConfigDriveLabel),
"-o", "device",
).CombinedOutput()
if err != nil {
return nil, fmt.Errorf("unable to run blkid: %v", err)
}
dev = strings.TrimSpace(string(out))
}
err := mds.mounter.Mount(dev, mds.mountTarget, "iso9660", []string{"ro"})
if err != nil {
err = mds.mounter.Mount(dev, mds.mountTarget, "vfat", []string{"ro"})
}
if err != nil {
return nil, fmt.Errorf("error mounting configdrive '%s': %v", dev, err)
}
defer mds.mounter.Unmount(mds.mountTarget)
f, err := os.Open(
path.Join(mds.mountTarget, mds.configDrivePath))
if err != nil {
return nil, fmt.Errorf("error reading '%s' on config drive: %v", mds.configDrivePath, err)
}
defer f.Close()
return mds.parseMetadata(f)
}
// getFromMetadataService tries to get metadata from a metadata service endpoint and returns it as InstanceMetadata.
// If the service endpoint cannot be contacted or reports a different status than StatusOK it will return an error.
func (mds MetadataService) getFromMetadataService() (*InstanceMetadata, error) {
var client http.Client var client http.Client
resp, err := client.Get(MetadataLatest)
resp, err := client.Get(mds.serviceURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode == http.StatusOK { if resp.StatusCode == http.StatusOK {
bodyBytes, err := io.ReadAll(resp.Body) return mds.parseMetadata(resp.Body)
if err != nil {
return nil, err
}
err = json.Unmarshal(bodyBytes, &meta)
if err != nil {
return nil, err
}
return &meta, nil
} }
err = fmt.Errorf("fetching metadata from '%s' returned status code '%d'", mds.serviceURL, resp.StatusCode)
return nil, err return nil, err
} }
// parseMetadata reads JSON data from a Reader and returns it as InstanceMetadata.
func (mds MetadataService) parseMetadata(r io.Reader) (*InstanceMetadata, error) {
var meta InstanceMetadata
data, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
err = json.Unmarshal(data, &meta)
if err != nil {
return nil, err
}
return &meta, nil
}
// getMetadata tries to get metadata for the instance by mounting the config drive and/or querying the metadata service endpoint.
// Depending on the searchOrder it will return data from the first source which successfully returns.
// If all the sources in searchOrder are erroneous it will propagate the last error to its caller.
func (mds MetadataService) getMetadata() (*InstanceMetadata, error) {
// Note(ederst): I used and modified code for getting the config drive metadata to work from here:
// * https://github.com/kubernetes/cloud-provider-openstack/blob/27b6fc483451b6df2112a6a4a40a34ffc9093635/pkg/util/metadata/metadata.go
var meta *InstanceMetadata
var err error
ids := strings.Split(mds.searchOrder, ",")
for _, id := range ids {
id = strings.TrimSpace(id)
switch id {
case ConfigDriveID:
meta, err = mds.getFromConfigDrive()
case MetadataID:
meta, err = mds.getFromMetadataService()
default:
err = fmt.Errorf("%s is not a valid metadata search order option. Supported options are %s and %s", id, ConfigDriveID, MetadataID)
}
if err == nil {
break
}
}
return meta, err
}
func newMetadataService(serviceURL string, configDrivePath string, mounter *mount.SafeFormatAndMount, mountTarget string, searchOrder string) *MetadataService {
return &MetadataService{
serviceURL: serviceURL,
configDrivePath: configDrivePath,
mounter: mounter,
mountTarget: mountTarget,
searchOrder: searchOrder,
}
}
// getDefaultMounter returns a mount and executor interface to use for getting metadata from a config drive
func getDefaultMounter() *mount.SafeFormatAndMount {
mounter := mount.New("")
exec := utilexec.New()
return &mount.SafeFormatAndMount{
Interface: mounter,
Exec: exec,
}
}
func getLocalMetadata() (*InstanceMetadata, error) {
mountTarget, err := ioutil.TempDir("", "configdrive")
if err != nil {
return nil, err
}
defer os.Remove(mountTarget)
return newMetadataService(MetadataLatestServiceURL, MetadataLatestPath, getDefaultMounter(), mountTarget, DefaultMetadataSearchOrder).getMetadata()
}
// NewOpenStackCloudProvider builds a OpenStackCloudProvider // NewOpenStackCloudProvider builds a OpenStackCloudProvider
func NewOpenStackCloudProvider() (*OpenStackCloudProvider, error) { func NewOpenStackCloudProvider() (*OpenStackCloudProvider, error) {
metadata, err := getLocalMetadata() metadata, err := getLocalMetadata()

View File

@ -0,0 +1,247 @@
/*
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 protokube
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"reflect"
"strings"
"testing"
"k8s.io/mount-utils"
ue "k8s.io/utils/exec"
uet "k8s.io/utils/exec/testing"
)
const (
metadataIDFirst = MetadataID + ", " + ConfigDriveID
configDriveIDFirst = ConfigDriveID + ", " + MetadataID
)
// expectedDriveMetadata is the metadata expected from the service endpoint
var expectedServiceMetadata = &InstanceMetadata{
ServerID: "01234567-cafe-babe-beef-0123456789ab",
Hostname: "test-server.serv.ice",
Name: "test-server",
AvailabilityZone: "nova",
ProjectID: "0123456789abcdeffedcba9876543210",
}
// expectedDriveMetadata is the metadata expected from the config drive
var expectedDriveMetadata = &InstanceMetadata{
ServerID: "01234567-cafe-babe-beef-0123456789ab",
Hostname: "test-server.config.drv",
Name: "test-server",
AvailabilityZone: "nova",
ProjectID: "0123456789abcdeffedcba9876543210",
}
func assertTestResults(t *testing.T, err error, expected interface{}, actual interface{}) {
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("expected %+v, but got %+v", expected, actual)
}
}
// mockMetadataEndpoint mocks the actual metadata endpoint by returning the passed data
func mockMetadataEndpoint(w http.ResponseWriter, r *http.Request, data string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, data)
}
// createMockServer creates and returns a mock HTTP server to test getting the metadata from a service endpoint
func createMockServer() (*httptest.Server, error) {
data, err := os.ReadFile("testdata/metadata_service.json")
if err != nil {
return nil, err
}
// Note(ederst): source of inspiration https://clavinjune.dev/en/blogs/mocking-http-call-in-golang-a-better-way/
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/openstack/latest/meta_data.json") {
mockMetadataEndpoint(w, r, string(data))
} else {
http.NotFoundHandler().ServeHTTP(w, r)
}
}))
return mockServer, nil
}
// fakeBlkidCmd builds a fake blkid command which returns the device path of the config drive
func fakeBlkidCmd(device string) uet.FakeCommandAction {
fakeCmd := &uet.FakeCmd{
CombinedOutputScript: []uet.FakeAction{
func() ([]byte, []byte, error) {
if device == "" {
return nil, nil, uet.FakeExitError{
Status: 2,
}
}
return []byte(device), nil, nil
},
},
}
return func(cmd string, args ...string) ue.Cmd {
return uet.InitFakeCmd(fakeCmd, "blkid", "-l", "-t", "LABEL=config-2", "-o", "device")
}
}
// getFakeMounter creates and returns a fake mounter to test getting the metadata from a config drive
func getFakeMounter(device string) *mount.SafeFormatAndMount {
fakeExec := &uet.FakeExec{
ExactOrder: true,
}
fakeExec.CommandScript = append(fakeExec.CommandScript, fakeBlkidCmd(device))
return &mount.SafeFormatAndMount{
Interface: &mount.FakeMounter{},
Exec: fakeExec,
}
}
func TestGetMetadataFromMetadataServiceReturnsNotFoundError(t *testing.T) {
mockServer, err := createMockServer()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
defer mockServer.Close()
metadataUrl := fmt.Sprintf("%s/%s", mockServer.URL, "no/meta_data.json")
expectedErr := fmt.Errorf("fetching metadata from '%s' returned status code '404'", metadataUrl)
_, actualErr := newMetadataService(metadataUrl, MetadataLatestPath, nil, "", MetadataID).getMetadata()
assertTestResults(t, nil, expectedErr, actualErr)
}
func TestGetMetadataFromMetadataService(t *testing.T) {
mockServer, err := createMockServer()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
defer mockServer.Close()
metadataUrl := fmt.Sprintf("%s/%s", mockServer.URL, MetadataLatestPath)
actual, err := newMetadataService(metadataUrl, MetadataLatestPath, nil, "", MetadataID).getMetadata()
assertTestResults(t, err, expectedServiceMetadata, actual)
}
func TestGetMetadataFromConfigDriveReturnsErrorWhenNoDeviceIsFound(t *testing.T) {
fakeMounter := getFakeMounter("")
expectedErr := fmt.Errorf("unable to run blkid: exit 2")
_, actualErr := newMetadataService("", "testdata/metadata_drive.json", fakeMounter, ".", ConfigDriveID).getMetadata()
assertTestResults(t, nil, expectedErr, actualErr)
}
func TestGetMetadataFromConfigDrive(t *testing.T) {
fakeMounter := getFakeMounter("/dev/sr0")
actual, err := newMetadataService("", "testdata/metadata_drive.json", fakeMounter, ".", ConfigDriveID).getMetadata()
assertTestResults(t, err, expectedDriveMetadata, actual)
}
func TestGetMetadataReturnsLastErrorWhenNoMetadataWasFound(t *testing.T) {
fakeMounter := getFakeMounter("")
mockServer, err := createMockServer()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
defer mockServer.Close()
metadataUrl := fmt.Sprintf("%s/%s", mockServer.URL, "no/meta_data.json")
expectedErr := fmt.Errorf("fetching metadata from '%s' returned status code '404'", metadataUrl)
_, actualErr := newMetadataService(metadataUrl, "testdata/metadata_drive.json", fakeMounter, ".", configDriveIDFirst).getMetadata()
assertTestResults(t, nil, expectedErr, actualErr)
}
func TestGetMetadataFromConfigDriveWhenItIsFirstInSearchOrder(t *testing.T) {
fakeMounter := getFakeMounter("/dev/sr0")
mockServer, err := createMockServer()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
defer mockServer.Close()
metadataUrl := fmt.Sprintf("%s/%s", mockServer.URL, MetadataLatestPath)
actual, err := newMetadataService(metadataUrl, "testdata/metadata_drive.json", fakeMounter, ".", configDriveIDFirst).getMetadata()
assertTestResults(t, err, expectedDriveMetadata, actual)
}
func TestGetMetadataFromServiceEndpointWhenConfigDriveFails(t *testing.T) {
fakeMounter := getFakeMounter("")
mockServer, err := createMockServer()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
defer mockServer.Close()
metadataUrl := fmt.Sprintf("%s/%s", mockServer.URL, MetadataLatestPath)
actual, err := newMetadataService(metadataUrl, "testdata/metadata_drive.json", fakeMounter, ".", configDriveIDFirst).getMetadata()
assertTestResults(t, err, expectedServiceMetadata, actual)
}
func TestGetMetadataFromServiceEndpointWhenItIsFirstInSearchOrder(t *testing.T) {
fakeMounter := getFakeMounter("/dev/sr0")
mockServer, err := createMockServer()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
defer mockServer.Close()
metadataUrl := fmt.Sprintf("%s/%s", mockServer.URL, MetadataLatestPath)
actual, err := newMetadataService(metadataUrl, "testdata/metadata_drive.json", fakeMounter, ".", metadataIDFirst).getMetadata()
assertTestResults(t, err, expectedServiceMetadata, actual)
}
func TestGetMetadataFromConfigDriveWhenServiceEndpointFails(t *testing.T) {
fakeMounter := getFakeMounter("/dev/sr0")
mockServer, err := createMockServer()
if err != nil {
t.Fatalf("unexpected error: %s", err)
}
defer mockServer.Close()
metadataUrl := fmt.Sprintf("%s/%s", mockServer.URL, "no/meta_data.json")
actual, err := newMetadataService(metadataUrl, "testdata/metadata_drive.json", fakeMounter, ".", metadataIDFirst).getMetadata()
assertTestResults(t, err, expectedDriveMetadata, actual)
}

View File

@ -0,0 +1,7 @@
{
"uuid": "01234567-cafe-babe-beef-0123456789ab",
"hostname": "test-server.config.drv",
"name": "test-server",
"availability_zone": "nova",
"project_id": "0123456789abcdeffedcba9876543210"
}

View File

@ -0,0 +1,7 @@
{
"uuid": "01234567-cafe-babe-beef-0123456789ab",
"hostname": "test-server.serv.ice",
"name": "test-server",
"availability_zone": "nova",
"project_id": "0123456789abcdeffedcba9876543210"
}

266
vendor/k8s.io/utils/exec/testing/fake_exec.go generated vendored Normal file
View File

@ -0,0 +1,266 @@
/*
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 testingexec
import (
"context"
"fmt"
"io"
"k8s.io/utils/exec"
)
// FakeExec is a simple scripted Interface type.
type FakeExec struct {
CommandScript []FakeCommandAction
CommandCalls int
LookPathFunc func(string) (string, error)
// ExactOrder enforces that commands are called in the order they are scripted,
// and with the exact same arguments
ExactOrder bool
// DisableScripts removes the requirement that a slice of FakeCommandAction be
// populated before calling Command(). This makes the fakeexec (and subsequent
// calls to Run() or CombinedOutput() always return success and there is no
// ability to set their output.
DisableScripts bool
}
var _ exec.Interface = &FakeExec{}
// FakeCommandAction is the function to be executed
type FakeCommandAction func(cmd string, args ...string) exec.Cmd
// Command is to track the commands that are executed
func (fake *FakeExec) Command(cmd string, args ...string) exec.Cmd {
if fake.DisableScripts {
fakeCmd := &FakeCmd{DisableScripts: true}
return InitFakeCmd(fakeCmd, cmd, args...)
}
if fake.CommandCalls > len(fake.CommandScript)-1 {
panic(fmt.Sprintf("ran out of Command() actions. Could not handle command [%d]: %s args: %v", fake.CommandCalls, cmd, args))
}
i := fake.CommandCalls
fake.CommandCalls++
fakeCmd := fake.CommandScript[i](cmd, args...)
if fake.ExactOrder {
argv := append([]string{cmd}, args...)
fc := fakeCmd.(*FakeCmd)
if cmd != fc.Argv[0] {
panic(fmt.Sprintf("received command: %s, expected: %s", cmd, fc.Argv[0]))
}
if len(argv) != len(fc.Argv) {
panic(fmt.Sprintf("command (%s) received with extra/missing arguments. Expected %v, Received %v", cmd, fc.Argv, argv))
}
for i, a := range argv[1:] {
if a != fc.Argv[i+1] {
panic(fmt.Sprintf("command (%s) called with unexpected argument. Expected %s, Received %s", cmd, fc.Argv[i+1], a))
}
}
}
return fakeCmd
}
// CommandContext wraps arguments into exec.Cmd
func (fake *FakeExec) CommandContext(ctx context.Context, cmd string, args ...string) exec.Cmd {
return fake.Command(cmd, args...)
}
// LookPath is for finding the path of a file
func (fake *FakeExec) LookPath(file string) (string, error) {
return fake.LookPathFunc(file)
}
// FakeCmd is a simple scripted Cmd type.
type FakeCmd struct {
Argv []string
CombinedOutputScript []FakeAction
CombinedOutputCalls int
CombinedOutputLog [][]string
OutputScript []FakeAction
OutputCalls int
OutputLog [][]string
RunScript []FakeAction
RunCalls int
RunLog [][]string
Dirs []string
Stdin io.Reader
Stdout io.Writer
Stderr io.Writer
Env []string
StdoutPipeResponse FakeStdIOPipeResponse
StderrPipeResponse FakeStdIOPipeResponse
WaitResponse error
StartResponse error
DisableScripts bool
}
var _ exec.Cmd = &FakeCmd{}
// InitFakeCmd is for creating a fake exec.Cmd
func InitFakeCmd(fake *FakeCmd, cmd string, args ...string) exec.Cmd {
fake.Argv = append([]string{cmd}, args...)
return fake
}
// FakeStdIOPipeResponse holds responses to use as fakes for the StdoutPipe and
// StderrPipe method calls
type FakeStdIOPipeResponse struct {
ReadCloser io.ReadCloser
Error error
}
// FakeAction is a function type
type FakeAction func() ([]byte, []byte, error)
// SetDir sets the directory
func (fake *FakeCmd) SetDir(dir string) {
fake.Dirs = append(fake.Dirs, dir)
}
// SetStdin sets the stdin
func (fake *FakeCmd) SetStdin(in io.Reader) {
fake.Stdin = in
}
// SetStdout sets the stdout
func (fake *FakeCmd) SetStdout(out io.Writer) {
fake.Stdout = out
}
// SetStderr sets the stderr
func (fake *FakeCmd) SetStderr(out io.Writer) {
fake.Stderr = out
}
// SetEnv sets the environment variables
func (fake *FakeCmd) SetEnv(env []string) {
fake.Env = env
}
// StdoutPipe returns an injected ReadCloser & error (via StdoutPipeResponse)
// to be able to inject an output stream on Stdout
func (fake *FakeCmd) StdoutPipe() (io.ReadCloser, error) {
return fake.StdoutPipeResponse.ReadCloser, fake.StdoutPipeResponse.Error
}
// StderrPipe returns an injected ReadCloser & error (via StderrPipeResponse)
// to be able to inject an output stream on Stderr
func (fake *FakeCmd) StderrPipe() (io.ReadCloser, error) {
return fake.StderrPipeResponse.ReadCloser, fake.StderrPipeResponse.Error
}
// Start mimicks starting the process (in the background) and returns the
// injected StartResponse
func (fake *FakeCmd) Start() error {
return fake.StartResponse
}
// Wait mimicks waiting for the process to exit returns the
// injected WaitResponse
func (fake *FakeCmd) Wait() error {
return fake.WaitResponse
}
// Run runs the command
func (fake *FakeCmd) Run() error {
if fake.DisableScripts {
return nil
}
if fake.RunCalls > len(fake.RunScript)-1 {
panic("ran out of Run() actions")
}
if fake.RunLog == nil {
fake.RunLog = [][]string{}
}
i := fake.RunCalls
fake.RunLog = append(fake.RunLog, append([]string{}, fake.Argv...))
fake.RunCalls++
stdout, stderr, err := fake.RunScript[i]()
if stdout != nil {
fake.Stdout.Write(stdout)
}
if stderr != nil {
fake.Stderr.Write(stderr)
}
return err
}
// CombinedOutput returns the output from the command
func (fake *FakeCmd) CombinedOutput() ([]byte, error) {
if fake.DisableScripts {
return []byte{}, nil
}
if fake.CombinedOutputCalls > len(fake.CombinedOutputScript)-1 {
panic("ran out of CombinedOutput() actions")
}
if fake.CombinedOutputLog == nil {
fake.CombinedOutputLog = [][]string{}
}
i := fake.CombinedOutputCalls
fake.CombinedOutputLog = append(fake.CombinedOutputLog, append([]string{}, fake.Argv...))
fake.CombinedOutputCalls++
stdout, _, err := fake.CombinedOutputScript[i]()
return stdout, err
}
// Output is the response from the command
func (fake *FakeCmd) Output() ([]byte, error) {
if fake.DisableScripts {
return []byte{}, nil
}
if fake.OutputCalls > len(fake.OutputScript)-1 {
panic("ran out of Output() actions")
}
if fake.OutputLog == nil {
fake.OutputLog = [][]string{}
}
i := fake.OutputCalls
fake.OutputLog = append(fake.OutputLog, append([]string{}, fake.Argv...))
fake.OutputCalls++
stdout, _, err := fake.OutputScript[i]()
return stdout, err
}
// Stop is to stop the process
func (fake *FakeCmd) Stop() {
// no-op
}
// FakeExitError is a simple fake ExitError type.
type FakeExitError struct {
Status int
}
var _ exec.ExitError = FakeExitError{}
func (fake FakeExitError) String() string {
return fmt.Sprintf("exit %d", fake.Status)
}
func (fake FakeExitError) Error() string {
return fake.String()
}
// Exited always returns true
func (fake FakeExitError) Exited() bool {
return true
}
// ExitStatus returns the fake status
func (fake FakeExitError) ExitStatus() int {
return fake.Status
}

1
vendor/modules.txt generated vendored
View File

@ -1657,6 +1657,7 @@ k8s.io/utils/buffer
k8s.io/utils/clock k8s.io/utils/clock
k8s.io/utils/clock/testing k8s.io/utils/clock/testing
k8s.io/utils/exec k8s.io/utils/exec
k8s.io/utils/exec/testing
k8s.io/utils/integer k8s.io/utils/integer
k8s.io/utils/internal/third_party/forked/golang/net k8s.io/utils/internal/third_party/forked/golang/net
k8s.io/utils/io k8s.io/utils/io