Move runtime to core/runtime
Signed-off-by: Derek McGowan <derek@mcg.dev>
This commit is contained in:
		
							
								
								
									
										219
									
								
								core/runtime/restart/restart.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								core/runtime/restart/restart.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,219 @@ | ||||
| /* | ||||
|    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 restart enables containers to have labels added and monitored to | ||||
| // keep the container's task running if it is killed. | ||||
| // | ||||
| // Setting the StatusLabel on a container instructs the restart monitor to keep | ||||
| // that container's task in a specific status. | ||||
| // Setting the LogPathLabel on a container will setup the task's IO to be redirected | ||||
| // to a log file when running a task within the restart manager. | ||||
| // | ||||
| // The restart labels can be cleared off of a container using the WithNoRestarts Opt. | ||||
| // | ||||
| // The restart monitor has one option in the containerd config under the [plugins.restart] | ||||
| // section.  `interval = "10s" sets the reconcile interval that the restart monitor checks | ||||
| // for task state and reconciles the desired status for that task. | ||||
| package restart | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"net/url" | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	containerd "github.com/containerd/containerd/v2/client" | ||||
| 	"github.com/containerd/containerd/v2/core/containers" | ||||
| 	"github.com/containerd/containerd/v2/pkg/cio" | ||||
| 	"github.com/containerd/log" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	// StatusLabel sets the restart status label for a container | ||||
| 	StatusLabel = "containerd.io/restart.status" | ||||
| 	// LogURILabel sets the restart log uri label for a container | ||||
| 	LogURILabel = "containerd.io/restart.loguri" | ||||
|  | ||||
| 	// PolicyLabel sets the restart policy label for a container | ||||
| 	PolicyLabel = "containerd.io/restart.policy" | ||||
| 	// CountLabel sets the restart count label for a container | ||||
| 	CountLabel = "containerd.io/restart.count" | ||||
| 	// ExplicitlyStoppedLabel sets the restart explicitly stopped label for a container | ||||
| 	ExplicitlyStoppedLabel = "containerd.io/restart.explicitly-stopped" | ||||
| ) | ||||
|  | ||||
| // Policy represents the restart policies of a container. | ||||
| type Policy struct { | ||||
| 	name              string | ||||
| 	maximumRetryCount int | ||||
| } | ||||
|  | ||||
| // NewPolicy creates a restart policy with the specified name. | ||||
| // supports the following restart policies: | ||||
| // - no, Do not restart the container. | ||||
| // - always, Always restart the container regardless of the exit status. | ||||
| // - on-failure[:max-retries], Restart only if the container exits with a non-zero exit status. | ||||
| // - unless-stopped, Always restart the container unless it is stopped. | ||||
| func NewPolicy(policy string) (*Policy, error) { | ||||
| 	policySlice := strings.Split(policy, ":") | ||||
| 	var ( | ||||
| 		err        error | ||||
| 		retryCount int | ||||
| 	) | ||||
| 	switch policySlice[0] { | ||||
| 	case "", "no", "always", "unless-stopped": | ||||
| 		policy = policySlice[0] | ||||
| 		if policy == "" { | ||||
| 			policy = "always" | ||||
| 		} | ||||
| 		if len(policySlice) > 1 { | ||||
| 			return nil, fmt.Errorf("restart policy %q not support max retry count", policySlice[0]) | ||||
| 		} | ||||
| 	case "on-failure": | ||||
| 		policy = policySlice[0] | ||||
| 		if len(policySlice) > 1 { | ||||
| 			retryCount, err = strconv.Atoi(policySlice[1]) | ||||
| 			if err != nil { | ||||
| 				return nil, fmt.Errorf("invalid max retry count: %s", policySlice[1]) | ||||
| 			} | ||||
| 		} | ||||
| 	default: | ||||
| 		return nil, fmt.Errorf("restart policy %q not supported", policy) | ||||
| 	} | ||||
| 	return &Policy{ | ||||
| 		name:              policy, | ||||
| 		maximumRetryCount: retryCount, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (rp *Policy) String() string { | ||||
| 	if rp.maximumRetryCount > 0 { | ||||
| 		return fmt.Sprintf("%s:%d", rp.name, rp.maximumRetryCount) | ||||
| 	} | ||||
| 	return rp.name | ||||
| } | ||||
|  | ||||
| func (rp *Policy) Name() string { | ||||
| 	return rp.name | ||||
| } | ||||
|  | ||||
| func (rp *Policy) MaximumRetryCount() int { | ||||
| 	return rp.maximumRetryCount | ||||
| } | ||||
|  | ||||
| // Reconcile reconciles the restart policy of a container. | ||||
| func Reconcile(status containerd.Status, labels map[string]string) bool { | ||||
| 	rp, err := NewPolicy(labels[PolicyLabel]) | ||||
| 	if err != nil { | ||||
| 		log.L.WithError(err).Error("policy reconcile") | ||||
| 		return false | ||||
| 	} | ||||
| 	switch rp.Name() { | ||||
| 	case "", "always": | ||||
| 		return true | ||||
| 	case "on-failure": | ||||
| 		restartCount, err := strconv.Atoi(labels[CountLabel]) | ||||
| 		if err != nil && labels[CountLabel] != "" { | ||||
| 			log.L.WithError(err).Error("policy reconcile") | ||||
| 			return false | ||||
| 		} | ||||
| 		if status.ExitStatus != 0 && (rp.maximumRetryCount == 0 || restartCount < rp.maximumRetryCount) { | ||||
| 			return true | ||||
| 		} | ||||
| 	case "unless-stopped": | ||||
| 		explicitlyStopped, _ := strconv.ParseBool(labels[ExplicitlyStoppedLabel]) | ||||
| 		if !explicitlyStopped { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // WithLogURI sets the specified log uri for a container. | ||||
| func WithLogURI(uri *url.URL) func(context.Context, *containerd.Client, *containers.Container) error { | ||||
| 	return WithLogURIString(uri.String()) | ||||
| } | ||||
|  | ||||
| // WithLogURIString sets the specified log uri string for a container. | ||||
| func WithLogURIString(uriString string) func(context.Context, *containerd.Client, *containers.Container) error { | ||||
| 	return func(_ context.Context, _ *containerd.Client, c *containers.Container) error { | ||||
| 		ensureLabels(c) | ||||
| 		c.Labels[LogURILabel] = uriString | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // WithBinaryLogURI sets the binary-type log uri for a container. | ||||
| // | ||||
| // Deprecated(in release 1.5): use WithLogURI | ||||
| func WithBinaryLogURI(binary string, args map[string]string) func(context.Context, *containerd.Client, *containers.Container) error { | ||||
| 	uri, err := cio.LogURIGenerator("binary", binary, args) | ||||
| 	if err != nil { | ||||
| 		return func(context.Context, *containerd.Client, *containers.Container) error { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	return WithLogURI(uri) | ||||
| } | ||||
|  | ||||
| // WithFileLogURI sets the file-type log uri for a container. | ||||
| // | ||||
| // Deprecated(in release 1.5): use WithLogURI | ||||
| func WithFileLogURI(path string) func(context.Context, *containerd.Client, *containers.Container) error { | ||||
| 	uri, err := cio.LogURIGenerator("file", path, nil) | ||||
| 	if err != nil { | ||||
| 		return func(context.Context, *containerd.Client, *containers.Container) error { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 	return WithLogURI(uri) | ||||
| } | ||||
|  | ||||
| // WithStatus sets the status for a container | ||||
| func WithStatus(status containerd.ProcessStatus) func(context.Context, *containerd.Client, *containers.Container) error { | ||||
| 	return func(_ context.Context, _ *containerd.Client, c *containers.Container) error { | ||||
| 		ensureLabels(c) | ||||
| 		c.Labels[StatusLabel] = string(status) | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // WithPolicy sets the restart policy for a container | ||||
| func WithPolicy(policy *Policy) func(context.Context, *containerd.Client, *containers.Container) error { | ||||
| 	return func(_ context.Context, _ *containerd.Client, c *containers.Container) error { | ||||
| 		ensureLabels(c) | ||||
| 		c.Labels[PolicyLabel] = policy.String() | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // WithNoRestarts clears any restart information from the container | ||||
| func WithNoRestarts(_ context.Context, _ *containerd.Client, c *containers.Container) error { | ||||
| 	if c.Labels == nil { | ||||
| 		return nil | ||||
| 	} | ||||
| 	delete(c.Labels, StatusLabel) | ||||
| 	delete(c.Labels, PolicyLabel) | ||||
| 	delete(c.Labels, LogURILabel) | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func ensureLabels(c *containers.Container) { | ||||
| 	if c.Labels == nil { | ||||
| 		c.Labels = make(map[string]string) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										221
									
								
								core/runtime/restart/restart_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										221
									
								
								core/runtime/restart/restart_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,221 @@ | ||||
| /* | ||||
|    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 restart | ||||
|  | ||||
| import ( | ||||
| 	"testing" | ||||
|  | ||||
| 	containerd "github.com/containerd/containerd/v2/client" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestNewRestartPolicy(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		policy string | ||||
| 		want   *Policy | ||||
| 	}{ | ||||
| 		{ | ||||
| 			policy: "unknow", | ||||
| 			want:   nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			policy: "", | ||||
| 			want:   &Policy{name: "always"}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			policy: "always", | ||||
| 			want:   &Policy{name: "always"}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			policy: "always:3", | ||||
| 			want:   nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			policy: "on-failure", | ||||
| 			want:   &Policy{name: "on-failure"}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			policy: "on-failure:10", | ||||
| 			want: &Policy{ | ||||
| 				name:              "on-failure", | ||||
| 				maximumRetryCount: 10, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			policy: "unless-stopped", | ||||
| 			want: &Policy{ | ||||
| 				name: "unless-stopped", | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, testCase := range tests { | ||||
| 		result, _ := NewPolicy(testCase.policy) | ||||
| 		assert.Equal(t, testCase.want, result) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestRestartPolicyToString(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		policy string | ||||
| 		want   string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			policy: "", | ||||
| 			want:   "always", | ||||
| 		}, | ||||
| 		{ | ||||
| 			policy: "always", | ||||
| 			want:   "always", | ||||
| 		}, | ||||
| 		{ | ||||
| 			policy: "on-failure", | ||||
| 			want:   "on-failure", | ||||
| 		}, | ||||
| 		{ | ||||
| 			policy: "on-failure:10", | ||||
| 			want:   "on-failure:10", | ||||
| 		}, | ||||
| 		{ | ||||
| 			policy: "unless-stopped", | ||||
| 			want:   "unless-stopped", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, testCase := range tests { | ||||
| 		policy, err := NewPolicy(testCase.policy) | ||||
| 		if err != nil { | ||||
| 			t.Fatal(err) | ||||
| 		} | ||||
| 		result := policy.String() | ||||
| 		assert.Equal(t, testCase.want, result) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestRestartPolicyReconcile(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		status containerd.Status | ||||
| 		labels map[string]string | ||||
| 		want   bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			status: containerd.Status{ | ||||
| 				Status: containerd.Stopped, | ||||
| 			}, | ||||
| 			labels: map[string]string{ | ||||
| 				PolicyLabel: "always", | ||||
| 			}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			status: containerd.Status{ | ||||
| 				Status: containerd.Unknown, | ||||
| 			}, | ||||
| 			labels: map[string]string{ | ||||
| 				PolicyLabel: "always", | ||||
| 			}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			status: containerd.Status{ | ||||
| 				Status: containerd.Stopped, | ||||
| 			}, | ||||
| 			labels: map[string]string{ | ||||
| 				PolicyLabel: "on-failure:10", | ||||
| 				CountLabel:  "1", | ||||
| 			}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			status: containerd.Status{ | ||||
| 				Status:     containerd.Unknown, | ||||
| 				ExitStatus: 1, | ||||
| 			}, | ||||
| 			// test without count label | ||||
| 			labels: map[string]string{ | ||||
| 				PolicyLabel: "on-failure:10", | ||||
| 			}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			status: containerd.Status{ | ||||
| 				Status:     containerd.Unknown, | ||||
| 				ExitStatus: 1, | ||||
| 			}, | ||||
| 			// test without valid count label | ||||
| 			labels: map[string]string{ | ||||
| 				PolicyLabel: "on-failure:10", | ||||
| 				CountLabel:  "invalid", | ||||
| 			}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			status: containerd.Status{ | ||||
| 				Status:     containerd.Unknown, | ||||
| 				ExitStatus: 1, | ||||
| 			}, | ||||
| 			labels: map[string]string{ | ||||
| 				PolicyLabel: "on-failure:10", | ||||
| 				CountLabel:  "1", | ||||
| 			}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			status: containerd.Status{ | ||||
| 				Status:     containerd.Unknown, | ||||
| 				ExitStatus: 1, | ||||
| 			}, | ||||
| 			labels: map[string]string{ | ||||
| 				PolicyLabel: "on-failure:3", | ||||
| 				CountLabel:  "3", | ||||
| 			}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			status: containerd.Status{ | ||||
| 				Status: containerd.Unknown, | ||||
| 			}, | ||||
| 			labels: map[string]string{ | ||||
| 				PolicyLabel: "unless-stopped", | ||||
| 			}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			status: containerd.Status{ | ||||
| 				Status: containerd.Stopped, | ||||
| 			}, | ||||
| 			labels: map[string]string{ | ||||
| 				PolicyLabel: "unless-stopped", | ||||
| 			}, | ||||
| 			want: true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			status: containerd.Status{ | ||||
| 				Status: containerd.Stopped, | ||||
| 			}, | ||||
| 			labels: map[string]string{ | ||||
| 				PolicyLabel:            "unless-stopped", | ||||
| 				ExplicitlyStoppedLabel: "true", | ||||
| 			}, | ||||
| 			want: false, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, testCase := range tests { | ||||
| 		result := Reconcile(testCase.status, testCase.labels) | ||||
| 		assert.Equal(t, testCase.want, result, testCase) | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Derek McGowan
					Derek McGowan