mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-30 15:05:27 +00:00
Merge pull request #13254 from soltysh/job_controller_kubectl
Job resource - kubectl part
This commit is contained in:
commit
b2c74a7297
@ -291,6 +291,7 @@ _kubectl_get()
|
||||
must_have_one_noun+=("endpoints")
|
||||
must_have_one_noun+=("event")
|
||||
must_have_one_noun+=("horizontalpodautoscaler")
|
||||
must_have_one_noun+=("job")
|
||||
must_have_one_noun+=("limitrange")
|
||||
must_have_one_noun+=("namespace")
|
||||
must_have_one_noun+=("node")
|
||||
@ -459,6 +460,7 @@ _kubectl_delete()
|
||||
must_have_one_noun+=("endpoints")
|
||||
must_have_one_noun+=("event")
|
||||
must_have_one_noun+=("horizontalpodautoscaler")
|
||||
must_have_one_noun+=("job")
|
||||
must_have_one_noun+=("limitrange")
|
||||
must_have_one_noun+=("namespace")
|
||||
must_have_one_noun+=("node")
|
||||
@ -829,6 +831,7 @@ _kubectl_label()
|
||||
must_have_one_noun+=("endpoints")
|
||||
must_have_one_noun+=("event")
|
||||
must_have_one_noun+=("horizontalpodautoscaler")
|
||||
must_have_one_noun+=("job")
|
||||
must_have_one_noun+=("limitrange")
|
||||
must_have_one_noun+=("namespace")
|
||||
must_have_one_noun+=("node")
|
||||
|
@ -36,6 +36,7 @@ type ExperimentalInterface interface {
|
||||
ScaleNamespacer
|
||||
DaemonSetsNamespacer
|
||||
DeploymentsNamespacer
|
||||
JobsNamespacer
|
||||
}
|
||||
|
||||
// ExperimentalClient is used to interact with experimental Kubernetes features.
|
||||
@ -90,6 +91,10 @@ func (c *ExperimentalClient) Deployments(namespace string) DeploymentInterface {
|
||||
return newDeployments(c, namespace)
|
||||
}
|
||||
|
||||
func (c *ExperimentalClient) Jobs(namespace string) JobInterface {
|
||||
return newJobs(c, namespace)
|
||||
}
|
||||
|
||||
// NewExperimental creates a new ExperimentalClient for the given config. This client
|
||||
// provides access to experimental Kubernetes features.
|
||||
// Experimental features are not supported and may be changed or removed in
|
||||
|
112
pkg/client/unversioned/jobs.go
Normal file
112
pkg/client/unversioned/jobs.go
Normal file
@ -0,0 +1,112 @@
|
||||
/*
|
||||
Copyright 2015 The Kubernetes Authors All rights reserved.
|
||||
|
||||
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 unversioned
|
||||
|
||||
import (
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/apis/experimental"
|
||||
"k8s.io/kubernetes/pkg/fields"
|
||||
"k8s.io/kubernetes/pkg/labels"
|
||||
"k8s.io/kubernetes/pkg/watch"
|
||||
)
|
||||
|
||||
// JobsNamespacer has methods to work with Job resources in a namespace
|
||||
type JobsNamespacer interface {
|
||||
Jobs(namespace string) JobInterface
|
||||
}
|
||||
|
||||
// JobInterface exposes methods to work on Job resources.
|
||||
type JobInterface interface {
|
||||
List(label labels.Selector, field fields.Selector) (*experimental.JobList, error)
|
||||
Get(name string) (*experimental.Job, error)
|
||||
Create(job *experimental.Job) (*experimental.Job, error)
|
||||
Update(job *experimental.Job) (*experimental.Job, error)
|
||||
Delete(name string, options *api.DeleteOptions) error
|
||||
Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error)
|
||||
UpdateStatus(job *experimental.Job) (*experimental.Job, error)
|
||||
}
|
||||
|
||||
// jobs implements JobsNamespacer interface
|
||||
type jobs struct {
|
||||
r *ExperimentalClient
|
||||
ns string
|
||||
}
|
||||
|
||||
// newJobs returns a jobs
|
||||
func newJobs(c *ExperimentalClient, namespace string) *jobs {
|
||||
return &jobs{c, namespace}
|
||||
}
|
||||
|
||||
// List returns a list of jobs that match the label and field selectors.
|
||||
func (c *jobs) List(label labels.Selector, field fields.Selector) (result *experimental.JobList, err error) {
|
||||
result = &experimental.JobList{}
|
||||
err = c.r.Get().Namespace(c.ns).Resource("jobs").LabelsSelectorParam(label).FieldsSelectorParam(field).Do().Into(result)
|
||||
return
|
||||
}
|
||||
|
||||
// Get returns information about a particular job.
|
||||
func (c *jobs) Get(name string) (result *experimental.Job, err error) {
|
||||
result = &experimental.Job{}
|
||||
err = c.r.Get().Namespace(c.ns).Resource("jobs").Name(name).Do().Into(result)
|
||||
return
|
||||
}
|
||||
|
||||
// Create creates a new job.
|
||||
func (c *jobs) Create(job *experimental.Job) (result *experimental.Job, err error) {
|
||||
result = &experimental.Job{}
|
||||
err = c.r.Post().Namespace(c.ns).Resource("jobs").Body(job).Do().Into(result)
|
||||
return
|
||||
}
|
||||
|
||||
// Update updates an existing job.
|
||||
func (c *jobs) Update(job *experimental.Job) (result *experimental.Job, err error) {
|
||||
result = &experimental.Job{}
|
||||
err = c.r.Put().Namespace(c.ns).Resource("jobs").Name(job.Name).Body(job).Do().Into(result)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete deletes a job, returns error if one occurs.
|
||||
func (c *jobs) Delete(name string, options *api.DeleteOptions) (err error) {
|
||||
if options == nil {
|
||||
return c.r.Delete().Namespace(c.ns).Resource("jobs").Name(name).Do().Error()
|
||||
}
|
||||
|
||||
body, err := api.Scheme.EncodeToVersion(options, c.r.APIVersion())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.r.Delete().Namespace(c.ns).Resource("jobs").Name(name).Body(body).Do().Error()
|
||||
}
|
||||
|
||||
// Watch returns a watch.Interface that watches the requested jobs.
|
||||
func (c *jobs) Watch(label labels.Selector, field fields.Selector, resourceVersion string) (watch.Interface, error) {
|
||||
return c.r.Get().
|
||||
Prefix("watch").
|
||||
Namespace(c.ns).
|
||||
Resource("jobs").
|
||||
Param("resourceVersion", resourceVersion).
|
||||
LabelsSelectorParam(label).
|
||||
FieldsSelectorParam(field).
|
||||
Watch()
|
||||
}
|
||||
|
||||
// UpdateStatus takes the name of the job and the new status. Returns the server's representation of the job, and an error, if it occurs.
|
||||
func (c *jobs) UpdateStatus(job *experimental.Job) (result *experimental.Job, err error) {
|
||||
result = &experimental.Job{}
|
||||
err = c.r.Put().Namespace(c.ns).Resource("jobs").Name(job.Name).SubResource("status").Body(job).Do().Into(result)
|
||||
return
|
||||
}
|
222
pkg/client/unversioned/jobs_test.go
Normal file
222
pkg/client/unversioned/jobs_test.go
Normal file
@ -0,0 +1,222 @@
|
||||
/*
|
||||
Copyright 2015 The Kubernetes Authors All rights reserved.
|
||||
|
||||
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 unversioned
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/testapi"
|
||||
"k8s.io/kubernetes/pkg/apis/experimental"
|
||||
"k8s.io/kubernetes/pkg/fields"
|
||||
"k8s.io/kubernetes/pkg/labels"
|
||||
)
|
||||
|
||||
func getJobResourceName() string {
|
||||
return "jobs"
|
||||
}
|
||||
|
||||
func TestListJobs(t *testing.T) {
|
||||
ns := api.NamespaceAll
|
||||
c := &testClient{
|
||||
Request: testRequest{
|
||||
Method: "GET",
|
||||
Path: testapi.Experimental.ResourcePath(getJobResourceName(), ns, ""),
|
||||
},
|
||||
Response: Response{StatusCode: 200,
|
||||
Body: &experimental.JobList{
|
||||
Items: []experimental.Job{
|
||||
{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "foo",
|
||||
Labels: map[string]string{
|
||||
"foo": "bar",
|
||||
"name": "baz",
|
||||
},
|
||||
},
|
||||
Spec: experimental.JobSpec{
|
||||
Template: &api.PodTemplateSpec{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
receivedJobList, err := c.Setup(t).Experimental().Jobs(ns).List(labels.Everything(), fields.Everything())
|
||||
c.Validate(t, receivedJobList, err)
|
||||
}
|
||||
|
||||
func TestGetJob(t *testing.T) {
|
||||
ns := api.NamespaceDefault
|
||||
c := &testClient{
|
||||
Request: testRequest{
|
||||
Method: "GET",
|
||||
Path: testapi.Experimental.ResourcePath(getJobResourceName(), ns, "foo"),
|
||||
Query: buildQueryValues(nil),
|
||||
},
|
||||
Response: Response{
|
||||
StatusCode: 200,
|
||||
Body: &experimental.Job{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "foo",
|
||||
Labels: map[string]string{
|
||||
"foo": "bar",
|
||||
"name": "baz",
|
||||
},
|
||||
},
|
||||
Spec: experimental.JobSpec{
|
||||
Template: &api.PodTemplateSpec{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
receivedJob, err := c.Setup(t).Experimental().Jobs(ns).Get("foo")
|
||||
c.Validate(t, receivedJob, err)
|
||||
}
|
||||
|
||||
func TestGetJobWithNoName(t *testing.T) {
|
||||
ns := api.NamespaceDefault
|
||||
c := &testClient{Error: true}
|
||||
receivedJob, err := c.Setup(t).Experimental().Jobs(ns).Get("")
|
||||
if (err != nil) && (err.Error() != nameRequiredError) {
|
||||
t.Errorf("Expected error: %v, but got %v", nameRequiredError, err)
|
||||
}
|
||||
|
||||
c.Validate(t, receivedJob, err)
|
||||
}
|
||||
|
||||
func TestUpdateJob(t *testing.T) {
|
||||
ns := api.NamespaceDefault
|
||||
requestJob := &experimental.Job{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "foo",
|
||||
Namespace: ns,
|
||||
ResourceVersion: "1",
|
||||
},
|
||||
}
|
||||
c := &testClient{
|
||||
Request: testRequest{
|
||||
Method: "PUT",
|
||||
Path: testapi.Experimental.ResourcePath(getJobResourceName(), ns, "foo"),
|
||||
Query: buildQueryValues(nil),
|
||||
},
|
||||
Response: Response{
|
||||
StatusCode: 200,
|
||||
Body: &experimental.Job{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "foo",
|
||||
Labels: map[string]string{
|
||||
"foo": "bar",
|
||||
"name": "baz",
|
||||
},
|
||||
},
|
||||
Spec: experimental.JobSpec{
|
||||
Template: &api.PodTemplateSpec{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
receivedJob, err := c.Setup(t).Experimental().Jobs(ns).Update(requestJob)
|
||||
c.Validate(t, receivedJob, err)
|
||||
}
|
||||
|
||||
func TestUpdateJobStatus(t *testing.T) {
|
||||
ns := api.NamespaceDefault
|
||||
requestJob := &experimental.Job{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "foo",
|
||||
Namespace: ns,
|
||||
ResourceVersion: "1",
|
||||
},
|
||||
}
|
||||
c := &testClient{
|
||||
Request: testRequest{
|
||||
Method: "PUT",
|
||||
Path: testapi.Experimental.ResourcePath(getJobResourceName(), ns, "foo") + "/status",
|
||||
Query: buildQueryValues(nil),
|
||||
},
|
||||
Response: Response{
|
||||
StatusCode: 200,
|
||||
Body: &experimental.Job{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "foo",
|
||||
Labels: map[string]string{
|
||||
"foo": "bar",
|
||||
"name": "baz",
|
||||
},
|
||||
},
|
||||
Spec: experimental.JobSpec{
|
||||
Template: &api.PodTemplateSpec{},
|
||||
},
|
||||
Status: experimental.JobStatus{
|
||||
Active: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
receivedJob, err := c.Setup(t).Experimental().Jobs(ns).UpdateStatus(requestJob)
|
||||
c.Validate(t, receivedJob, err)
|
||||
}
|
||||
|
||||
func TestDeleteJob(t *testing.T) {
|
||||
ns := api.NamespaceDefault
|
||||
c := &testClient{
|
||||
Request: testRequest{
|
||||
Method: "DELETE",
|
||||
Path: testapi.Experimental.ResourcePath(getJobResourceName(), ns, "foo"),
|
||||
Query: buildQueryValues(nil),
|
||||
},
|
||||
Response: Response{StatusCode: 200},
|
||||
}
|
||||
err := c.Setup(t).Experimental().Jobs(ns).Delete("foo", nil)
|
||||
c.Validate(t, nil, err)
|
||||
}
|
||||
|
||||
func TestCreateJob(t *testing.T) {
|
||||
ns := api.NamespaceDefault
|
||||
requestJob := &experimental.Job{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "foo",
|
||||
Namespace: ns,
|
||||
},
|
||||
}
|
||||
c := &testClient{
|
||||
Request: testRequest{
|
||||
Method: "POST",
|
||||
Path: testapi.Experimental.ResourcePath(getJobResourceName(), ns, ""),
|
||||
Body: requestJob,
|
||||
Query: buildQueryValues(nil),
|
||||
},
|
||||
Response: Response{
|
||||
StatusCode: 200,
|
||||
Body: &experimental.Job{
|
||||
ObjectMeta: api.ObjectMeta{
|
||||
Name: "foo",
|
||||
Labels: map[string]string{
|
||||
"foo": "bar",
|
||||
"name": "baz",
|
||||
},
|
||||
},
|
||||
Spec: experimental.JobSpec{
|
||||
Template: &api.PodTemplateSpec{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
receivedJob, err := c.Setup(t).Experimental().Jobs(ns).Create(requestJob)
|
||||
c.Validate(t, receivedJob, err)
|
||||
}
|
@ -261,3 +261,7 @@ func (c *FakeExperimental) Deployments(namespace string) client.DeploymentInterf
|
||||
func (c *FakeExperimental) Scales(namespace string) client.ScaleInterface {
|
||||
return &FakeScales{Fake: c, Namespace: namespace}
|
||||
}
|
||||
|
||||
func (c *FakeExperimental) Jobs(namespace string) client.JobInterface {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import (
|
||||
"github.com/golang/glog"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/api/resource"
|
||||
"k8s.io/kubernetes/pkg/apis/experimental"
|
||||
client "k8s.io/kubernetes/pkg/client/unversioned"
|
||||
"k8s.io/kubernetes/pkg/fieldpath"
|
||||
"k8s.io/kubernetes/pkg/fields"
|
||||
@ -85,6 +86,7 @@ func describerMap(c *client.Client) map[string]Describer {
|
||||
func expDescriberMap(c *client.Client) map[string]Describer {
|
||||
return map[string]Describer{
|
||||
"HorizontalPodAutoscaler": &HorizontalPodAutoscalerDescriber{c},
|
||||
"Job": &JobDescriber{c},
|
||||
}
|
||||
}
|
||||
|
||||
@ -854,6 +856,46 @@ func describeReplicationController(controller *api.ReplicationController, events
|
||||
})
|
||||
}
|
||||
|
||||
// JobDescriber generates information about a job and the pods it has created.
|
||||
type JobDescriber struct {
|
||||
client *client.Client
|
||||
}
|
||||
|
||||
func (d *JobDescriber) Describe(namespace, name string) (string, error) {
|
||||
job, err := d.client.Experimental().Jobs(namespace).Get(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
events, _ := d.client.Events(namespace).Search(job)
|
||||
|
||||
return describeJob(job, events)
|
||||
}
|
||||
|
||||
func describeJob(job *experimental.Job, events *api.EventList) (string, error) {
|
||||
return tabbedString(func(out io.Writer) error {
|
||||
fmt.Fprintf(out, "Name:\t%s\n", job.Name)
|
||||
fmt.Fprintf(out, "Namespace:\t%s\n", job.Namespace)
|
||||
if job.Spec.Template != nil {
|
||||
fmt.Fprintf(out, "Image(s):\t%s\n", makeImageList(&job.Spec.Template.Spec))
|
||||
} else {
|
||||
fmt.Fprintf(out, "Image(s):\t%s\n", "<no template>")
|
||||
}
|
||||
fmt.Fprintf(out, "Selector:\t%s\n", labels.FormatLabels(job.Spec.Selector))
|
||||
fmt.Fprintf(out, "Parallelism:\t%d\n", job.Spec.Parallelism)
|
||||
fmt.Fprintf(out, "Completions:\t%d\n", job.Spec.Completions)
|
||||
fmt.Fprintf(out, "Labels:\t%s\n", labels.FormatLabels(job.Labels))
|
||||
fmt.Fprintf(out, "Pods Statuses:\t%d Running / %d Succeeded / %d Failed\n", job.Status.Active, job.Status.Successful, job.Status.Unsuccessful)
|
||||
if job.Spec.Template != nil {
|
||||
describeVolumes(job.Spec.Template.Spec.Volumes, out)
|
||||
}
|
||||
if events != nil {
|
||||
DescribeEvents(events, out)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// SecretDescriber generates information about a secret
|
||||
type SecretDescriber struct {
|
||||
client.Interface
|
||||
|
@ -380,6 +380,7 @@ func (h *HumanReadablePrinter) HandledResources() []string {
|
||||
var podColumns = []string{"NAME", "READY", "STATUS", "RESTARTS", "AGE"}
|
||||
var podTemplateColumns = []string{"TEMPLATE", "CONTAINER(S)", "IMAGE(S)", "PODLABELS"}
|
||||
var replicationControllerColumns = []string{"CONTROLLER", "CONTAINER(S)", "IMAGE(S)", "SELECTOR", "REPLICAS", "AGE"}
|
||||
var jobColumns = []string{"JOB", "CONTAINER(S)", "IMAGE(S)", "SELECTOR", "SUCCESSFUL"}
|
||||
var serviceColumns = []string{"NAME", "CLUSTER_IP", "EXTERNAL_IP", "PORT(S)", "SELECTOR", "AGE"}
|
||||
var endpointColumns = []string{"NAME", "ENDPOINTS", "AGE"}
|
||||
var nodeColumns = []string{"NAME", "LABELS", "STATUS", "AGE"}
|
||||
@ -405,6 +406,8 @@ func (h *HumanReadablePrinter) addDefaultHandlers() {
|
||||
h.Handler(podTemplateColumns, printPodTemplateList)
|
||||
h.Handler(replicationControllerColumns, printReplicationController)
|
||||
h.Handler(replicationControllerColumns, printReplicationControllerList)
|
||||
h.Handler(jobColumns, printJob)
|
||||
h.Handler(jobColumns, printJobList)
|
||||
h.Handler(serviceColumns, printService)
|
||||
h.Handler(serviceColumns, printServiceList)
|
||||
h.Handler(endpointColumns, printEndpoints)
|
||||
@ -655,7 +658,6 @@ func printPodTemplateList(podList *api.PodTemplateList, w io.Writer, withNamespa
|
||||
func printReplicationController(controller *api.ReplicationController, w io.Writer, withNamespace bool, wide bool, showAll bool, columnLabels []string) error {
|
||||
name := controller.Name
|
||||
namespace := controller.Namespace
|
||||
|
||||
containers := controller.Spec.Template.Spec.Containers
|
||||
var firstContainer api.Container
|
||||
if len(containers) > 0 {
|
||||
@ -707,6 +709,33 @@ func printReplicationControllerList(list *api.ReplicationControllerList, w io.Wr
|
||||
return nil
|
||||
}
|
||||
|
||||
func printJob(job *experimental.Job, w io.Writer, withNamespace bool, wide bool, showAll bool, columnLabels []string) error {
|
||||
containers := job.Spec.Template.Spec.Containers
|
||||
var firstContainer api.Container
|
||||
if len(containers) > 0 {
|
||||
firstContainer = containers[0]
|
||||
}
|
||||
_, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\n",
|
||||
job.Name,
|
||||
firstContainer.Name,
|
||||
firstContainer.Image,
|
||||
labels.FormatLabels(job.Spec.Selector),
|
||||
job.Status.Successful)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printJobList(list *experimental.JobList, w io.Writer, withNamespace bool, wide bool, showAll bool, columnLabels []string) error {
|
||||
for _, job := range list.Items {
|
||||
if err := printJob(&job, w, withNamespace, wide, showAll, columnLabels); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getServiceExternalIP(svc *api.Service) string {
|
||||
switch svc.Spec.Type {
|
||||
case api.ServiceTypeClusterIP:
|
||||
|
@ -14,6 +14,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package controller provides Registry interface and it's RESTStorage
|
||||
// Package job provides Registry interface and it's RESTStorage
|
||||
// implementation for storing Job api objects.
|
||||
package job
|
||||
|
@ -28,7 +28,7 @@ import (
|
||||
"k8s.io/kubernetes/pkg/storage"
|
||||
)
|
||||
|
||||
// rest implements a RESTStorage for jobs against etcd
|
||||
// REST implements a RESTStorage for jobs against etcd
|
||||
type REST struct {
|
||||
*etcdgeneric.Etcd
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user